From 647984e92e4dc5c6df8cf04dae4287b37bdde1bd Mon Sep 17 00:00:00 2001 From: E1int <110526002+E1int@users.noreply.github.com> Date: Wed, 6 Aug 2025 00:52:46 +0200 Subject: [PATCH 1/7] Add play CLI commands for albums, playlists and tracks --- backend/app.go | 6 ++++++ backend/cmdlineoptions.go | 31 +++++++++++++++++++++++++++---- backend/ipc/api.go | 15 +++++++++++++++ backend/ipc/client.go | 12 ++++++++++++ backend/ipc/server.go | 29 +++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 4 deletions(-) diff --git a/backend/app.go b/backend/app.go index 1eae20e..53d7f18 100644 --- a/backend/app.go +++ b/backend/app.go @@ -558,6 +558,12 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error { return cli.SeekSeconds(SeekToCLIArg) case SeekByCLIArg != 0: return cli.SeekBySeconds(SeekByCLIArg) + case PlayAlbumCLIArg != "": + return cli.PlayAlbum(PlayAlbumCLIArg, FirstTrackCLIArg, *FlagShuffle) + case PlayPlaylistCLIArg != "": + return cli.PlayPlaylist(PlayPlaylistCLIArg, FirstTrackCLIArg, *FlagShuffle) + case PlayTrackCLIArg != "": + return cli.PlayTrack(PlayTrackCLIArg) default: return nil } diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go index c7252ed..883431d 100644 --- a/backend/cmdlineoptions.go +++ b/backend/cmdlineoptions.go @@ -7,10 +7,14 @@ import ( ) var ( - VolumeCLIArg int = -1 - SeekToCLIArg float64 = -1 - SeekByCLIArg float64 = 0 - VolumePctCLIArg float64 = 0 + VolumeCLIArg int = -1 + SeekToCLIArg float64 = -1 + SeekByCLIArg float64 = 0 + VolumePctCLIArg float64 = 0 + PlayAlbumCLIArg string = "" + PlayPlaylistCLIArg string = "" + PlayTrackCLIArg string = "" + FirstTrackCLIArg int = 0 FlagPlay = flag.Bool("play", false, "unpause or begin playback") FlagPause = flag.Bool("pause", false, "pause playback") @@ -19,6 +23,7 @@ var ( FlagNext = flag.Bool("next", false, "seek to next track") FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized") FlagShow = flag.Bool("show", false, "show minimized app") + FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album or -play-playlist)") FlagVersion = flag.Bool("version", false, "print app version and exit") FlagHelp = flag.Bool("help", false, "print command line options and exit") ) @@ -48,6 +53,24 @@ func init() { VolumePctCLIArg = v return err }) + + flag.Func("play-album", "start playing the given album (ID)", func(s string) error { + PlayAlbumCLIArg = s + return nil + }) + flag.Func("play-playlist", "start playing the given playlist (ID)", func(s string) error { + PlayPlaylistCLIArg = s + return nil + }) + flag.Func("play-track", "start playing the given track (ID)", func(s string) error { + PlayTrackCLIArg = s + return nil + }) + flag.Func("first-track", "start playing from given track (positive integer, to be used with either -play-album or -play-playlist)", func(s string) error { + v, err := strconv.Atoi(s) + FirstTrackCLIArg = v + return err + }) } func HaveCommandLineOptions() bool { diff --git a/backend/ipc/api.go b/backend/ipc/api.go index f53a4a4..266a815 100644 --- a/backend/ipc/api.go +++ b/backend/ipc/api.go @@ -5,6 +5,9 @@ import "fmt" const ( PingPath = "/ping" PlayPath = "/transport/play" + PlayAlbumPath = "/transport/play-album" // ?id=&t=&s= + PlayPlaylistPath = "/transport/play-playlist" // ?id=&t=&s= + PlayTrackPath = "/transport/play-track" // ?id= PlayPausePath = "/transport/playpause" PausePath = "/transport/pause" StopPath = "/transport/stop" @@ -37,3 +40,15 @@ func SeekToSecondsPath(secs float64) string { func SeekBySecondsPath(secs float64) string { return fmt.Sprintf("%s?s=%0.2f", SeekByPath, secs) } + +func BuildPlayAlbumPath(id string, firstTrack int, shuffle bool) string { + return fmt.Sprintf("%s?id=%s&t=%d&s=%t", PlayAlbumPath, id, firstTrack, shuffle) +} + +func BuildPlayPlaylistPath(id string, firstTrack int, shuffle bool) string { + return fmt.Sprintf("%s?id=%s&t=%d&s=%t", PlayPlaylistPath, id, firstTrack, shuffle) +} + +func BuildPlayTrackPath(id string) string { + return fmt.Sprintf("%s?id=%s", PlayTrackPath, id) +} diff --git a/backend/ipc/client.go b/backend/ipc/client.go index fbc6737..4c0a22e 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -44,6 +44,18 @@ func (c *Client) Pause() error { return c.sendRequest(PausePath) } +func (c *Client) PlayAlbum(id string, firstTrack int, shuffle bool) error { + return c.sendRequest(BuildPlayAlbumPath(id, firstTrack, shuffle)) +} + +func (c *Client) PlayPlaylist(id string, firstTrack int, shuffle bool) error { + return c.sendRequest(BuildPlayPlaylistPath(id, firstTrack, shuffle)) +} + +func (c *Client) PlayTrack(id string) error { + return c.sendRequest(BuildPlayTrackPath(id)) +} + func (c *Client) PlayPause() error { return c.sendRequest(PlayPausePath) } diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 8de46ad..5d688ae 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -19,6 +19,9 @@ type PlaybackHandler interface { SeekBySeconds(float64) Volume() int SetVolume(int) + PlayAlbum(string, int, bool) error + PlayPlaylist(string, int, bool) error + PlayTrack(string) error } type IPCServer interface { @@ -86,6 +89,13 @@ func (s *serverImpl) createHandler() http.Handler { vol = vol + int(float64(vol)*(pct/100)) s.pbHandler.SetVolume(vol) // will clamp to range for us })) + m.HandleFunc(PlayAlbumPath, s.makeTracklistEndpointHandler(s.pbHandler.PlayAlbum)) + m.HandleFunc(PlayPlaylistPath, s.makeTracklistEndpointHandler(s.pbHandler.PlayPlaylist)) + m.HandleFunc(PlayTrackPath, func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + s.pbHandler.PlayTrack(id) + s.writeOK(w) + }) return m } @@ -108,6 +118,25 @@ func (s *serverImpl) makeFloatEndpointHandler(queryParam string, f func(float64) } } +func (s *serverImpl) makeTracklistEndpointHandler(f func(string, int, bool) error) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + id := query.Get("id") + firstTrack, err := strconv.Atoi(query.Get("t")) + if err != nil { + s.writeErr(w, err) + return + } + shuffle, err := strconv.ParseBool(query.Get("s")) + if err != nil { + s.writeErr(w, err) + return + } + f(id, firstTrack, shuffle) + s.writeOK(w) + } +} + func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) { var r Response b, err := json.Marshal(&r) From 91458a2094a22d4ff77ff30233b11eb3be2fc930 Mon Sep 17 00:00:00 2001 From: E1int <110526002+E1int@users.noreply.github.com> Date: Sat, 9 Aug 2025 21:27:08 +0200 Subject: [PATCH 2/7] Rename play flags --- backend/cmdlineoptions.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go index 883431d..cd13012 100644 --- a/backend/cmdlineoptions.go +++ b/backend/cmdlineoptions.go @@ -54,15 +54,15 @@ func init() { return err }) - flag.Func("play-album", "start playing the given album (ID)", func(s string) error { + flag.Func("play-album-by-id", "start playing the given album (ID)", func(s string) error { PlayAlbumCLIArg = s return nil }) - flag.Func("play-playlist", "start playing the given playlist (ID)", func(s string) error { + flag.Func("play-playlist-by-id", "start playing the given playlist (ID)", func(s string) error { PlayPlaylistCLIArg = s return nil }) - flag.Func("play-track", "start playing the given track (ID)", func(s string) error { + flag.Func("play-track-by-id", "start playing the given track (ID)", func(s string) error { PlayTrackCLIArg = s return nil }) From 6c8d82cda10d7382db6431085c1064460847dded Mon Sep 17 00:00:00 2001 From: E1int <110526002+E1int@users.noreply.github.com> Date: Sat, 9 Aug 2025 22:42:30 +0200 Subject: [PATCH 3/7] Add search CLI commands --- backend/app.go | 23 ++++++++++- backend/cmdlineoptions.go | 32 ++++++++++++---- backend/ipc/api.go | 59 ++++++++++++++++++++--------- backend/ipc/client.go | 72 +++++++++++++++++++++++------------ backend/ipc/server.go | 80 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 213 insertions(+), 53 deletions(-) diff --git a/backend/app.go b/backend/app.go index 53d7f18..71b383a 100644 --- a/backend/app.go +++ b/backend/app.go @@ -192,7 +192,10 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas ipc.DestroyConn() // cleanup socket possibly orphaned by crashed process listener, err := ipc.Listen() if err == nil { - a.ipcServer = ipc.NewServer(a.PlaybackManager, a.callOnReactivate, + a.ipcServer = ipc.NewServer( + a.PlaybackManager, + &a.ServerManager.Server, + a.callOnReactivate, func() { _ = a.callOnExit() }) go a.ipcServer.Serve(listener) } else { @@ -564,6 +567,24 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error { return cli.PlayPlaylist(PlayPlaylistCLIArg, FirstTrackCLIArg, *FlagShuffle) case PlayTrackCLIArg != "": return cli.PlayTrack(PlayTrackCLIArg) + case SearchAlbumCLIArg != "": + data, err := cli.SearchAlbum(SearchAlbumCLIArg) + if err == nil { + fmt.Println(data) + } + return err + case SearchPlaylistCLIArg != "": + data, err := cli.SearchPlaylist(SearchPlaylistCLIArg) + if err == nil { + fmt.Println(data) + } + return err + case SearchTrackCLIArg != "": + data, err := cli.SearchTrack(SearchTrackCLIArg) + if err == nil { + fmt.Println(data) + } + return err default: return nil } diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go index cd13012..ffe8d73 100644 --- a/backend/cmdlineoptions.go +++ b/backend/cmdlineoptions.go @@ -7,14 +7,17 @@ import ( ) var ( - VolumeCLIArg int = -1 - SeekToCLIArg float64 = -1 - SeekByCLIArg float64 = 0 - VolumePctCLIArg float64 = 0 - PlayAlbumCLIArg string = "" - PlayPlaylistCLIArg string = "" - PlayTrackCLIArg string = "" - FirstTrackCLIArg int = 0 + VolumeCLIArg int = -1 + SeekToCLIArg float64 = -1 + SeekByCLIArg float64 = 0 + VolumePctCLIArg float64 = 0 + PlayAlbumCLIArg string = "" + PlayPlaylistCLIArg string = "" + PlayTrackCLIArg string = "" + FirstTrackCLIArg int = 0 + SearchAlbumCLIArg string = "" + SearchPlaylistCLIArg string = "" + SearchTrackCLIArg string = "" FlagPlay = flag.Bool("play", false, "unpause or begin playback") FlagPause = flag.Bool("pause", false, "pause playback") @@ -71,6 +74,19 @@ func init() { FirstTrackCLIArg = v return err }) + + flag.Func("search-album", "search album", func(s string) error { + SearchAlbumCLIArg = s + return nil + }) + flag.Func("search-playlist", "search playlist", func(s string) error { + SearchPlaylistCLIArg = s + return nil + }) + flag.Func("search-track", "search track", func(s string) error { + SearchTrackCLIArg = s + return nil + }) } func HaveCommandLineOptions() bool { diff --git a/backend/ipc/api.go b/backend/ipc/api.go index 266a815..edb6430 100644 --- a/backend/ipc/api.go +++ b/backend/ipc/api.go @@ -1,28 +1,36 @@ package ipc -import "fmt" +import ( + "encoding/json" + "fmt" + "net/url" +) const ( - PingPath = "/ping" - PlayPath = "/transport/play" - PlayAlbumPath = "/transport/play-album" // ?id=&t=&s= - PlayPlaylistPath = "/transport/play-playlist" // ?id=&t=&s= - PlayTrackPath = "/transport/play-track" // ?id= - PlayPausePath = "/transport/playpause" - PausePath = "/transport/pause" - StopPath = "/transport/stop" - PreviousPath = "/transport/previous" - NextPath = "/transport/next" - TimePosPath = "/transport/timepos" // ?s= - SeekByPath = "/transport/seek-by" // ?s=<+/- seconds> - VolumePath = "/volume" // ?v= - VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage> - ShowPath = "/window/show" - QuitPath = "/window/quit" + PingPath = "/ping" + PlayPath = "/transport/play" + PlayAlbumPath = "/transport/play-album" // ?id=&t=&s= + PlayPlaylistPath = "/transport/play-playlist" // ?id=&t=&s= + PlayTrackPath = "/transport/play-track" // ?id= + SearchAlbumPath = "/transport/search-album" // ?s= + SearchPlaylistPath = "/transport/search-playlist" // ?s= + SearchTrackPath = "/transport/search-track" // ?s= + PlayPausePath = "/transport/playpause" + PausePath = "/transport/pause" + StopPath = "/transport/stop" + PreviousPath = "/transport/previous" + NextPath = "/transport/next" + TimePosPath = "/transport/timepos" // ?s= + SeekByPath = "/transport/seek-by" // ?s=<+/- seconds> + VolumePath = "/volume" // ?v= + VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage> + ShowPath = "/window/show" + QuitPath = "/window/quit" ) type Response struct { - Error string `json:"error"` + Data json.RawMessage `json:"data"` + Error string `json:"error"` } func SetVolumePath(vol int) string { @@ -52,3 +60,18 @@ func BuildPlayPlaylistPath(id string, firstTrack int, shuffle bool) string { func BuildPlayTrackPath(id string) string { return fmt.Sprintf("%s?id=%s", PlayTrackPath, id) } + +func BuildSearchAlbumPath(search string) string { + s := url.QueryEscape(search) + return fmt.Sprintf("%s?s=%s", SearchAlbumPath, s) +} + +func BuildSearchPlaylistPath(search string) string { + s := url.QueryEscape(search) + return fmt.Sprintf("%s?s=%s", SearchPlaylistPath, s) +} + +func BuildSearchTrackPath(search string) string { + s := url.QueryEscape(search) + return fmt.Sprintf("%s?s=%s", SearchTrackPath, s) +} diff --git a/backend/ipc/client.go b/backend/ipc/client.go index 4c0a22e..95142cc 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -30,79 +30,103 @@ func Connect() (*Client, error) { } func (c *Client) Ping() error { - if c.sendRequest(PingPath) != nil { - return ErrPingFail - } - return nil + _, err := c.sendRequest(PingPath) + return err } func (c *Client) Play() error { - return c.sendRequest(PlayPath) + _, err := c.sendRequest(PlayPath) + return err } func (c *Client) Pause() error { - return c.sendRequest(PausePath) + _, err := c.sendRequest(PausePath) + return err } func (c *Client) PlayAlbum(id string, firstTrack int, shuffle bool) error { - return c.sendRequest(BuildPlayAlbumPath(id, firstTrack, shuffle)) + _, err := c.sendRequest(BuildPlayAlbumPath(id, firstTrack, shuffle)) + return err } func (c *Client) PlayPlaylist(id string, firstTrack int, shuffle bool) error { - return c.sendRequest(BuildPlayPlaylistPath(id, firstTrack, shuffle)) + _, err := c.sendRequest(BuildPlayPlaylistPath(id, firstTrack, shuffle)) + return err +} + +func (c *Client) SearchAlbum(search string) (string, error) { + return c.sendRequest(BuildSearchAlbumPath(search)) +} + +func (c *Client) SearchPlaylist(search string) (string, error) { + return c.sendRequest(BuildSearchPlaylistPath(search)) +} + +func (c *Client) SearchTrack(search string) (string, error) { + return c.sendRequest(BuildSearchTrackPath(search)) } func (c *Client) PlayTrack(id string) error { - return c.sendRequest(BuildPlayTrackPath(id)) + _, err := c.sendRequest(BuildPlayTrackPath(id)) + return err } func (c *Client) PlayPause() error { - return c.sendRequest(PlayPausePath) + _, err := c.sendRequest(PlayPausePath) + return err } func (c *Client) SeekNext() error { - return c.sendRequest(NextPath) + _, err := c.sendRequest(NextPath) + return err } func (c *Client) SeekBackOrPrevious() error { - return c.sendRequest(PreviousPath) + _, err := c.sendRequest(PreviousPath) + return err } func (c *Client) SeekSeconds(secs float64) error { - return c.sendRequest(SeekToSecondsPath(secs)) + _, err := c.sendRequest(SeekToSecondsPath(secs)) + return err } func (c *Client) SeekBySeconds(secs float64) error { - return c.sendRequest(SeekBySecondsPath(secs)) + _, err := c.sendRequest(SeekBySecondsPath(secs)) + return err } func (c *Client) SetVolume(vol int) error { - return c.sendRequest(SetVolumePath(vol)) + _, err := c.sendRequest(SetVolumePath(vol)) + return err } func (c *Client) AdjustVolumePct(pct float64) error { - return c.sendRequest(AdjustVolumePctPath(pct)) + _, err := c.sendRequest(AdjustVolumePctPath(pct)) + return err } func (c *Client) Show() error { - return c.sendRequest(ShowPath) + _, err := c.sendRequest(ShowPath) + return err } func (c *Client) Quit() error { - return c.sendRequest(QuitPath) + _, err := c.sendRequest(QuitPath) + return err } -func (c *Client) sendRequest(path string) error { +func (c *Client) sendRequest(path string) (string, error) { resp, err := c.httpC.Get("http://supersonic/" + path) if err != nil { - return err + return "", err } defer resp.Body.Close() + var r Response + json.NewDecoder(resp.Body).Decode(&r) if resp.StatusCode != http.StatusOK { - var r Response - json.NewDecoder(resp.Body).Decode(&r) - return errors.New(r.Error) + return "", errors.New(r.Error) } - return nil + return string(r.Data), nil } diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 5d688ae..53c24dd 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -6,6 +6,9 @@ import ( "net" "net/http" "strconv" + "strings" + + "github.com/dweymouth/supersonic/backend/mediaprovider" ) type PlaybackHandler interface { @@ -32,12 +35,13 @@ type IPCServer interface { type serverImpl struct { server *http.Server pbHandler PlaybackHandler + mp *mediaprovider.MediaProvider showFn func() quitFn func() } -func NewServer(pbHandler PlaybackHandler, showFn, quitFn func()) IPCServer { - s := &serverImpl{pbHandler: pbHandler, showFn: showFn, quitFn: quitFn} +func NewServer(pbHandler PlaybackHandler, mp *mediaprovider.MediaProvider, showFn, quitFn func()) IPCServer { + s := &serverImpl{pbHandler: pbHandler, mp: mp, showFn: showFn, quitFn: quitFn} s.server = &http.Server{ Handler: s.createHandler(), } @@ -96,6 +100,52 @@ func (s *serverImpl) createHandler() http.Handler { s.pbHandler.PlayTrack(id) s.writeOK(w) }) + m.HandleFunc(SearchAlbumPath, s.makeSearchEndpointHandler(func(search string) (any, error) { + filter := mediaprovider.NewAlbumFilter(mediaprovider.AlbumFilterOptions{}) + i := (*s.mp).SearchAlbums(search, filter) + + album := i.Next() + albums := make([]mediaprovider.Album, 0) + for album != nil { + albums = append(albums, *album) + album = i.Next() + } + + return albums, nil + })) + m.HandleFunc(SearchPlaylistPath, s.makeSearchEndpointHandler(func(search string) (any, error) { + all, err := (*s.mp).GetPlaylists() + if err != nil { + return nil, err + } + + search = strings.ReplaceAll(search, " ", "") + search = strings.ToLower(search) + + filtered := make([]mediaprovider.Playlist, 0) + for i := 0; i < len(all); i++ { + playlist := all[i] + name := strings.ReplaceAll(playlist.Name, " ", "") + name = strings.ToLower(name) + if strings.Contains(name, search) { + filtered = append(filtered, *playlist) + } + } + + return filtered, nil + })) + m.HandleFunc(SearchTrackPath, s.makeSearchEndpointHandler(func(search string) (any, error) { + i := (*s.mp).IterateTracks(search) + + track := i.Next() + tracks := make([]mediaprovider.Track, 0) + for track != nil { + tracks = append(tracks, *track) + track = i.Next() + } + + return tracks, nil + })) return m } @@ -137,6 +187,22 @@ func (s *serverImpl) makeTracklistEndpointHandler(f func(string, int, bool) erro } } +func (s *serverImpl) makeSearchEndpointHandler(f func(string) (any, error)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + search := r.URL.Query().Get("s") + data, err := f(search) + if err != nil { + s.writeErr(w, err) + return + } + bytes, err := json.Marshal(data) + if err != nil { + s.writeErr(w, err) + } + s.writeData(w, bytes) + } +} + func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) { var r Response b, err := json.Marshal(&r) @@ -146,6 +212,16 @@ func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) { return w.Write(b) } +func (s *serverImpl) writeData(w http.ResponseWriter, data []byte) (int, error) { + r := Response{Data: data} + b, err := json.Marshal(&r) + if err != nil { + return 0, err + } + w.Header().Set("Content-Type", "application/json") + return w.Write(b) +} + func (s *serverImpl) writeErr(w http.ResponseWriter, err error) (int, error) { r := Response{Error: err.Error()} b, err := json.Marshal(&r) From 4f997fc8b8dcc0ee8348140303e4802d6dfc208f Mon Sep 17 00:00:00 2001 From: E1int <110526002+E1int@users.noreply.github.com> Date: Sat, 9 Aug 2025 23:10:50 +0200 Subject: [PATCH 4/7] Allow passing IDs from standard input --- backend/cmdlineoptions.go | 37 +++++++++++++++++++++++++------------ go.mod | 1 + go.sum | 2 ++ main.go | 10 ++++++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go index ffe8d73..531eed3 100644 --- a/backend/cmdlineoptions.go +++ b/backend/cmdlineoptions.go @@ -2,8 +2,11 @@ package backend import ( "flag" + "os" "strconv" "strings" + + "golang.org/x/term" ) var ( @@ -29,6 +32,10 @@ var ( FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album or -play-playlist)") FlagVersion = flag.Bool("version", false, "print app version and exit") FlagHelp = flag.Bool("help", false, "print command line options and exit") + + FlagPlayAlbum *bool + FlagPlayPlaylist *bool + FlagPlayTrack *bool ) func init() { @@ -57,18 +64,24 @@ func init() { return err }) - flag.Func("play-album-by-id", "start playing the given album (ID)", func(s string) error { - PlayAlbumCLIArg = s - return nil - }) - flag.Func("play-playlist-by-id", "start playing the given playlist (ID)", func(s string) error { - PlayPlaylistCLIArg = s - return nil - }) - flag.Func("play-track-by-id", "start playing the given track (ID)", func(s string) error { - PlayTrackCLIArg = s - return nil - }) + if term.IsTerminal(int(os.Stdin.Fd())) { + flag.Func("play-album-by-id", "start playing the album with the given ID (can also be passed from standard input)", func(s string) error { + PlayAlbumCLIArg = s + return nil + }) + flag.Func("play-playlist-by-id", "start playing the playlist with the given ID (can also be passed from standard input)", func(s string) error { + PlayPlaylistCLIArg = s + return nil + }) + flag.Func("play-track-by-id", "start playing the track with the given ID (can also be passed from standard input)", func(s string) error { + PlayTrackCLIArg = s + return nil + }) + } else { + FlagPlayAlbum = flag.Bool("play-album-by-id", false, "") + FlagPlayPlaylist = flag.Bool("play-playlist-by-id", false, "") + FlagPlayTrack = flag.Bool("play-track-by-id", false, "") + } flag.Func("first-track", "start playing from given track (positive integer, to be used with either -play-album or -play-playlist)", func(s string) error { v, err := strconv.Atoi(s) FirstTrackCLIArg = v diff --git a/go.mod b/go.mod index f26226d..c956a07 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/zalando/go-keyring v0.2.6 golang.org/x/net v0.35.0 golang.org/x/sys v0.30.0 + golang.org/x/term v0.29.0 golang.org/x/text v0.22.0 ) diff --git a/go.sum b/go.sum index fbd0355..142f6fa 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,8 @@ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/main.go b/main.go index 25e8563..b9ea66f 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/ui" "github.com/dweymouth/supersonic/ui/util" + "golang.org/x/term" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" @@ -31,6 +32,15 @@ func main() { flag.Usage() return } + if !term.IsTerminal(int(os.Stdin.Fd())) { + if *backend.FlagPlayAlbum { + fmt.Scanln(&backend.PlayAlbumCLIArg) + } else if *backend.FlagPlayPlaylist { + fmt.Scanln(&backend.PlayPlaylistCLIArg) + } else if *backend.FlagPlayTrack { + fmt.Scanln(&backend.PlayTrackCLIArg) + } + } // rest of flag actions are handled in backend.StartupApp myApp, err := backend.StartupApp(res.AppName, res.DisplayName, res.AppVersion, res.AppVersionTag, res.LatestReleaseURL) From 6d4897bc1c627bd3890ce5bc04306d826df44c88 Mon Sep 17 00:00:00 2001 From: E1int <110526002+E1int@users.noreply.github.com> Date: Sat, 9 Aug 2025 23:42:19 +0200 Subject: [PATCH 5/7] Maintain old ping behavior --- backend/ipc/client.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/ipc/client.go b/backend/ipc/client.go index 88c703d..3b91ad6 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -30,8 +30,10 @@ func Connect() (*Client, error) { } func (c *Client) Ping() error { - _, err := c.sendRequest(PingPath) - return err + if _, err := c.sendRequest(PingPath); err != nil { + return ErrPingFail + } + return nil } func (c *Client) Play() error { From ff6fa87a0afd84e608551a082d9dac9c66585080 Mon Sep 17 00:00:00 2001 From: E1int <110526002+E1int@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:52:35 +0200 Subject: [PATCH 6/7] Fix flag descriptions --- backend/cmdlineoptions.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go index 7375e00..22e792c 100644 --- a/backend/cmdlineoptions.go +++ b/backend/cmdlineoptions.go @@ -31,7 +31,7 @@ var ( FlagStopAfterCurrent = flag.Bool("stop-after-current", false, "stop playback after current track") FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized") FlagShow = flag.Bool("show", false, "show minimized app") - FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album or -play-playlist)") + FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album-by-id or -play-playlist-by-id)") FlagVersion = flag.Bool("version", false, "print app version and exit") FlagHelp = flag.Bool("help", false, "print command line options and exit") @@ -90,15 +90,15 @@ func init() { return err }) - flag.Func("search-album", "search album", func(s string) error { + flag.Func("search-album", "search album by name, return results as JSON", func(s string) error { SearchAlbumCLIArg = s return nil }) - flag.Func("search-playlist", "search playlist", func(s string) error { + flag.Func("search-playlist", "search playlist by name, return results as JSON", func(s string) error { SearchPlaylistCLIArg = s return nil }) - flag.Func("search-track", "search track", func(s string) error { + flag.Func("search-track", "search track by name, return results as JSON", func(s string) error { SearchTrackCLIArg = s return nil }) From 59082cd6d64cd601cc66a4ba2d02cb46776ce55d Mon Sep 17 00:00:00 2001 From: E1int <110526002+E1int@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:15:57 +0200 Subject: [PATCH 7/7] Handle server disconnections/changes --- backend/app.go | 2 +- backend/ipc/server.go | 34 ++++++++++++++++++++++++++++------ backend/servermanager.go | 4 ++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/backend/app.go b/backend/app.go index dca6bc9..274dc77 100644 --- a/backend/app.go +++ b/backend/app.go @@ -195,7 +195,7 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas if err == nil { a.ipcServer = ipc.NewServer( a.PlaybackManager, - &a.ServerManager.Server, + a.ServerManager, a.callOnReactivate, func() { _ = a.callOnExit() }) go a.ipcServer.Serve(listener) diff --git a/backend/ipc/server.go b/backend/ipc/server.go index f9d28c5..d5dc662 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -3,6 +3,7 @@ package ipc import ( "context" "encoding/json" + "errors" "net" "net/http" "strconv" @@ -11,6 +12,8 @@ import ( "github.com/dweymouth/supersonic/backend/mediaprovider" ) +var ErrNoServerConnection = errors.New("not connected to a server") + type PlaybackHandler interface { PlayPause() Stop() @@ -33,16 +36,20 @@ type IPCServer interface { Shutdown(context.Context) error } +type ServerManager interface { + GetServer() mediaprovider.MediaProvider +} + type serverImpl struct { server *http.Server pbHandler PlaybackHandler - mp *mediaprovider.MediaProvider + sm ServerManager showFn func() quitFn func() } -func NewServer(pbHandler PlaybackHandler, mp *mediaprovider.MediaProvider, showFn, quitFn func()) IPCServer { - s := &serverImpl{pbHandler: pbHandler, mp: mp, showFn: showFn, quitFn: quitFn} +func NewServer(pbHandler PlaybackHandler, sm ServerManager, showFn, quitFn func()) IPCServer { + s := &serverImpl{pbHandler: pbHandler, sm: sm, showFn: showFn, quitFn: quitFn} s.server = &http.Server{ Handler: s.createHandler(), } @@ -105,8 +112,13 @@ func (s *serverImpl) createHandler() http.Handler { s.writeOK(w) }) m.HandleFunc(SearchAlbumPath, s.makeSearchEndpointHandler(func(search string) (any, error) { + mp := s.sm.GetServer() + if mp == nil { + return nil, ErrNoServerConnection + } + filter := mediaprovider.NewAlbumFilter(mediaprovider.AlbumFilterOptions{}) - i := (*s.mp).SearchAlbums(search, filter) + i := mp.SearchAlbums(search, filter) album := i.Next() albums := make([]mediaprovider.Album, 0) @@ -118,7 +130,12 @@ func (s *serverImpl) createHandler() http.Handler { return albums, nil })) m.HandleFunc(SearchPlaylistPath, s.makeSearchEndpointHandler(func(search string) (any, error) { - all, err := (*s.mp).GetPlaylists() + mp := s.sm.GetServer() + if mp == nil { + return nil, ErrNoServerConnection + } + + all, err := mp.GetPlaylists() if err != nil { return nil, err } @@ -139,7 +156,12 @@ func (s *serverImpl) createHandler() http.Handler { return filtered, nil })) m.HandleFunc(SearchTrackPath, s.makeSearchEndpointHandler(func(search string) (any, error) { - i := (*s.mp).IterateTracks(search) + mp := s.sm.GetServer() + if mp == nil { + return nil, ErrNoServerConnection + } + + i := mp.IterateTracks(search) track := i.Next() tracks := make([]mediaprovider.Track, 0) diff --git a/backend/servermanager.go b/backend/servermanager.go index 3bba6ef..4cb23de 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -262,3 +262,7 @@ func (s *ServerManager) checkSetInsecureSkipVerify(cli *http.Client) { } } } + +func (a *ServerManager) GetServer() mediaprovider.MediaProvider { + return a.Server +}