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] 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)