Merge pull request #367 from dweymouth/feature/savequeuetoserver

Add ability to save and load play queue from Subsonic servers
This commit is contained in:
Drew Weymouth
2024-04-17 07:52:30 -07:00
committed by GitHub
10 changed files with 114 additions and 20 deletions
+10 -2
View File
@@ -11,6 +11,7 @@ import (
"slices" "slices"
"time" "time"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/util"
@@ -305,7 +306,13 @@ func (a *App) Shutdown() {
a.MPRISHandler.Shutdown() a.MPRISHandler.Shutdown()
a.PlaybackManager.DisableCallbacks() a.PlaybackManager.DisableCallbacks()
if a.Config.Application.SavePlayQueue { if a.Config.Application.SavePlayQueue {
SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile)) 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.PlaybackManager.Stop() // will trigger scrobble check
a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume() a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume()
@@ -316,7 +323,8 @@ func (a *App) Shutdown() {
} }
func (a *App) LoadSavedPlayQueue() error { func (a *App) LoadSavedPlayQueue() error {
queue, err := LoadPlayQueue(configdir.LocalConfig(a.appName, savedQueueFile), a.ServerManager) queueFilePath := configdir.LocalConfig(a.appName, savedQueueFile)
queue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer)
if err != nil { if err != nil {
return err return err
} }
+3 -1
View File
@@ -42,6 +42,7 @@ type AppConfig struct {
AllowMultiInstance bool AllowMultiInstance bool
MaxImageCacheSizeMB int MaxImageCacheSizeMB int
SavePlayQueue bool SavePlayQueue bool
SaveQueueToServer bool
DefaultPlaylistID string DefaultPlaylistID string
ShowTrackChangeNotification bool ShowTrackChangeNotification bool
@@ -155,7 +156,8 @@ func DefaultConfig(appVersionTag string) *Config {
AllowMultiInstance: false, AllowMultiInstance: false,
MaxImageCacheSizeMB: 50, MaxImageCacheSizeMB: 50,
UIScaleSize: "Normal", UIScaleSize: "Normal",
SavePlayQueue: false, SavePlayQueue: true,
SaveQueueToServer: false,
ShowTrackChangeNotification: false, ShowTrackChangeNotification: false,
}, },
AlbumPage: AlbumPageConfig{ AlbumPage: AlbumPageConfig{
+5
View File
@@ -258,6 +258,11 @@ type SupportsSharing interface {
CanShareArtists() bool CanShareArtists() bool
} }
type CanSavePlayQueue interface {
SavePlayQueue(trackIDs []string, currentTrackPos int, timeSeconds int) error
GetPlayQueue() (*SavedPlayQueue, error)
}
type LyricsProvider interface { type LyricsProvider interface {
GetLyrics(track *Track) (*Lyrics, error) GetLyrics(track *Track) (*Lyrics, error)
} }
+6
View File
@@ -133,6 +133,12 @@ type LyricLine struct {
Start float64 // seconds Start float64 // seconds
} }
type SavedPlayQueue struct {
Tracks []*Track
TrackPos int
TimePos int // seconds
}
type ContentType int type ContentType int
const ( const (
@@ -379,6 +379,32 @@ func (s *subsonicMediaProvider) GetLyrics(track *mediaprovider.Track) (*mediapro
return mpLyrics, nil return mpLyrics, nil
} }
// 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),
})
}
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 = slices.IndexFunc(pq.Entries, func(e *subsonic.Child) bool {
return e.ID == pq.Current
})
savedQueue.TimePos = int(pq.Position / 1000)
return savedQueue, nil
}
func toTrack(ch *subsonic.Child) *mediaprovider.Track { func toTrack(ch *subsonic.Child) *mediaprovider.Track {
if ch == nil { if ch == nil {
return nil return nil
+28 -7
View File
@@ -3,6 +3,7 @@ package backend
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"log"
"os" "os"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
@@ -22,7 +23,8 @@ type serializedSavedPlayQueue struct {
} }
// SavePlayQueue saves the current play queue and playback position to a JSON file. // SavePlayQueue saves the current play queue and playback position to a JSON file.
func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string) 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() queue := pm.GetPlayQueue()
stats := pm.PlayerStatus() stats := pm.PlayerStatus()
trackIdx := pm.NowPlayingIndex() trackIdx := pm.NowPlayingIndex()
@@ -38,18 +40,37 @@ func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string) error
TrackIndex: trackIdx, TrackIndex: trackIdx,
TimePos: stats.TimePos, TimePos: stats.TimePos,
} }
b, err := json.Marshal(saved) b, _ := json.Marshal(saved)
if err != nil { err := os.WriteFile(filepath, b, 0644)
if server != nil {
// save to server
err = server.SavePlayQueue(trackIDs, trackIdx, int(stats.TimePos))
}
return err return err
} }
return os.WriteFile(filepath, b, 0644)
}
// Loads the saved play queue from the given filepath using the current server. // 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 // 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. // 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
} else {
log.Printf("error loading queue from server: %v", err.Error())
}
}
// load queue from local file
b, err := os.ReadFile(filepath) b, err := os.ReadFile(filepath)
if err != nil { if err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -8,7 +8,7 @@ require (
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee 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/fsnotify/fsnotify v1.6.0
github.com/godbus/dbus/v5 v5.1.0 github.com/godbus/dbus/v5 v5.1.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
+2 -2
View File
@@ -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-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 h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= 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-20240417012336-798603e9f3a3 h1:DJwu4MrQ6cPJF+eqcuP0eGKelsaZbHa08xsRHrbJYc8=
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/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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+2 -1
View File
@@ -578,12 +578,13 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
curPlayer := c.App.PlaybackManager.CurrentPlayer() curPlayer := c.App.PlaybackManager.CurrentPlayer()
_, isReplayGainPlayer := curPlayer.(player.ReplayGainPlayer) _, isReplayGainPlayer := curPlayer.(player.ReplayGainPlayer)
_, isEqualizerPlayer := curPlayer.(*mpv.Player) _, isEqualizerPlayer := curPlayer.(*mpv.Player)
_, canSavePlayQueue := c.App.ServerManager.Server.(mediaprovider.CanSavePlayQueue)
isLocalPlayer := isEqualizerPlayer isLocalPlayer := isEqualizerPlayer
bands := c.App.LocalPlayer.Equalizer().BandFrequencies() bands := c.App.LocalPlayer.Equalizer().BandFrequencies()
dlg := dialogs.NewSettingsDialog(c.App.Config, dlg := dialogs.NewSettingsDialog(c.App.Config,
devs, themeFiles, bands, devs, themeFiles, bands,
c.App.ServerManager.Server.ClientDecidesScrobble(), c.App.ServerManager.Server.ClientDecidesScrobble(),
isLocalPlayer, isReplayGainPlayer, isEqualizerPlayer, isLocalPlayer, isReplayGainPlayer, isEqualizerPlayer, canSavePlayQueue,
c.MainWindow) c.MainWindow)
dlg.OnReplayGainSettingsChanged = func() { dlg.OnReplayGainSettingsChanged = func() {
c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain) c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain)
+31 -6
View File
@@ -56,6 +56,7 @@ func NewSettingsDialog(
isLocalPlayer bool, isLocalPlayer bool,
isReplayGainPlayer bool, isReplayGainPlayer bool,
isEqualizerPlayer bool, isEqualizerPlayer bool,
canSavePlayQueue bool,
window fyne.Window, window fyne.Window,
) *SettingsDialog { ) *SettingsDialog {
s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList, clientDecidesScrobble: clientDecidesScrobble} s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList, clientDecidesScrobble: clientDecidesScrobble}
@@ -66,14 +67,14 @@ func NewSettingsDialog(
var tabs *container.AppTabs var tabs *container.AppTabs
if isEqualizerPlayer { if isEqualizerPlayer {
tabs = container.NewAppTabs( tabs = container.NewAppTabs(
s.createGeneralTab(), s.createGeneralTab(canSavePlayQueue),
s.createPlaybackTab(isLocalPlayer, isReplayGainPlayer), s.createPlaybackTab(isLocalPlayer, isReplayGainPlayer),
s.createEqualizerTab(equalizerBands), s.createEqualizerTab(equalizerBands),
s.createExperimentalTab(window), s.createExperimentalTab(window),
) )
} else { } else {
tabs = container.NewAppTabs( tabs = container.NewAppTabs(
s.createGeneralTab(), s.createGeneralTab(canSavePlayQueue),
s.createPlaybackTab(isLocalPlayer, isReplayGainPlayer), s.createPlaybackTab(isLocalPlayer, isReplayGainPlayer),
s.createExperimentalTab(window), s.createExperimentalTab(window),
) )
@@ -94,7 +95,7 @@ func NewSettingsDialog(
return s return s
} }
func (s *SettingsDialog) createGeneralTab() *container.TabItem { func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.TabItem {
themeNames := []string{"Default"} themeNames := []string{"Default"}
themeFileNames := []string{""} themeFileNames := []string{""}
i, selIndex := 1, 0 i, selIndex := 1, 0
@@ -157,8 +158,32 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
}) })
systemTrayEnable.Checked = s.config.Application.EnableSystemTray systemTrayEnable.Checked = s.config.Application.EnableSystemTray
saveQueue := widget.NewCheckWithData("Save play queue on exit", // save play queue settings
binding.BindBool(&s.config.Application.SavePlayQueue)) 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", trackNotif := widget.NewCheckWithData("Show notification on track change",
binding.BindBool(&s.config.Application.ShowTrackChangeNotification)) binding.BindBool(&s.config.Application.ShowTrackChangeNotification))
@@ -250,7 +275,7 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
widget.NewLabel("Startup page"), container.NewGridWithColumns(2, startupPage), widget.NewLabel("Startup page"), container.NewGridWithColumns(2, startupPage),
), ),
container.NewHBox(systemTrayEnable, closeToTray), container.NewHBox(systemTrayEnable, closeToTray),
saveQueue, saveQueueHBox,
trackNotif, trackNotif,
s.newSectionSeparator(), s.newSectionSeparator(),