From 6ab7947358b92ff28c97978ab3528db144406400 Mon Sep 17 00:00:00 2001 From: Bert Hekman Date: Mon, 11 Jul 2022 22:07:52 +0200 Subject: [PATCH 1/3] Add media player widget which can display album art and play/pause icons depending on the playback status; Add media player commands --- README.md | 43 ++++++++++ config.go | 13 +-- deck.go | 37 ++++++++ go.mod | 1 + go.sum | 4 +- main.go | 29 ++++++- media_player.go | 104 +++++++++++++++++++++++ media_player_handler.go | 9 ++ media_player_status.go | 72 ++++++++++++++++ media_players.go | 182 ++++++++++++++++++++++++++++++++++++++++ widget.go | 3 + widget_media_player.go | 160 +++++++++++++++++++++++++++++++++++ 12 files changed, 647 insertions(+), 10 deletions(-) create mode 100644 media_player.go create mode 100644 media_player_handler.go create mode 100644 media_player_status.go create mode 100644 media_players.go create mode 100644 widget_media_player.go diff --git a/README.md b/README.md index 7c86da7..96cc400 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,26 @@ corresponding icons with correct names need to be placed in `~/.local/share/deckmaster/themes/[theme]`. The default icons with their respective names can be found [here](https://github.com/muesli/deckmaster/tree/master/assets/weather). +#### Media player + +A widget that displays media player information. + +```toml +[keys.widget] + id = "mediaPlayer" + [keys.widget.config] + mode = "art" # optional + player_name = true # optional + icon_playing = "images/stop.jpg" + icon_paused = "images/play.jpg" + icon_no_player = "images/no-player.jpg" +``` + +The supported modes are `art` (default) and `playback status`. If `playback +status` has been chosen, the icons `icon_playing`, `icon_paused` or +`icon_no_player` will be shown depending on the playback status of the selected +media player. + ### Actions You can hook up any key with several actions. A regular keypress will trigger @@ -369,6 +389,29 @@ pressed: device = "sleep" ``` +#### Media player actions + +Control playback: + +```toml +[keys.action] + media_player = "play_pause" +``` + +Supported playback actions are `play`, `play_pause`, `stop`, `previous` and +`next`. + +Cycle through players: + +```toml +[keys.action] + media_player = "select+" +``` + +Use `select+` to select the next player to be active and `select-` to select +the previous player. + + ### Background Image You can configure each deck to display an individual wallpaper behind its diff --git a/config.go b/config.go index e21add0..cd3ae98 100644 --- a/config.go +++ b/config.go @@ -25,12 +25,13 @@ type DBusConfig struct { // ActionConfig describes an action that can be triggered. type ActionConfig struct { - Deck string `toml:"deck,omitempty"` - Keycode string `toml:"keycode,omitempty"` - Exec string `toml:"exec,omitempty"` - Paste string `toml:"paste,omitempty"` - Device string `toml:"device,omitempty"` - DBus DBusConfig `toml:"dbus,omitempty"` + Deck string `toml:"deck,omitempty"` + Keycode string `toml:"keycode,omitempty"` + Exec string `toml:"exec,omitempty"` + Paste string `toml:"paste,omitempty"` + Device string `toml:"device,omitempty"` + DBus DBusConfig `toml:"dbus,omitempty"` + MediaPlayer string `toml:"media_player,omitempty"` } // WidgetConfig describes configuration data for widgets. diff --git a/deck.go b/deck.go index a260afb..4efaecb 100644 --- a/deck.go +++ b/deck.go @@ -264,6 +264,43 @@ func (d *Deck) triggerAction(dev *streamdeck.Device, index uint8, hold bool) { fmt.Fprintln(os.Stderr, "Unrecognized special action:", a.Device) } } + if a.MediaPlayer != "" { + switch { + case a.MediaPlayer == "play": + if player := mediaPlayers.ActivePlayer(); player != nil { + player.Play() + } + + case a.MediaPlayer == "play_pause": + if player := mediaPlayers.ActivePlayer(); player != nil { + player.PlayPause() + } + + case a.MediaPlayer == "stop": + if player := mediaPlayers.ActivePlayer(); player != nil { + player.Stop() + } + + case a.MediaPlayer == "previous": + if player := mediaPlayers.ActivePlayer(); player != nil { + player.Previous() + } + + case a.MediaPlayer == "next": + if player := mediaPlayers.ActivePlayer(); player != nil { + player.Next() + } + + case a.MediaPlayer == "select+": + mediaPlayers.SelectPlayer(1) + + case a.MediaPlayer == "select-": + mediaPlayers.SelectPlayer(-1) + + default: + fmt.Fprintln(os.Stderr, "Unrecognized media player action:", a.MediaPlayer) + } + } } } diff --git a/go.mod b/go.mod index 48c61d7..03d18ab 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/flopp/go-findfont v0.1.0 github.com/godbus/dbus v4.1.0+incompatible + github.com/godbus/dbus/v5 v5.1.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 diff --git a/go.sum b/go.sum index 50f1ab9..365ba89 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -31,8 +33,6 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8= -github.com/muesli/streamdeck v0.3.0 h1:BrUXEPtzDy9hO/l2ZLMKsik0VrNDwt8hHRomKq0NlFQ= -github.com/muesli/streamdeck v0.3.0/go.mod h1:iBrvujOQ0WXWBJfi25gFOj/AVgq7FKfjPLi5AISKZx8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/main.go b/main.go index c808619..1aa2d4d 100644 --- a/main.go +++ b/main.go @@ -35,6 +35,8 @@ var ( xorg *Xorg recentWindows []Window + mediaPlayers *MediaPlayers + deckFile = flag.String("deck", "main.deck", "path to deck config file") device = flag.String("device", "", "which device to use (serial number)") brightness = flag.Uint("brightness", 80, "brightness in percent") @@ -81,7 +83,7 @@ func expandPath(base, path string) (string, error) { return filepath.Abs(path) } -func eventLoop(dev *streamdeck.Device, tch chan interface{}) error { +func eventLoop(dev *streamdeck.Device, tch chan interface{}, mch chan interface{}) error { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) @@ -145,6 +147,18 @@ func eventLoop(dev *streamdeck.Device, tch chan interface{}) error { handleActiveWindowChanged(dev, event) } + case e := <-mch: + switch event := e.(type) { + case MediaPlayerStatusChanged: + fmt.Fprintf(os.Stderr, "Media player event: %T %+v\n", event, event) + handleMediaPlayerStatusChanged() + case ActiveMediaPlayerChanged: + fmt.Fprintf(os.Stderr, "Media player event: %T %+v\n", event, event) + handleMediaPlayerActivePlayerChanged() + default: + fmt.Fprintf(os.Stderr, "Invalid event: %T %+v\n", event, event) + } + case err := <-shutdown: return err @@ -275,6 +289,17 @@ func run() error { defer keyboard.Close() //nolint:errcheck } + // initialize mediaPlayer + mch := make(chan interface{}, 20) + mediaPlayers, err = NewMediaPlayers(mch) + if err != nil { + return fmt.Errorf("Error while initializing media players: %s", err) + } + err = mediaPlayers.Run() + if err != nil { + return fmt.Errorf("Error while running media players: %s", err) + } + // load deck deck, err = LoadDeck(dev, ".", *deckFile) if err != nil { @@ -282,7 +307,7 @@ func run() error { } deck.updateWidgets() - return eventLoop(dev, tch) + return eventLoop(dev, tch, mch) } func main() { diff --git a/media_player.go b/media_player.go new file mode 100644 index 0000000..d67b4aa --- /dev/null +++ b/media_player.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "os" + "sync" + + "github.com/godbus/dbus/v5" +) + +type MediaPlayer struct { + conn *dbus.Conn + eventCh chan<- interface{} + busName string + ownerName string + + name string + busObj dbus.BusObject + status MediaPlayerStatus + + mutex sync.RWMutex +} + +func NewMediaPlayer(conn *dbus.Conn, eventCh chan<- interface{}, busName string, ownerName string) *MediaPlayer { + return &MediaPlayer{ + conn: conn, + eventCh: eventCh, + busName: busName, + ownerName: ownerName, + } +} + +func (p *MediaPlayer) Initialize() { + p.mutex.Lock() + defer p.mutex.Unlock() + + p.busObj = p.conn.Object(p.busName, "/org/mpris/MediaPlayer2") + p.name = p.busName + + var identity string + p.busObj.Call("org.freedesktop.DBus.Properties.Get", 0, "org.mpris.MediaPlayer2", "Identity").Store(&identity) + if len(identity) != 0 { + p.name = identity + } + + playbackStatus, err := p.busObj.GetProperty("org.mpris.MediaPlayer2.Player.PlaybackStatus") + if err != nil { + fmt.Fprintf(os.Stderr, "error while getting playback status for player %s: %s", p.name, err) + } else { + if variant, ok := playbackStatus.Value().(string); ok { + p.status.UpdatePlaybackStatus(variant) + } + } + + metadata, err := p.busObj.GetProperty("org.mpris.MediaPlayer2.Player.Metadata") + if err != nil { + fmt.Fprintf(os.Stderr, "error while getting metadata for player %s: %s", p.name, err) + } else { + if variant, ok := metadata.Value().(map[string]dbus.Variant); ok { + p.status.UpdateFromMetadata(variant) + } + } + + p.busObj.AddMatchSignal("org.freedesktop.DBus.Properties", "PropertiesChanged") +} + +func (p *MediaPlayer) Close() { + p.busObj.RemoveMatchSignal("org.freedesktop.DBus.Properties", "PropertiesChanged") +} + +func (p *MediaPlayer) Stop() { + p.busObj.Call("org.mpris.MediaPlayer2.Player.Stop", 0).Store() +} + +func (p *MediaPlayer) Play() { + p.busObj.Call("org.mpris.MediaPlayer2.Player.Play", 0).Store() +} + +func (p *MediaPlayer) PlayPause() { + p.busObj.Call("org.mpris.MediaPlayer2.Player.PlayPause", 0).Store() +} + +func (p *MediaPlayer) Previous() { + p.busObj.Call("org.mpris.MediaPlayer2.Player.Previous", 0).Store() +} + +func (p *MediaPlayer) Next() { + p.busObj.Call("org.mpris.MediaPlayer2.Player.Next", 0).Store() +} + +func (p *MediaPlayer) Status() MediaPlayerStatus { + p.mutex.RLock() + defer p.mutex.RUnlock() + + return p.status +} + +func (p *MediaPlayer) onPropertiesChanged(propertiesVariant map[string]dbus.Variant) { + p.mutex.Lock() + defer p.mutex.Unlock() + + p.status.UpdateFromPropertiesVariant(propertiesVariant) + p.eventCh <- MediaPlayerStatusChanged{PlayerName: p.name, Status: p.status} +} diff --git a/media_player_handler.go b/media_player_handler.go new file mode 100644 index 0000000..aa4d2e6 --- /dev/null +++ b/media_player_handler.go @@ -0,0 +1,9 @@ +package main + +func handleMediaPlayerStatusChanged() { + deck.updateWidgets() +} + +func handleMediaPlayerActivePlayerChanged() { + deck.updateWidgets() +} diff --git a/media_player_status.go b/media_player_status.go new file mode 100644 index 0000000..7670f4c --- /dev/null +++ b/media_player_status.go @@ -0,0 +1,72 @@ +package main + +import "github.com/godbus/dbus/v5" + +type MediaPlayerStatus struct { + status string + + artist string + albumArtist string + album string + title string + trackNumber int + artURL string +} + +func (s *MediaPlayerStatus) UpdateFromPropertiesVariant(p map[string]dbus.Variant) { + if variant, found := p["PlaybackStatus"]; found { + if val, ok := variant.Value().(string); ok { + s.UpdatePlaybackStatus(val) + } + } + if variant, found := p["Metadata"]; found { + if metadata, ok := variant.Value().(map[string]dbus.Variant); ok { + s.UpdateFromMetadata(metadata) + } + } +} + +func (s *MediaPlayerStatus) UpdatePlaybackStatus(status string) { + s.status = status +} + +func (s *MediaPlayerStatus) UpdateFromMetadata(metadata map[string]dbus.Variant) { + s.artist = s.getMetaFirstOrEmptyString(metadata, "xesam:artist") + s.albumArtist = s.getMetaFirstOrEmptyString(metadata, "xesam:albumArtist") + s.album = s.getMetaOrEmptyString(metadata, "xesam:album") + s.title = s.getMetaOrEmptyString(metadata, "xesam:title") + s.trackNumber = s.getMetaOrZero(metadata, "xesam:trackNumber") + s.artURL = s.getMetaOrEmptyString(metadata, "mpris:artUrl") +} + +func (s *MediaPlayerStatus) getMetaOrEmptyString(metadata map[string]dbus.Variant, key string) string { + if variant, ok := metadata[key]; ok { + if val, ok := variant.Value().(string); ok { + return val + } + } + + return "" +} + +func (s *MediaPlayerStatus) getMetaOrZero(metadata map[string]dbus.Variant, key string) int { + if variant, ok := metadata[key]; ok { + if val, ok := variant.Value().(int32); ok { + return int(val) + } + } + + return 0 +} + +func (s *MediaPlayerStatus) getMetaFirstOrEmptyString(metadata map[string]dbus.Variant, key string) string { + if variant, ok := metadata[key]; ok { + if val, ok := variant.Value().([]string); ok { + if len(val) != 0 { + return val[0] + } + } + } + + return "" +} diff --git a/media_players.go b/media_players.go new file mode 100644 index 0000000..c6c62fe --- /dev/null +++ b/media_players.go @@ -0,0 +1,182 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/godbus/dbus/v5" +) + +type ActiveMediaPlayerChanged struct { + PlayerName string +} + +type MediaPlayerStatusChanged struct { + PlayerName string + Status MediaPlayerStatus +} + +type MediaPlayers struct { + conn *dbus.Conn + eventCh chan<- interface{} + players map[string]*MediaPlayer + activePlayer *string +} + +func NewMediaPlayers(eventCh chan<- interface{}) (*MediaPlayers, error) { + conn, err := dbus.SessionBus() + if err != nil { + return nil, err + } + return &MediaPlayers{ + conn: conn, + eventCh: eventCh, + players: make(map[string]*MediaPlayer), + }, nil +} + +func (m *MediaPlayers) Close() error { + return m.conn.Close() +} + +func (m *MediaPlayers) Run() error { + go func() { + var names []string + err := m.conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names) + if err != nil { + fmt.Fprintf(os.Stderr, "Error while listing all media players: %s", err) + } + + for _, name := range names { + if strings.HasPrefix(name, "org.mpris.MediaPlayer2.") { + ownerName := "" + err = m.conn.BusObject().Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&ownerName) + if err != nil { + fmt.Fprintf(os.Stderr, "Error while getting owner name for media player: %s", err) + } else { + m.addPlayer(name, ownerName) + } + } + } + + sigCh := make(chan *dbus.Signal) + + m.conn.AddMatchSignal(dbus.WithMatchMember("NameOwnerChanged")) + m.conn.Signal(sigCh) + + for signal := range sigCh { + m.handleSignal(signal) + } + }() + + return nil +} + +func (m *MediaPlayers) ActivePlayer() *MediaPlayer { + if m.activePlayer == nil { + return nil + } + return m.players[*m.activePlayer] +} + +func (m *MediaPlayers) SelectPlayer(offset int) { + if len(m.players) < 2 { + return + } + + var keyList []string + current := -1 + index := 0 + for key := range m.players { + keyList = append(keyList, key) + if key == *m.activePlayer { + current = index + } + index++ + } + + newIndex := (((current + offset) % index) + index) % index + m.activePlayer = &keyList[newIndex] + m.eventCh <- ActiveMediaPlayerChanged{PlayerName: m.players[*m.activePlayer].name} +} + +func (m *MediaPlayers) handleSignal(signal *dbus.Signal) { + switch signal.Name { + case "org.freedesktop.DBus.NameOwnerChanged": + busName := signal.Body[0].(string) + oldOwnerName := signal.Body[1].(string) + newOwnerName := signal.Body[2].(string) + if !strings.HasPrefix(busName, "org.mpris.MediaPlayer2.") { + return + } + if len(newOwnerName) != 0 && len(oldOwnerName) == 0 { + m.addPlayer(busName, newOwnerName) + } else if len(oldOwnerName) != 0 && len(newOwnerName) == 0 { + m.removePlayer(busName, oldOwnerName) + } else { + m.changePlayerOwner(busName, oldOwnerName, newOwnerName) + } + case "org.freedesktop.DBus.Properties.PropertiesChanged": + properties := signal.Body[1].(map[string]dbus.Variant) + if player, ok := m.players[signal.Sender]; ok { + player.onPropertiesChanged(properties) + } + default: + verbosef("Unknown signal: %+v\n", signal) + } +} + +func (m *MediaPlayers) addPlayer(busName string, ownerName string) { + verbosef("Adding new player %s owner %s", busName, ownerName) + + player := NewMediaPlayer(m.conn, m.eventCh, busName, ownerName) + player.Initialize() + + m.players[ownerName] = player + + if m.activePlayer == nil { + m.activePlayer = &ownerName + m.eventCh <- ActiveMediaPlayerChanged{PlayerName: player.name} + } +} + +func (m *MediaPlayers) removePlayer(busName string, ownerName string) { + verbosef("Removing player %s owner %s", busName, ownerName) + + player, ok := m.players[ownerName] + + if !ok || player.busName != busName { + return + } + + if *m.activePlayer == ownerName { + m.SelectPlayer(-1) + } + + player.Close() + delete(m.players, ownerName) + + if *m.activePlayer == ownerName { + m.activePlayer = nil + m.eventCh <- ActiveMediaPlayerChanged{PlayerName: ""} + } +} + +func (m *MediaPlayers) changePlayerOwner(busName string, oldOwnerName string, newOwnerName string) { + verbosef("Changing owner of player %s from %s to %s", busName, oldOwnerName, newOwnerName) + + player, ok := m.players[oldOwnerName] + + if !ok || player.busName != busName { + return + } + + m.players[newOwnerName] = player + m.players[newOwnerName].ownerName = newOwnerName + delete(m.players, oldOwnerName) + + if m.activePlayer != nil && *m.activePlayer == oldOwnerName { + m.activePlayer = &newOwnerName + } +} diff --git a/widget.go b/widget.go index 60764b1..7ce71b0 100644 --- a/widget.go +++ b/widget.go @@ -124,6 +124,9 @@ func NewWidget(dev *streamdeck.Device, base string, kc KeyConfig, bg image.Image case "weather": return NewWeatherWidget(bw, kc.Widget) + + case "mediaPlayer": + return NewMediaPlayerWidget(bw, kc.Widget) } // unknown widget ID diff --git a/widget_media_player.go b/widget_media_player.go new file mode 100644 index 0000000..e2d6e6c --- /dev/null +++ b/widget_media_player.go @@ -0,0 +1,160 @@ +package main + +import ( + "errors" + "fmt" + "image" + "net/http" + "os" + "strings" + "time" +) + +type MediaPlayerWidget struct { + *ButtonWidget + + mode string + + iconPlaying string + iconPaused string + iconNoPlayer string + + playerName bool + + currentPlayer *string + currentArtURL string + + currentPlaybackStatus string + + icon image.Image +} + +func NewMediaPlayerWidget(bw *BaseWidget, opts WidgetConfig) (*MediaPlayerWidget, error) { + widget, err := NewButtonWidget(bw, opts) + if err != nil { + return nil, err + } + + bw.setInterval(time.Duration(opts.Interval)*time.Millisecond, 100) + + var mode, iconPlaying, iconPaused, iconNoPlayer string + var playerName bool + _ = ConfigValue(opts.Config["mode"], &mode) + _ = ConfigValue(opts.Config["icon_playing"], &iconPlaying) + _ = ConfigValue(opts.Config["icon_paused"], &iconPaused) + _ = ConfigValue(opts.Config["icon_no_player"], &iconNoPlayer) + _ = ConfigValue(opts.Config["player_name"], &playerName) + + return &MediaPlayerWidget{ + ButtonWidget: widget, + + mode: mode, + + iconPlaying: iconPlaying, + iconPaused: iconPaused, + iconNoPlayer: iconNoPlayer, + + playerName: playerName, + }, nil +} + +func (w *MediaPlayerWidget) Update() error { + fresh := true + + player := mediaPlayers.ActivePlayer() + + if w.playerName { + if w.currentPlayer != mediaPlayers.activePlayer { + w.currentPlayer = mediaPlayers.activePlayer + fresh = false + + w.label = "" + if player != nil { + w.label = player.name + } + } + } + + if w.mode == "playback status" { + if player == nil { + if w.currentPlaybackStatus != "No player" { + w.currentPlaybackStatus = "No player" + fresh = false + + if err := w.LoadImage(w.iconNoPlayer); err != nil { + return err + } + } + } else { + status := player.Status() + if status.status != w.currentPlaybackStatus { + w.currentPlaybackStatus = status.status + fresh = false + + if w.currentPlaybackStatus == "Playing" { + if err := w.LoadImage(w.iconPlaying); err != nil { + return err + } + } else { + if err := w.LoadImage(w.iconPaused); err != nil { + return err + } + } + } + } + } + + if w.mode == "art" || w.mode == "" { + if player != nil { + url := player.Status().artURL + if url != w.currentArtURL { + w.currentArtURL = url + fresh = false + w.SetImageURL(url) + } + } else { + w.SetImage(nil) + } + } + + if !fresh { + return w.ButtonWidget.Update() + } + + return nil +} + +func (w *MediaPlayerWidget) SetImageURL(url string) { + if url == "" { + w.SetImage(nil) + } else { + if strings.HasPrefix(url, "file://") { + err := w.LoadImage(strings.TrimPrefix(url, "file://")) + if err != nil { + fmt.Fprintf(os.Stderr, "Error while opening image: %s: %s", url, err) + } + } else { + img, err := w.downloadImage(url) + if err != nil { + fmt.Fprintf(os.Stderr, "Error while downloading image: %s: %s", url, err) + } + w.SetImage(img) + } + } +} + +func (w *MediaPlayerWidget) downloadImage(url string) (image.Image, error) { + response, err := http.Get(url) + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != 200 { + return nil, errors.New("unable to download image from URL") + } + img, _, err := image.Decode(response.Body) + if err != nil { + return nil, err + } + return img, nil +} From dcf23ed6888c88d0aca643d3fab11642c34db9dd Mon Sep 17 00:00:00 2001 From: Bert Hekman Date: Mon, 11 Jul 2022 22:33:10 +0200 Subject: [PATCH 2/3] Cache image downloads for a while --- image_downloader.go | 92 ++++++++++++++++++++++++++++++++++++++++++ main.go | 5 +++ widget_media_player.go | 23 +---------- 3 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 image_downloader.go diff --git a/image_downloader.go b/image_downloader.go new file mode 100644 index 0000000..cb38f24 --- /dev/null +++ b/image_downloader.go @@ -0,0 +1,92 @@ +package main + +import ( + "errors" + "image" + "net/http" + "sync" + "time" +) + +type ImageDownloader struct { + ttl time.Duration + + cache map[string]CachedImage + mutex *sync.RWMutex +} + +type CachedImage struct { + Expires time.Time + Image image.Image +} + +func NewImageDownloader(ttl time.Duration) *ImageDownloader { + return &ImageDownloader{ + ttl: ttl, + + cache: make(map[string]CachedImage), + mutex: &sync.RWMutex{}, + } +} + +func (d *ImageDownloader) Watch() { + go func() { + time.Sleep(1 * time.Minute) + + now := time.Now() + d.mutex.Lock() + for url, c := range d.cache { + if now.After(c.Expires) { + verbosef("removing expired image %s from cache", url) + delete(d.cache, url) + } + } + d.mutex.Unlock() + }() +} + +func (d *ImageDownloader) Download(url string) (image.Image, error) { + cachedImage := d.fromCache(url) + if cachedImage != nil { + return cachedImage, nil + } + + response, err := http.Get(url) + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != 200 { + return nil, errors.New("unable to download image from URL") + } + img, _, err := image.Decode(response.Body) + if err != nil { + return nil, err + } + + d.mutex.Lock() + d.cache[url] = CachedImage{ + Expires: time.Now().Add(d.ttl), + Image: img, + } + d.mutex.Unlock() + + verbosef("saving image %s in cache", url) + + return img, nil +} + +func (d *ImageDownloader) fromCache(url string) image.Image { + d.mutex.RLock() + defer d.mutex.RUnlock() + + if cache, ok := d.cache[url]; ok { + if time.Now().Before(cache.Expires) { + verbosef("returning image %s from cache", url) + return cache.Image + } + verbosef("cache expired for image %s", url) + } + + return nil +} diff --git a/main.go b/main.go index 1aa2d4d..69c2ec6 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,8 @@ var ( mediaPlayers *MediaPlayers + imageDownloader *ImageDownloader + deckFile = flag.String("deck", "main.deck", "path to deck config file") device = flag.String("device", "", "which device to use (serial number)") brightness = flag.Uint("brightness", 80, "brightness in percent") @@ -300,6 +302,9 @@ func run() error { return fmt.Errorf("Error while running media players: %s", err) } + // initialize image downloader + imageDownloader = NewImageDownloader(1 * time.Hour) + // load deck deck, err = LoadDeck(dev, ".", *deckFile) if err != nil { diff --git a/widget_media_player.go b/widget_media_player.go index e2d6e6c..e406d6a 100644 --- a/widget_media_player.go +++ b/widget_media_player.go @@ -1,10 +1,7 @@ package main import ( - "errors" "fmt" - "image" - "net/http" "os" "strings" "time" @@ -25,8 +22,6 @@ type MediaPlayerWidget struct { currentArtURL string currentPlaybackStatus string - - icon image.Image } func NewMediaPlayerWidget(bw *BaseWidget, opts WidgetConfig) (*MediaPlayerWidget, error) { @@ -134,7 +129,7 @@ func (w *MediaPlayerWidget) SetImageURL(url string) { fmt.Fprintf(os.Stderr, "Error while opening image: %s: %s", url, err) } } else { - img, err := w.downloadImage(url) + img, err := imageDownloader.Download(url) if err != nil { fmt.Fprintf(os.Stderr, "Error while downloading image: %s: %s", url, err) } @@ -142,19 +137,3 @@ func (w *MediaPlayerWidget) SetImageURL(url string) { } } } - -func (w *MediaPlayerWidget) downloadImage(url string) (image.Image, error) { - response, err := http.Get(url) - if err != nil { - return nil, err - } - defer response.Body.Close() - if response.StatusCode != 200 { - return nil, errors.New("unable to download image from URL") - } - img, _, err := image.Decode(response.Body) - if err != nil { - return nil, err - } - return img, nil -} From 267150155ce334be5902ebea1f46e381ef25fa4c Mon Sep 17 00:00:00 2001 From: Bert Hekman Date: Mon, 11 Jul 2022 22:37:52 +0200 Subject: [PATCH 3/3] Upgrade godbus to v5 --- deck.go | 2 +- go.mod | 1 - go.sum | 4 ++-- main.go | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/deck.go b/deck.go index 4efaecb..b2b6feb 100644 --- a/deck.go +++ b/deck.go @@ -13,7 +13,7 @@ import ( "time" "github.com/atotto/clipboard" - "github.com/godbus/dbus" + "github.com/godbus/dbus/v5" "github.com/muesli/streamdeck" ) diff --git a/go.mod b/go.mod index 03d18ab..4402011 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/bendahl/uinput v1.5.1 github.com/davecgh/go-spew v1.1.1 // indirect github.com/flopp/go-findfont v0.1.0 - github.com/godbus/dbus v4.1.0+incompatible github.com/godbus/dbus/v5 v5.1.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 diff --git a/go.sum b/go.sum index 365ba89..88a2c3c 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,6 @@ github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6 github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= -github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= @@ -33,6 +31,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8= +github.com/muesli/streamdeck v0.3.0 h1:BrUXEPtzDy9hO/l2ZLMKsik0VrNDwt8hHRomKq0NlFQ= +github.com/muesli/streamdeck v0.3.0/go.mod h1:iBrvujOQ0WXWBJfi25gFOj/AVgq7FKfjPLi5AISKZx8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/main.go b/main.go index 69c2ec6..676a842 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "time" "github.com/bendahl/uinput" - "github.com/godbus/dbus" + "github.com/godbus/dbus/v5" "github.com/mitchellh/go-homedir" "github.com/muesli/streamdeck" )