From 98a140791b71e66d9d6fc2dd693d7d9a6fd7d738 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 13 Apr 2024 08:34:14 -0700 Subject: [PATCH 1/5] beginning of work to save queue to server --- backend/app.go | 2 +- backend/mediaprovider/mediaprovider.go | 5 +++++ backend/mediaprovider/model.go | 6 ++++++ .../subsonic/subsonicmediaprovider.go | 21 +++++++++++++++++++ backend/savedplayqueue.go | 12 +++++------ 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/backend/app.go b/backend/app.go index 1ea8d41..1f211c5 100644 --- a/backend/app.go +++ b/backend/app.go @@ -305,7 +305,7 @@ func (a *App) Shutdown() { a.MPRISHandler.Shutdown() a.PlaybackManager.DisableCallbacks() if a.Config.Application.SavePlayQueue { - SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile)) + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile), false) } a.PlaybackManager.Stop() // will trigger scrobble check a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume() diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index cc04d2c..648a363 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -258,6 +258,11 @@ type SupportsSharing interface { CanShareArtists() bool } +type CanSavePlayQueue interface { + SavePlayQueue(trackIDs []string, currentTrackPos int, timeSeconds int) error + GetPlayQueue() (*SavedPlayQueue, error) +} + type LyricsProvider interface { GetLyrics(track *Track) (*Lyrics, error) } diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index 33a9d70..c865bce 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -133,6 +133,12 @@ type LyricLine struct { Start float64 // seconds } +type SavedPlayQueue struct { + Tracks []*Track + TrackPos int + TimePos int // seconds +} + type ContentType int const ( diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index a8316bb..ea7001d 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -379,6 +379,27 @@ func (s *subsonicMediaProvider) GetLyrics(track *mediaprovider.Track) (*mediapro return mpLyrics, nil } +// CanSavePlayQueue interface + +func (s *subsonicMediaProvider) SavePlayQueue(trackIDs []string, currentTrackPos int, timeSeconds int) error { + return s.client.SavePlayQueue(trackIDs, map[string]string{ + "current": trackIDs[currentTrackPos], + "position": strconv.Itoa(timeSeconds * 1000), + }) +} + +func (s *subsonicMediaProvider) GetPlayQueue() (*mediaprovider.SavedPlayQueue, error) { + pq, err := s.client.GetPlayQueue() + if err != nil { + return nil, err + } + savedQueue := &mediaprovider.SavedPlayQueue{} + savedQueue.Tracks = sharedutil.MapSlice(pq.Entries, toTrack) + savedQueue.TrackPos = pq.Current + savedQueue.TimePos = int(pq.Position) + return savedQueue, nil +} + func toTrack(ch *subsonic.Child) *mediaprovider.Track { if ch == nil { return nil diff --git a/backend/savedplayqueue.go b/backend/savedplayqueue.go index 1fe8e85..53cdca6 100644 --- a/backend/savedplayqueue.go +++ b/backend/savedplayqueue.go @@ -22,7 +22,7 @@ type serializedSavedPlayQueue struct { } // SavePlayQueue saves the current play queue and playback position to a JSON file. -func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string) error { +func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, saveToServer bool) error { queue := pm.GetPlayQueue() stats := pm.PlayerStatus() trackIdx := pm.NowPlayingIndex() @@ -38,12 +38,12 @@ func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string) error TrackIndex: trackIdx, TimePos: stats.TimePos, } - b, err := json.Marshal(saved) - if err != nil { - return err - } + b, _ := json.Marshal(saved) + err := os.WriteFile(filepath, b, 0644) - return os.WriteFile(filepath, b, 0644) + if saveToServer { + } + return err } // Loads the saved play queue from the given filepath using the current server. From 4d6626b7f23398e7629bd566cca1b4b2879097d7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 16 Apr 2024 17:17:48 -0700 Subject: [PATCH 2/5] add restoring of play queue from server --- backend/app.go | 4 ++-- backend/savedplayqueue.go | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/backend/app.go b/backend/app.go index 1f211c5..b3dbc5b 100644 --- a/backend/app.go +++ b/backend/app.go @@ -305,7 +305,7 @@ func (a *App) Shutdown() { a.MPRISHandler.Shutdown() a.PlaybackManager.DisableCallbacks() if a.Config.Application.SavePlayQueue { - SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile), false) + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile), nil) } a.PlaybackManager.Stop() // will trigger scrobble check a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume() @@ -316,7 +316,7 @@ func (a *App) Shutdown() { } func (a *App) LoadSavedPlayQueue() error { - queue, err := LoadPlayQueue(configdir.LocalConfig(a.appName, savedQueueFile), a.ServerManager) + queue, err := LoadPlayQueue(configdir.LocalConfig(a.appName, savedQueueFile), a.ServerManager, false) if err != nil { return err } diff --git a/backend/savedplayqueue.go b/backend/savedplayqueue.go index 53cdca6..2e492da 100644 --- a/backend/savedplayqueue.go +++ b/backend/savedplayqueue.go @@ -22,7 +22,8 @@ type serializedSavedPlayQueue struct { } // SavePlayQueue saves the current play queue and playback position to a JSON file. -func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, saveToServer bool) error { +// If the provided CanSavePlayQueue server is non-nil, it will also save to the server. +func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error { queue := pm.GetPlayQueue() stats := pm.PlayerStatus() trackIdx := pm.NowPlayingIndex() @@ -41,15 +42,32 @@ func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, saveTo b, _ := json.Marshal(saved) err := os.WriteFile(filepath, b, 0644) - if saveToServer { + if server != nil { + // save to server + err = server.SavePlayQueue(trackIDs, trackIdx, int(stats.TimePos)) } return err } // Loads the saved play queue from the given filepath using the current server. +// If loadFromServer is true and the current server supports saving the play queue, +// the queue will attempt to load from the server and only use the local file as a fallback. // Returns an error if the queue could not be loaded for any reason, including the // currently logged in server being different than the server from which the queue was saved. -func LoadPlayQueue(filepath string, sm *ServerManager) (*SavedPlayQueue, error) { +func LoadPlayQueue(filepath string, sm *ServerManager, loadFromServer bool) (*SavedPlayQueue, error) { + if pq, ok := sm.Server.(mediaprovider.CanSavePlayQueue); loadFromServer && ok && pq != nil { + // load queue from server + queue, err := pq.GetPlayQueue() + if err == nil { + return &SavedPlayQueue{ + Tracks: queue.Tracks, + TrackIndex: queue.TrackPos, + TimePos: float64(queue.TimePos), + }, nil + } + } + + // load queue from local file b, err := os.ReadFile(filepath) if err != nil { return nil, err From 8eeb2aa53a836b2efce5bfcc45a3108061c82f8d Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 16 Apr 2024 17:26:11 -0700 Subject: [PATCH 3/5] add config setting for save queue to server --- backend/app.go | 12 ++++++++++-- backend/config.go | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/app.go b/backend/app.go index b3dbc5b..24ec723 100644 --- a/backend/app.go +++ b/backend/app.go @@ -11,6 +11,7 @@ import ( "slices" "time" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/util" @@ -305,7 +306,13 @@ func (a *App) Shutdown() { a.MPRISHandler.Shutdown() a.PlaybackManager.DisableCallbacks() if a.Config.Application.SavePlayQueue { - SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile), nil) + var queueServer mediaprovider.CanSavePlayQueue = nil + if a.Config.Application.SaveQueueToServer { + if qs, ok := a.ServerManager.Server.(mediaprovider.CanSavePlayQueue); ok { + queueServer = qs + } + } + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile), queueServer) } a.PlaybackManager.Stop() // will trigger scrobble check a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume() @@ -316,7 +323,8 @@ func (a *App) Shutdown() { } func (a *App) LoadSavedPlayQueue() error { - queue, err := LoadPlayQueue(configdir.LocalConfig(a.appName, savedQueueFile), a.ServerManager, false) + queueFilePath := configdir.LocalConfig(a.appName, savedQueueFile) + queue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) if err != nil { return err } diff --git a/backend/config.go b/backend/config.go index dcb8402..1c980f5 100644 --- a/backend/config.go +++ b/backend/config.go @@ -42,6 +42,7 @@ type AppConfig struct { AllowMultiInstance bool MaxImageCacheSizeMB int SavePlayQueue bool + SaveQueueToServer bool DefaultPlaylistID string ShowTrackChangeNotification bool @@ -155,7 +156,8 @@ func DefaultConfig(appVersionTag string) *Config { AllowMultiInstance: false, MaxImageCacheSizeMB: 50, UIScaleSize: "Normal", - SavePlayQueue: false, + SavePlayQueue: true, + SaveQueueToServer: false, ShowTrackChangeNotification: false, }, AlbumPage: AlbumPageConfig{ From 63b0ab73743a9cf6da90c997869539c513181dcd Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 16 Apr 2024 18:13:22 -0700 Subject: [PATCH 4/5] add settings dialog radio for saving queue locally or to server --- .../subsonic/subsonicmediaprovider.go | 3 ++ backend/savedplayqueue.go | 3 ++ ui/controller/controller.go | 3 +- ui/dialogs/settingsdialog.go | 37 ++++++++++++++++--- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index ea7001d..0dd6a20 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -382,6 +382,9 @@ func (s *subsonicMediaProvider) GetLyrics(track *mediaprovider.Track) (*mediapro // CanSavePlayQueue interface func (s *subsonicMediaProvider) SavePlayQueue(trackIDs []string, currentTrackPos int, timeSeconds int) error { + if len(trackIDs) == 0 { + return nil // don't save an empty queue + } return s.client.SavePlayQueue(trackIDs, map[string]string{ "current": trackIDs[currentTrackPos], "position": strconv.Itoa(timeSeconds * 1000), diff --git a/backend/savedplayqueue.go b/backend/savedplayqueue.go index 2e492da..a0baf41 100644 --- a/backend/savedplayqueue.go +++ b/backend/savedplayqueue.go @@ -3,6 +3,7 @@ package backend import ( "encoding/json" "errors" + "log" "os" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -64,6 +65,8 @@ func LoadPlayQueue(filepath string, sm *ServerManager, loadFromServer bool) (*Sa TrackIndex: queue.TrackPos, TimePos: float64(queue.TimePos), }, nil + } else { + log.Printf("error loading queue from server: %v", err.Error()) } } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index b598ace..6e6fe18 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -578,12 +578,13 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map curPlayer := c.App.PlaybackManager.CurrentPlayer() _, isReplayGainPlayer := curPlayer.(player.ReplayGainPlayer) _, isEqualizerPlayer := curPlayer.(*mpv.Player) + _, canSavePlayQueue := c.App.ServerManager.Server.(mediaprovider.CanSavePlayQueue) isLocalPlayer := isEqualizerPlayer bands := c.App.LocalPlayer.Equalizer().BandFrequencies() dlg := dialogs.NewSettingsDialog(c.App.Config, devs, themeFiles, bands, c.App.ServerManager.Server.ClientDecidesScrobble(), - isLocalPlayer, isReplayGainPlayer, isEqualizerPlayer, + isLocalPlayer, isReplayGainPlayer, isEqualizerPlayer, canSavePlayQueue, c.MainWindow) dlg.OnReplayGainSettingsChanged = func() { c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain) diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index de2ec73..df95061 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -56,6 +56,7 @@ func NewSettingsDialog( isLocalPlayer bool, isReplayGainPlayer bool, isEqualizerPlayer bool, + canSavePlayQueue bool, window fyne.Window, ) *SettingsDialog { s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList, clientDecidesScrobble: clientDecidesScrobble} @@ -66,14 +67,14 @@ func NewSettingsDialog( var tabs *container.AppTabs if isEqualizerPlayer { tabs = container.NewAppTabs( - s.createGeneralTab(), + s.createGeneralTab(canSavePlayQueue), s.createPlaybackTab(isLocalPlayer, isReplayGainPlayer), s.createEqualizerTab(equalizerBands), s.createExperimentalTab(window), ) } else { tabs = container.NewAppTabs( - s.createGeneralTab(), + s.createGeneralTab(canSavePlayQueue), s.createPlaybackTab(isLocalPlayer, isReplayGainPlayer), s.createExperimentalTab(window), ) @@ -94,7 +95,7 @@ func NewSettingsDialog( return s } -func (s *SettingsDialog) createGeneralTab() *container.TabItem { +func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.TabItem { themeNames := []string{"Default"} themeFileNames := []string{""} i, selIndex := 1, 0 @@ -157,8 +158,32 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem { }) systemTrayEnable.Checked = s.config.Application.EnableSystemTray - saveQueue := widget.NewCheckWithData("Save play queue on exit", - binding.BindBool(&s.config.Application.SavePlayQueue)) + // save play queue settings + saveToServer := widget.NewRadioGroup([]string{"Locally", "To server"}, func(choice string) { + s.config.Application.SaveQueueToServer = choice == "To server" + }) + saveToServer.Horizontal = true + if !s.config.Application.SavePlayQueue { + saveToServer.Disable() + } + saveToServer.Selected = "Locally" + if s.config.Application.SaveQueueToServer { + saveToServer.Selected = "To server" + } + saveQueue := widget.NewCheck("Save play queue on exit", func(save bool) { + s.config.Application.SavePlayQueue = save + if save && canSaveQueueToServer { + saveToServer.Enable() + } else if canSaveQueueToServer { + saveToServer.Disable() + } + }) + saveQueue.Checked = s.config.Application.SavePlayQueue + saveQueueHBox := container.NewHBox(saveQueue) + if canSaveQueueToServer { + saveQueueHBox.Add(saveToServer) + } + trackNotif := widget.NewCheckWithData("Show notification on track change", binding.BindBool(&s.config.Application.ShowTrackChangeNotification)) @@ -250,7 +275,7 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem { widget.NewLabel("Startup page"), container.NewGridWithColumns(2, startupPage), ), container.NewHBox(systemTrayEnable, closeToTray), - saveQueue, + saveQueueHBox, trackNotif, s.newSectionSeparator(), From f1aee40b057bfd8358db54b89a947d0453c711ba Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 16 Apr 2024 19:37:35 -0700 Subject: [PATCH 5/5] pull in queue parsing fix --- backend/mediaprovider/subsonic/subsonicmediaprovider.go | 6 ++++-- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 0dd6a20..6c07c07 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -398,8 +398,10 @@ func (s *subsonicMediaProvider) GetPlayQueue() (*mediaprovider.SavedPlayQueue, e } savedQueue := &mediaprovider.SavedPlayQueue{} savedQueue.Tracks = sharedutil.MapSlice(pq.Entries, toTrack) - savedQueue.TrackPos = pq.Current - savedQueue.TimePos = int(pq.Position) + savedQueue.TrackPos = slices.IndexFunc(pq.Entries, func(e *subsonic.Child) bool { + return e.ID == pq.Current + }) + savedQueue.TimePos = int(pq.Position / 1000) return savedQueue, nil } diff --git a/go.mod b/go.mod index c870538..f8c6d5d 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee - github.com/dweymouth/go-subsonic v0.0.0-20240331151503-47a6f310eb73 + github.com/dweymouth/go-subsonic v0.0.0-20240417012336-798603e9f3a3 github.com/fsnotify/fsnotify v1.6.0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 0c65773..f6e0aee 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e h1:89N7tfmGP github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= -github.com/dweymouth/go-subsonic v0.0.0-20240331151503-47a6f310eb73 h1:uSy9D1HzfY7Y2Ifat14WIDSkWRhbyo57fPsqdM1XMUA= -github.com/dweymouth/go-subsonic v0.0.0-20240331151503-47a6f310eb73/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= +github.com/dweymouth/go-subsonic v0.0.0-20240417012336-798603e9f3a3 h1:DJwu4MrQ6cPJF+eqcuP0eGKelsaZbHa08xsRHrbJYc8= +github.com/dweymouth/go-subsonic v0.0.0-20240417012336-798603e9f3a3/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=