Skip to content

Commit

Permalink
Merge pull request #310 from dweymouth/save-play-queue
Browse files Browse the repository at this point in the history
Add option to save and restore play queue on exit/startup
  • Loading branch information
dweymouth authored Jan 8, 2024
2 parents d274381 + fca5abc commit daa1c44
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 25 deletions.
26 changes: 26 additions & 0 deletions backend/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
sessionDir = "session"
sessionLockFile = ".lock"
sessionActivateFile = ".activate"
savedQueueFile = "saved_queue.json"
)

var (
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type AppConfig struct {
SettingsTab string
AllowMultiInstance bool
MaxImageCacheSizeMB int
SavePlayQueue bool

// Experimental - may be removed in future
FontNormalTTF string
Expand Down Expand Up @@ -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"},
Expand Down
86 changes: 86 additions & 0 deletions backend/savedplayqueue.go
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions ui/dialogs/settingsdialog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}),
Expand Down
64 changes: 39 additions & 25 deletions ui/mainwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ui

import (
"fmt"
"log"
"strings"

"github.com/20after4/configdir"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit daa1c44

Please sign in to comment.