feat(ipc): add an endpoint for getting current track (#911)
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user