Add search CLI commands

This commit is contained in:
E1int
2025-08-09 22:42:30 +02:00
parent 91458a2094
commit 6c8d82cda1
5 changed files with 213 additions and 53 deletions
+22 -1
View File
@@ -192,7 +192,10 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
ipc.DestroyConn() // cleanup socket possibly orphaned by crashed process ipc.DestroyConn() // cleanup socket possibly orphaned by crashed process
listener, err := ipc.Listen() listener, err := ipc.Listen()
if err == nil { 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() }) func() { _ = a.callOnExit() })
go a.ipcServer.Serve(listener) go a.ipcServer.Serve(listener)
} else { } else {
@@ -564,6 +567,24 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
return cli.PlayPlaylist(PlayPlaylistCLIArg, FirstTrackCLIArg, *FlagShuffle) return cli.PlayPlaylist(PlayPlaylistCLIArg, FirstTrackCLIArg, *FlagShuffle)
case PlayTrackCLIArg != "": case PlayTrackCLIArg != "":
return cli.PlayTrack(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: default:
return nil return nil
} }
+16
View File
@@ -15,6 +15,9 @@ var (
PlayPlaylistCLIArg string = "" PlayPlaylistCLIArg string = ""
PlayTrackCLIArg string = "" PlayTrackCLIArg string = ""
FirstTrackCLIArg int = 0 FirstTrackCLIArg int = 0
SearchAlbumCLIArg string = ""
SearchPlaylistCLIArg string = ""
SearchTrackCLIArg string = ""
FlagPlay = flag.Bool("play", false, "unpause or begin playback") FlagPlay = flag.Bool("play", false, "unpause or begin playback")
FlagPause = flag.Bool("pause", false, "pause playback") FlagPause = flag.Bool("pause", false, "pause playback")
@@ -71,6 +74,19 @@ func init() {
FirstTrackCLIArg = v FirstTrackCLIArg = v
return err 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 { func HaveCommandLineOptions() bool {
+24 -1
View File
@@ -1,6 +1,10 @@
package ipc package ipc
import "fmt" import (
"encoding/json"
"fmt"
"net/url"
)
const ( const (
PingPath = "/ping" PingPath = "/ping"
@@ -8,6 +12,9 @@ const (
PlayAlbumPath = "/transport/play-album" // ?id=<album ID>&t=<firstTrack>&s=<shuffle> PlayAlbumPath = "/transport/play-album" // ?id=<album ID>&t=<firstTrack>&s=<shuffle>
PlayPlaylistPath = "/transport/play-playlist" // ?id=<playlist ID>&t=<firstTrack>&s=<shuffle> PlayPlaylistPath = "/transport/play-playlist" // ?id=<playlist ID>&t=<firstTrack>&s=<shuffle>
PlayTrackPath = "/transport/play-track" // ?id=<track ID> PlayTrackPath = "/transport/play-track" // ?id=<track ID>
SearchAlbumPath = "/transport/search-album" // ?s=<searchQuery>
SearchPlaylistPath = "/transport/search-playlist" // ?s=<searchQuery>
SearchTrackPath = "/transport/search-track" // ?s=<searchQuery>
PlayPausePath = "/transport/playpause" PlayPausePath = "/transport/playpause"
PausePath = "/transport/pause" PausePath = "/transport/pause"
StopPath = "/transport/stop" StopPath = "/transport/stop"
@@ -22,6 +29,7 @@ const (
) )
type Response struct { type Response struct {
Data json.RawMessage `json:"data"`
Error string `json:"error"` Error string `json:"error"`
} }
@@ -52,3 +60,18 @@ func BuildPlayPlaylistPath(id string, firstTrack int, shuffle bool) string {
func BuildPlayTrackPath(id string) string { func BuildPlayTrackPath(id string) string {
return fmt.Sprintf("%s?id=%s", PlayTrackPath, id) 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)
}
+47 -23
View File
@@ -30,79 +30,103 @@ func Connect() (*Client, error) {
} }
func (c *Client) Ping() error { func (c *Client) Ping() error {
if c.sendRequest(PingPath) != nil { _, err := c.sendRequest(PingPath)
return ErrPingFail return err
}
return nil
} }
func (c *Client) Play() error { func (c *Client) Play() error {
return c.sendRequest(PlayPath) _, err := c.sendRequest(PlayPath)
return err
} }
func (c *Client) Pause() error { 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 { 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 { 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 { func (c *Client) PlayTrack(id string) error {
return c.sendRequest(BuildPlayTrackPath(id)) _, err := c.sendRequest(BuildPlayTrackPath(id))
return err
} }
func (c *Client) PlayPause() error { func (c *Client) PlayPause() error {
return c.sendRequest(PlayPausePath) _, err := c.sendRequest(PlayPausePath)
return err
} }
func (c *Client) SeekNext() error { func (c *Client) SeekNext() error {
return c.sendRequest(NextPath) _, err := c.sendRequest(NextPath)
return err
} }
func (c *Client) SeekBackOrPrevious() error { func (c *Client) SeekBackOrPrevious() error {
return c.sendRequest(PreviousPath) _, err := c.sendRequest(PreviousPath)
return err
} }
func (c *Client) SeekSeconds(secs float64) error { 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 { 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 { 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 { func (c *Client) AdjustVolumePct(pct float64) error {
return c.sendRequest(AdjustVolumePctPath(pct)) _, err := c.sendRequest(AdjustVolumePctPath(pct))
return err
} }
func (c *Client) Show() error { func (c *Client) Show() error {
return c.sendRequest(ShowPath) _, err := c.sendRequest(ShowPath)
return err
} }
func (c *Client) Quit() error { 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) resp, err := c.httpC.Get("http://supersonic/" + path)
if err != nil { if err != nil {
return err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var r Response var r Response
json.NewDecoder(resp.Body).Decode(&r) json.NewDecoder(resp.Body).Decode(&r)
return errors.New(r.Error) if resp.StatusCode != http.StatusOK {
return "", errors.New(r.Error)
} }
return nil return string(r.Data), nil
} }
+78 -2
View File
@@ -6,6 +6,9 @@ import (
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/dweymouth/supersonic/backend/mediaprovider"
) )
type PlaybackHandler interface { type PlaybackHandler interface {
@@ -32,12 +35,13 @@ type IPCServer interface {
type serverImpl struct { type serverImpl struct {
server *http.Server server *http.Server
pbHandler PlaybackHandler pbHandler PlaybackHandler
mp *mediaprovider.MediaProvider
showFn func() showFn func()
quitFn func() quitFn func()
} }
func NewServer(pbHandler PlaybackHandler, showFn, quitFn func()) IPCServer { func NewServer(pbHandler PlaybackHandler, mp *mediaprovider.MediaProvider, showFn, quitFn func()) IPCServer {
s := &serverImpl{pbHandler: pbHandler, showFn: showFn, quitFn: quitFn} s := &serverImpl{pbHandler: pbHandler, mp: mp, showFn: showFn, quitFn: quitFn}
s.server = &http.Server{ s.server = &http.Server{
Handler: s.createHandler(), Handler: s.createHandler(),
} }
@@ -96,6 +100,52 @@ func (s *serverImpl) createHandler() http.Handler {
s.pbHandler.PlayTrack(id) s.pbHandler.PlayTrack(id)
s.writeOK(w) 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 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) { func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) {
var r Response var r Response
b, err := json.Marshal(&r) b, err := json.Marshal(&r)
@@ -146,6 +212,16 @@ func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) {
return w.Write(b) 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) { func (s *serverImpl) writeErr(w http.ResponseWriter, err error) (int, error) {
r := Response{Error: err.Error()} r := Response{Error: err.Error()}
b, err := json.Marshal(&r) b, err := json.Marshal(&r)