Merge pull request #310 from dweymouth/save-play-queue

Add option to save and restore play queue on exit/startup
This commit is contained in:
Drew Weymouth
2024-01-08 09:00:11 -08:00
committed by GitHub
5 changed files with 157 additions and 25 deletions
+26
View File
@@ -25,6 +25,7 @@ const (
sessionDir = "session"
sessionLockFile = ".lock"
sessionActivateFile = ".activate"
savedQueueFile = "saved_queue.json"
)
var (
@@ -300,6 +301,9 @@ func (a *App) DeleteServerCacheDir(serverID uuid.UUID) error {
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))
}
a.PlaybackManager.Stop() // will trigger scrobble check
a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume()
a.cancel()
@@ -308,6 +312,28 @@ func (a *App) Shutdown() {
os.RemoveAll(configdir.LocalConfig(a.appName, sessionDir))
}
func (a *App) LoadSavedPlayQueue() error {
queue, err := LoadPlayQueue(configdir.LocalConfig(a.appName, savedQueueFile), a.ServerManager)
if err != nil {
return err
}
if len(queue.Tracks) == 0 {
return nil
}
if err := a.PlaybackManager.LoadTracks(queue.Tracks, false, false); err != nil {
return err
}
if queue.TrackIndex >= 0 {
// TODO: This isn't ideal but doesn't seem to cause an audible play-for-a-split-second artifact
a.PlaybackManager.PlayTrackAt(queue.TrackIndex)
a.PlaybackManager.Pause()
time.Sleep(50 * time.Millisecond) // MPV seek fails if run immediately after
a.PlaybackManager.SeekSeconds(queue.TimePos)
}
return nil
}
func (a *App) SaveConfigFile() {
a.Config.WriteConfigFile(a.configPath())
a.lastWrittenCfg = *a.Config
+2
View File
@@ -41,6 +41,7 @@ type AppConfig struct {
SettingsTab string
AllowMultiInstance bool
MaxImageCacheSizeMB int
SavePlayQueue bool
// Experimental - may be removed in future
FontNormalTTF string
@@ -147,6 +148,7 @@ func DefaultConfig(appVersionTag string) *Config {
AllowMultiInstance: false,
MaxImageCacheSizeMB: 50,
UIScaleSize: "Normal",
SavePlayQueue: false,
},
AlbumPage: AlbumPageConfig{
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"},
+86
View File
@@ -0,0 +1,86 @@
package backend
import (
"encoding/json"
"errors"
"os"
"github.com/dweymouth/supersonic/backend/mediaprovider"
)
type SavedPlayQueue struct {
Tracks []*mediaprovider.Track
TrackIndex int
TimePos float64
}
type serializedSavedPlayQueue struct {
ServerID string `json:"serverID"`
TrackIDs []string `json:"trackIDs"`
TrackIndex int `json:"trackIndex"`
TimePos float64 `json:"timePos"`
}
// SavePlayQueue saves the current play queue and playback position to a JSON file.
func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string) error {
queue := pm.GetPlayQueue()
stats := pm.PlayerStatus()
trackIdx := pm.NowPlayingIndex()
trackIDs := make([]string, len(queue))
for i, tr := range queue {
trackIDs[i] = tr.ID
}
saved := serializedSavedPlayQueue{
ServerID: serverID,
TrackIDs: trackIDs,
TrackIndex: trackIdx,
TimePos: stats.TimePos,
}
b, err := json.Marshal(saved)
if err != nil {
return err
}
return os.WriteFile(filepath, b, 0644)
}
// Loads the saved play queue from the given filepath using the current server.
// 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) {
b, err := os.ReadFile(filepath)
if err != nil {
return nil, err
}
var savedData serializedSavedPlayQueue
if err := json.Unmarshal(b, &savedData); err != nil {
return nil, err
}
if sm.ServerID.String() != savedData.ServerID {
return nil, errors.New("saved play queue was from a different server")
}
tracks := make([]*mediaprovider.Track, 0, len(savedData.TrackIDs))
mp := sm.Server
for i, id := range savedData.TrackIDs {
if tr, err := mp.GetTrack(id); err != nil {
// ignore/skip individual track failures
if i < savedData.TrackIndex {
savedData.TrackIndex--
}
} else {
tracks = append(tracks, tr)
}
}
savedQueue := &SavedPlayQueue{
Tracks: tracks,
TrackIndex: savedData.TrackIndex,
TimePos: savedData.TimePos,
}
return savedQueue, nil
}
+4
View File
@@ -159,6 +159,9 @@ 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))
// Scrobble settings
twoDigitValidator := func(text, selText string, r rune) bool {
@@ -247,6 +250,7 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
widget.NewLabel("Startup page"), container.NewGridWithColumns(2, startupPage),
),
container.NewHBox(systemTrayEnable, closeToTray),
container.NewHBox(saveQueue),
s.newSectionSeparator(),
widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: util.BoldRichTextStyle}),
+39 -25
View File
@@ -2,6 +2,7 @@ package ui
import (
"fmt"
"log"
"strings"
"github.com/20after4/configdir"
@@ -92,31 +93,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
m.Window.SetTitle(fmt.Sprintf("%s %s · %s", song.Name, strings.Join(song.ArtistNames, ", "), displayAppName))
})
app.ServerManager.OnServerConnected(func() {
m.BrowsingPane.EnableNavigationButtons()
m.Router.NavigateTo(m.StartupPage())
_, canRate := m.App.ServerManager.Server.(mediaprovider.SupportsRating)
m.BottomPanel.NowPlaying.DisableRating = !canRate
// check if launching new version, else if found available update on startup
if l := app.Config.Application.LastLaunchedVersion; app.VersionTag() != l {
if !app.IsFirstLaunch() {
m.ShowWhatsNewDialog()
}
m.App.Config.Application.LastLaunchedVersion = app.VersionTag()
} else if t := app.UpdateChecker.VersionTagFound(); t != "" && t != app.Config.Application.LastCheckedVersion {
if t != app.VersionTag() {
m.ShowNewVersionDialog(displayAppName, t)
}
m.App.Config.Application.LastCheckedVersion = t
}
// register callback for the ongoing periodic update check
m.App.UpdateChecker.OnUpdatedVersionFound = func() {
t := m.App.UpdateChecker.VersionTagFound()
if t != app.VersionTag() {
m.ShowNewVersionDialog(displayAppName, t)
}
m.App.Config.Application.LastCheckedVersion = t
}
m.App.SaveConfigFile()
m.RunOnServerConnectedTasks(app, displayAppName)
})
app.ServerManager.OnLogout(func() {
m.BrowsingPane.DisableNavigationButtons()
@@ -158,6 +135,43 @@ func (m *MainWindow) StartupPage() controller.Route {
}
}
func (m *MainWindow) RunOnServerConnectedTasks(app *backend.App, displayAppName string) {
m.BrowsingPane.EnableNavigationButtons()
m.Router.NavigateTo(m.StartupPage())
_, canRate := m.App.ServerManager.Server.(mediaprovider.SupportsRating)
m.BottomPanel.NowPlaying.DisableRating = !canRate
if app.Config.Application.SavePlayQueue {
go func() {
if err := app.LoadSavedPlayQueue(); err != nil {
log.Printf("failed to load saved play queue: %s", err.Error())
}
}()
}
// check if launching new version, else if found available update on startup
if l := app.Config.Application.LastLaunchedVersion; app.VersionTag() != l {
if !app.IsFirstLaunch() {
m.ShowWhatsNewDialog()
}
m.App.Config.Application.LastLaunchedVersion = app.VersionTag()
} else if t := app.UpdateChecker.VersionTagFound(); t != "" && t != app.Config.Application.LastCheckedVersion {
if t != app.VersionTag() {
m.ShowNewVersionDialog(displayAppName, t)
}
m.App.Config.Application.LastCheckedVersion = t
}
// register callback for the ongoing periodic update check
m.App.UpdateChecker.OnUpdatedVersionFound = func() {
t := m.App.UpdateChecker.VersionTagFound()
if t != app.VersionTag() {
m.ShowNewVersionDialog(displayAppName, t)
}
m.App.Config.Application.LastCheckedVersion = t
}
m.App.SaveConfigFile()
}
func (m *MainWindow) SetupSystemTrayMenu(appName string, fyneApp fyne.App) {
if desk, ok := fyneApp.(desktop.App); ok {
menu := fyne.NewMenu(appName,