feat(ipc): add an endpoint for getting current track (#911)

This commit is contained in:
tdakkota
2026-04-20 19:36:11 -07:00
committed by GitHub
parent 56546a9314
commit cfa45a4647
10 changed files with 94 additions and 18 deletions
+6
View File
@@ -717,6 +717,12 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
return cli.Show() return cli.Show()
case *FlagReloadTheme: case *FlagReloadTheme:
return cli.ReloadTheme() return cli.ReloadTheme()
case *FlagCurrentTrack:
data, err := cli.CurrentTrack()
if err == nil {
fmt.Println(data)
}
return err
case VolumeCLIArg >= 0: case VolumeCLIArg >= 0:
return cli.SetVolume(VolumeCLIArg) return cli.SetVolume(VolumeCLIArg)
case VolumePctCLIArg != 0: case VolumePctCLIArg != 0:
+1
View File
@@ -34,6 +34,7 @@ var (
FlagShow = flag.Bool("show", false, "show minimized app") FlagShow = flag.Bool("show", false, "show minimized app")
FlagReloadTheme = flag.Bool("reload-theme", false, "reload the current theme") FlagReloadTheme = flag.Bool("reload-theme", false, "reload the current theme")
FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album-by-id or -play-playlist-by-id)") FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album-by-id or -play-playlist-by-id)")
FlagCurrentTrack = flag.Bool("current-track", false, "print current track metadata as JSON")
FlagVersion = flag.Bool("version", false, "print app version and exit") FlagVersion = flag.Bool("version", false, "print app version and exit")
FlagHelp = flag.Bool("help", false, "print command line options and exit") FlagHelp = flag.Bool("help", false, "print command line options and exit")
+1
View File
@@ -28,6 +28,7 @@ const (
ShowPath = "/window/show" ShowPath = "/window/show"
ReloadThemePath = "/window/reload-theme" ReloadThemePath = "/window/reload-theme"
QuitPath = "/window/quit" QuitPath = "/window/quit"
CurrentTrackPath = "/current_track"
RateCurrentTrackPath = "/current_track/rate" // ?r=<rating 0-5> RateCurrentTrackPath = "/current_track/rate" // ?r=<rating 0-5>
) )
+4
View File
@@ -118,6 +118,10 @@ func (c *Client) AdjustVolumePct(pct float64) error {
return err return err
} }
func (c *Client) CurrentTrack() (string, error) {
return c.sendRequest(CurrentTrackPath)
}
func (c *Client) RateCurrentTrack(rating int) error { func (c *Client) RateCurrentTrack(rating int) error {
_, err := c.sendRequest(BuildRateCurrentTrackPath(rating)) _, err := c.sendRequest(BuildRateCurrentTrackPath(rating))
return err return err
+36 -8
View File
@@ -29,6 +29,7 @@ type PlaybackHandler interface {
PlayAlbum(string, int, bool) error PlayAlbum(string, int, bool) error
PlayPlaylist(string, int, bool) error PlayPlaylist(string, int, bool) error
PlayTrack(string) error PlayTrack(string) error
NowPlaying() mediaprovider.MediaItem
} }
type IPCServer interface { type IPCServer interface {
@@ -41,16 +42,21 @@ type ServerManager interface {
} }
type serverImpl struct { type serverImpl struct {
server *http.Server server *http.Server
pbHandler PlaybackHandler pbHandler PlaybackHandler
rateFn func(int) rateFn func(int)
sm ServerManager sm ServerManager
showFn func() showFn func()
quitFn func() quitFn func()
reloadThemeFn func() reloadThemeFn func()
} }
func NewServer(pbHandler PlaybackHandler, rateFn func(int), sm ServerManager, showFn, quitFn, reloadThemeFn func()) IPCServer { func NewServer(
pbHandler PlaybackHandler,
rateFn func(int),
sm ServerManager,
showFn, quitFn, reloadThemeFn func(),
) IPCServer {
s := &serverImpl{pbHandler: pbHandler, rateFn: rateFn, sm: sm, showFn: showFn, quitFn: quitFn, reloadThemeFn: reloadThemeFn} s := &serverImpl{pbHandler: pbHandler, rateFn: rateFn, sm: sm, showFn: showFn, quitFn: quitFn, reloadThemeFn: reloadThemeFn}
s.server = &http.Server{ s.server = &http.Server{
Handler: s.createHandler(), Handler: s.createHandler(),
@@ -175,6 +181,13 @@ func (s *serverImpl) createHandler() http.Handler {
return tracks, nil return tracks, nil
})) }))
m.HandleFunc(CurrentTrackPath, s.makeStatusEndpointHandler(func() (any, error) {
track := s.pbHandler.NowPlaying()
if track == nil {
return nil, errors.New("nothing is playing right now")
}
return track.Metadata(), nil
}))
m.HandleFunc(RateCurrentTrackPath, func(w http.ResponseWriter, r *http.Request) { m.HandleFunc(RateCurrentTrackPath, func(w http.ResponseWriter, r *http.Request) {
v := r.URL.Query().Get("r") v := r.URL.Query().Get("r")
if rating, err := strconv.Atoi(v); err == nil { if rating, err := strconv.Atoi(v); err == nil {
@@ -231,6 +244,21 @@ func (s *serverImpl) makeTracklistEndpointHandler(f func(string, int, bool) erro
} }
} }
func (s *serverImpl) makeStatusEndpointHandler(f func() (any, error)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
data, err := f()
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) makeSearchEndpointHandler(f func(string) (any, error)) func(http.ResponseWriter, *http.Request) { func (s *serverImpl) makeSearchEndpointHandler(f func(string) (any, error)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("s") search := r.URL.Query().Get("s")
+22 -4
View File
@@ -1,6 +1,7 @@
package mediaprovider package mediaprovider
import ( import (
"slices"
"time" "time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
@@ -205,11 +206,14 @@ type SavedPlayQueue struct {
} }
type RadioStation struct { type RadioStation struct {
Name string
ID string ID string
StationName string
HomePageURL string HomePageURL string
StreamURL string StreamURL string
CoverArtID string
Title string
Artists []string
CoverArtID string
} }
type MediaItemType int type MediaItemType int
@@ -267,16 +271,30 @@ func (r *RadioStation) Metadata() MediaItemMetadata {
if r == nil { if r == nil {
return MediaItemMetadata{} return MediaItemMetadata{}
} }
name := r.Title
if name == "" {
name = r.StationName
}
return MediaItemMetadata{ return MediaItemMetadata{
Type: MediaItemTypeRadioStation, Type: MediaItemTypeRadioStation,
ID: r.ID, ID: r.ID,
Name: r.Name, Name: name,
CoverArtID: r.CoverArtID, CoverArtID: r.CoverArtID,
Artists: r.Artists,
} }
} }
func (r *RadioStation) Copy() MediaItem { func (r *RadioStation) Copy() MediaItem {
return r // no need to copy since RadioStations are immutable return &RadioStation{
ID: r.ID,
StationName: r.StationName,
HomePageURL: r.HomePageURL,
StreamURL: r.StreamURL,
Title: r.Title,
Artists: slices.Clone(r.Artists),
CoverArtID: r.CoverArtID,
}
} }
type ContentType int type ContentType int
+2 -2
View File
@@ -70,7 +70,7 @@ func (s *subsonicMediaProvider) SearchAll(searchQuery string, maxResults int) ([
r, e := s.GetRadioStations() r, e := s.GetRadioStations()
if e == nil { if e == nil {
radios = sharedutil.FilterSlice(r, func(r *mediaprovider.RadioStation) bool { radios = sharedutil.FilterSlice(r, func(r *mediaprovider.RadioStation) bool {
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(r.Name)), queryLowerWords) return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(r.StationName)), queryLowerWords)
}) })
} }
wg.Done() wg.Done()
@@ -156,7 +156,7 @@ func mergeResults(
results = append(results, &mediaprovider.SearchResult{ results = append(results, &mediaprovider.SearchResult{
Type: mediaprovider.ContentTypeRadioStation, Type: mediaprovider.ContentTypeRadioStation,
ID: r.ID, ID: r.ID,
Name: r.Name, Name: r.StationName,
Item: r, Item: r,
}) })
} }
@@ -505,7 +505,7 @@ func (s *subsonicMediaProvider) GetRadioStations() ([]*mediaprovider.RadioStatio
return &mediaprovider.RadioStation{ return &mediaprovider.RadioStation{
// TODO - subsonic library is missing ID in its radiostation object. add it // TODO - subsonic library is missing ID in its radiostation object. add it
ID: "radio-" + strings.ReplaceAll(rs.Name, " ", ""), ID: "radio-" + strings.ReplaceAll(rs.Name, " ", ""),
Name: rs.Name, StationName: rs.Name,
HomePageURL: rs.HomePageUrl, HomePageURL: rs.HomePageUrl,
StreamURL: rs.StreamUrl, StreamURL: rs.StreamUrl,
CoverArtID: rs.CoverArt, CoverArtID: rs.CoverArt,
+19 -1
View File
@@ -47,6 +47,11 @@ type PlaybackManager struct {
// whether autoplay tracks are currently being fetched/enqueued // whether autoplay tracks are currently being fetched/enqueued
pendingAutoplay bool pendingAutoplay bool
wasLoadTrackPaused bool wasLoadTrackPaused bool
// current radio metadata
radioStationName string
radioIcyTitle string
radioIcyArtist string
} }
type RemotePlaybackDevice struct { type RemotePlaybackDevice struct {
@@ -157,6 +162,11 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
}() }()
} }
}) })
p.OnRadioMetadataChange(func(radioName, title, artist string) {
p.radioStationName = radioName
p.radioIcyTitle = title
p.radioIcyArtist = artist
})
} }
func (p *PlaybackManager) handleWaveformImageSongChange(item mediaprovider.MediaItem) { func (p *PlaybackManager) handleWaveformImageSongChange(item mediaprovider.MediaItem) {
@@ -320,7 +330,15 @@ func (p *PlaybackManager) DisableCallbacks() {
// Gets the now playing media item, if any. // Gets the now playing media item, if any.
func (p *PlaybackManager) NowPlaying() mediaprovider.MediaItem { func (p *PlaybackManager) NowPlaying() mediaprovider.MediaItem {
return p.engine.NowPlaying() item := p.engine.NowPlaying()
if station, ok := item.(*mediaprovider.RadioStation); ok {
station = station.Copy().(*mediaprovider.RadioStation)
station.StationName = p.radioStationName
station.Title = p.radioIcyTitle
station.Artists = []string{p.radioIcyArtist}
item = station
}
return item
} }
func (p *PlaybackManager) NowPlayingIndex() int { func (p *PlaybackManager) NowPlayingIndex() int {
+2 -2
View File
@@ -122,7 +122,7 @@ func (a *RadiosPage) onSearched(query string) {
} else { } else {
query = strings.ToLower(query) query = strings.ToLower(query)
result := sharedutil.FilterSlice(a.radios, func(x *mediaprovider.RadioStation) bool { result := sharedutil.FilterSlice(a.radios, func(x *mediaprovider.RadioStation) bool {
return strings.Contains(strings.ToLower(x.Name), query) return strings.Contains(strings.ToLower(x.StationName), query)
}) })
a.list.SetRadios(result) a.list.SetRadios(result)
} }
@@ -288,7 +288,7 @@ func NewRadioList(nowPlayingIDPtr *string) *RadioList {
row.EnsureUnfocused() row.EnsureUnfocused()
row.ListItemID = id row.ListItemID = id
row.Item = a.radios[id] row.Item = a.radios[id]
row.nameLabel.Segments[0].(*widget.TextSegment).Text = row.Item.Name row.nameLabel.Segments[0].(*widget.TextSegment).Text = row.Item.StationName
row.homePageLink.Text = row.Item.HomePageURL row.homePageLink.Text = row.Item.HomePageURL
if u, err := url.Parse(row.Item.HomePageURL); err == nil { if u, err := url.Parse(row.Item.HomePageURL); err == nil {
row.homePageLink.URL = u row.homePageLink.URL = u