diff --git a/README.md b/README.md index 6edf6b2..d4e7721 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,9 @@ These controls are accessible from any view: - `j`: Move song down in queue - `s`: Save the queue as a playlist - `S`: Shuffle the songs in the queue +- `l`: Load a queue previously saved to the server + +When stmps exits, the queue is automatically recorded to the server, including the position in the song being played. There is a *single* queue per user that can be thusly saved. Because empty queues can not be stored on Subsonic servers, this queue is not automatically loaded; the `l` binding on the queue page will load the previous queue and seek to the last position in the top song. If the currently playing song is moved, the music is stopped before the move, and must be re-started manually. diff --git a/gui_handlers.go b/gui_handlers.go index 14ff0cb..43f8634 100644 --- a/gui_handlers.go +++ b/gui_handlers.go @@ -4,6 +4,8 @@ package main import ( + "log" + "github.com/gdamore/tcell/v2" "github.com/spezifisch/stmps/mpvplayer" "github.com/spezifisch/stmps/subsonic" @@ -117,7 +119,21 @@ func (ui *Ui) ShowPage(name string) { } func (ui *Ui) Quit() { - // TODO savePlayQueue/getPlayQueue + if len(ui.queuePage.queueData.playerQueue) > 0 { + ids := make([]string, len(ui.queuePage.queueData.playerQueue)) + for i, it := range ui.queuePage.queueData.playerQueue { + ids[i] = it.Id + } + // stmps always only ever plays the first song in the queue + pos := ui.player.GetTimePos() + if err := ui.connection.SavePlayQueue(ids, ids[0], int(pos)); err != nil { + log.Printf("error stashing play queue: %s", err) + } + } else { + // The only way to purge a saved play queue is to force an error by providing + // bad data. Therefore, we ignore errors. + _ = ui.connection.SavePlayQueue([]string{"XXX"}, "XXX", 0) + } ui.player.Quit() ui.app.Stop() } diff --git a/help_text.go b/help_text.go index bea4e2e..385a3ea 100644 --- a/help_text.go +++ b/help_text.go @@ -37,6 +37,7 @@ k move selected song up in queue j move selected song down in queue s save queue as a playlist S shuffle the current queue +l load last queue from server ` const helpPagePlaylists = ` diff --git a/mpvplayer/player.go b/mpvplayer/player.go index fb13811..febe14e 100644 --- a/mpvplayer/player.go +++ b/mpvplayer/player.go @@ -376,8 +376,8 @@ func (p *Player) IsSeeking() (bool, error) { return false, nil } -func (p *Player) SeekAbsolute(float64) error { - return nil +func (p *Player) SeekAbsolute(position int) error { + return p.instance.Command([]string{"seek", strconv.Itoa(position), "absolute"}) } func (p *Player) Play() error { diff --git a/page_playlist.go b/page_playlist.go index 358012f..4174bb7 100644 --- a/page_playlist.go +++ b/page_playlist.go @@ -248,6 +248,11 @@ func (p *PlaylistPage) UpdatePlaylists() { if err != nil { p.logger.PrintError("GetPlaylists", err) } + if response == nil { + p.logger.Printf("no error from GetPlaylists, but also no response!") + stop <- true + return + } p.updatingMutex.Lock() defer p.updatingMutex.Unlock() p.ui.playlists = response.Playlists.Playlists diff --git a/page_queue.go b/page_queue.go index a7b12e7..181f35c 100644 --- a/page_queue.go +++ b/page_queue.go @@ -110,6 +110,30 @@ func (ui *Ui) createQueuePage() *QueuePage { queuePage.ui.ShowSelectPlaylist() case 'S': queuePage.shuffle() + case 'l': + go func() { + ssr, err := queuePage.ui.connection.LoadPlayQueue() + if err != nil { + queuePage.logger.Printf("unable to load play queue from server: %s", err) + return + } + queuePage.queueList.Clear() + queuePage.queueData.Clear() + if ssr.PlayQueue.Entries != nil { + for _, ent := range ssr.PlayQueue.Entries { + ui.addSongToQueue(&ent) + } + ui.queuePage.UpdateQueue() + if err := ui.player.Play(); err != nil { + queuePage.logger.Printf("error playing: %s", err) + } + if err = ui.player.Seek(ssr.PlayQueue.Position); err != nil { + queuePage.logger.Printf("unable to seek to position %s: %s", time.Duration(ssr.PlayQueue.Position)*time.Second, err) + } + _ = ui.player.Pause() + } + }() + default: return event } diff --git a/remote/interfaces.go b/remote/interfaces.go index 08dbf2f..b206330 100644 --- a/remote/interfaces.go +++ b/remote/interfaces.go @@ -28,7 +28,7 @@ type ControlledPlayer interface { Play() error Pause() error Stop() error - SeekAbsolute(float64) error + SeekAbsolute(int) error NextTrack() error PreviousTrack() error diff --git a/subsonic/api.go b/subsonic/api.go index 95ae7c3..9c64041 100644 --- a/subsonic/api.go +++ b/subsonic/api.go @@ -125,6 +125,12 @@ type ScanStatus struct { Count int `json:"count"` } +type PlayQueue struct { + Current string `json:"current"` + Position int `json:"position"` + Entries SubsonicEntities `json:"entry"` +} + type Artist struct { Id string `json:"id"` Name string `json:"name"` @@ -271,6 +277,7 @@ type SubsonicResponse struct { Album Album `json:"album"` SearchResults SubsonicResults `json:"searchResult3"` ScanStatus ScanStatus `json:"scanStatus"` + PlayQueue PlayQueue `json:"playQueue"` } type responseWrapper struct { @@ -663,3 +670,21 @@ func (connection *SubsonicConnection) StartScan() error { } return nil } + +func (connection *SubsonicConnection) SavePlayQueue(queueIds []string, current string, position int) error { + query := defaultQuery(connection) + for _, songId := range queueIds { + query.Add("id", songId) + } + query.Set("current", current) + query.Set("position", fmt.Sprintf("%d", position)) + requestUrl := fmt.Sprintf("%s/rest/savePlayQueue?%s", connection.Host, query.Encode()) + _, err := connection.getResponse("SavePlayQueue", requestUrl) + return err +} + +func (connection *SubsonicConnection) LoadPlayQueue() (*SubsonicResponse, error) { + query := defaultQuery(connection) + requestUrl := fmt.Sprintf("%s/rest/getPlayQueue?%s", connection.Host, query.Encode()) + return connection.getResponse("GetPlayQueue", requestUrl) +}