diff --git a/backend/app.go b/backend/app.go index e1e71b5..9d21d48 100644 --- a/backend/app.go +++ b/backend/app.go @@ -449,6 +449,15 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) { } }() }) + a.PlaybackManager.OnRadioMetadataChange(func(radioName, title, artist string) { + if title != "" { + smtc.UpdateMetadata(title, artist) + smtc.UpdatePosition(0, 0) + } else { + smtc.UpdateMetadata(radioName, "") + smtc.UpdatePosition(0, 0) + } + }) a.PlaybackManager.OnSeek(func() { playbackStatus := a.PlaybackManager.PlaybackStatus() smtc.UpdatePosition(int(playbackStatus.TimePos*1000), int(playbackStatus.Duration*1000)) diff --git a/backend/mpmedia_mac.go b/backend/mpmedia_mac.go index e6d36ab..ccbbdad 100644 --- a/backend/mpmedia_mac.go +++ b/backend/mpmedia_mac.go @@ -69,15 +69,28 @@ func InitMPMediaHandler(playbackManager *PlaybackManager, artURLLookup func(trac mpMediaEventRecipient = mp C.register_os_remote_commands() - mp.playbackManager.OnSongChange(func(track mediaprovider.MediaItem, _ *mediaprovider.Track) { + mp.playbackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { // Asynchronously because artwork fetching can take time var meta *mediaprovider.MediaItemMetadata - if track != nil { - m := track.Metadata() + if item != nil { + m := item.Metadata() meta = &m } go mp.updateMetadata(meta) }) + mp.playbackManager.OnRadioMetadataChange(func(radioName, title, artist string) { + if title != "" { + go mp.updateMetadata(&mediaprovider.MediaItemMetadata{ + Name: title, + Artists: []string{artist}, + Album: radioName, + }) + } else { + go mp.updateMetadata(&mediaprovider.MediaItemMetadata{ + Name: radioName, + }) + } + }) mp.playbackManager.OnStopped(func() { C.set_os_playback_state_stopped() @@ -103,16 +116,18 @@ func InitMPMediaHandler(playbackManager *PlaybackManager, artURLLookup func(trac func (mp *MPMediaHandler) updateMetadata(meta *mediaprovider.MediaItemMetadata) { var title, artist, artURL string var duration int - if meta != nil && meta.ID != "" { + if meta != nil { title = meta.Name + artist = strings.Join(meta.Artists, ", ") + duration = int(meta.Duration.Seconds()) + } + if meta != nil && meta.ID != "" { var err error if artURL, err = mp.artURLLookup(meta.CoverArtID); err != nil { if meta.CoverArtID != "" { log.Printf("error fetching art url: %s", err.Error()) } } - artist = strings.Join(meta.Artists, ", ") - duration = int(meta.Duration.Seconds()) } cTitle := C.CString(title) diff --git a/backend/mpris.go b/backend/mpris.go index 72fe67d..b910601 100644 --- a/backend/mpris.go +++ b/backend/mpris.go @@ -43,6 +43,11 @@ type MPRISHandler struct { pm *PlaybackManager s *server.Server evt *events.EventHandler + + // current radio metadata + radioStationName string + radioIcyTitle string + radioIcyArtist string } func NewMPRISHandler(playerName string, pm *PlaybackManager) *MPRISHandler { @@ -66,6 +71,14 @@ func NewMPRISHandler(playerName string, pm *PlaybackManager) *MPRISHandler { m.evt.Player.OnTitle() } }) + pm.OnRadioMetadataChange(func(radioName, title, artist string) { + m.radioStationName = radioName + m.radioIcyTitle = title + m.radioIcyArtist = artist + if m.connErr == nil { + m.evt.Player.OnTitle() + } + }) pm.OnVolumeChange(func(vol int) { if m.connErr == nil { m.evt.Player.OnVolume() @@ -276,12 +289,23 @@ func (m *MPRISHandler) Metadata() (types.Metadata, error) { artURL = u } } + + title := meta.Name + artists := meta.Artists + album := meta.Album + // if playing a radio station, override title/artist with current Icy metadata if present + if m.radioStationName == title && m.radioIcyTitle != "" { + title = m.radioIcyTitle + artists = []string{m.radioIcyArtist} + album = m.radioStationName + } + mprisMeta := types.Metadata{ TrackId: dbus.ObjectPath(trackObjPath), Length: secondsToMicroseconds(status.Duration), - Title: meta.Name, - Album: meta.Album, - Artist: meta.Artists, + Title: title, + Album: album, + Artist: artists, DiscNumber: discNumber, TrackNumber: trackNumber, UserRating: float64(userRating) / 5, diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 53d89f0..20e31bb 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "math/rand" + "strings" "time" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -103,6 +104,8 @@ type playbackEngine struct { onStopped []func() onPlaying []func() onQueueChange []func() + + onRadioMetadataChange []func(radioName, title, artist string) } func NewPlaybackEngine( @@ -698,6 +701,22 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { url = filepath } } + if mpvP, ok := p.player.(*mpv.Player); ok && !isTrack { + mpvP.ObserveIcyRadioTitle(func(icytitle string) { + var title, artist string + if s := strings.Split(icytitle, " - "); len(s) == 2 { + title, artist = s[1], s[0] + } else { + title = icytitle + } + log.Println("Radio metadata changed: ", icytitle) + for _, cb := range p.onRadioMetadataChange { + cb(meta.Name, title, artist) + } + }) + } else if ok { + mpvP.UnobserveIcyRadioTitle() + } if url == "" { return errors.New("no stream URL") } diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 3757de6..8b9e361 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -318,6 +318,11 @@ func (p *PlaybackManager) OnSongChange(cb func(nowPlaying mediaprovider.MediaIte p.engine.onSongChange = append(p.engine.onSongChange, cb) } +// Sets a callback that is notified whenever the Icy radio metadata changes. +func (p *PlaybackManager) OnRadioMetadataChange(cb func(radioName, title, artist string)) { + p.engine.onRadioMetadataChange = append(p.engine.onRadioMetadataChange, cb) +} + // Registers a callback that is notified whenever the play time should be updated. func (p *PlaybackManager) OnPlayTimeUpdate(cb func(curTime float64, totalTime float64, seeked bool)) { p.engine.onPlayTimeUpdate = append(p.engine.onPlayTimeUpdate, cb) diff --git a/backend/player/mpv/player.go b/backend/player/mpv/player.go index 89140ed..b74059d 100644 --- a/backend/player/mpv/player.go +++ b/backend/player/mpv/player.go @@ -72,6 +72,8 @@ type Player struct { peaksEnabled bool pauseFade bool + icyTitleCb func(string) + fileLoadedLock sync.Mutex fileLoadedSig *sync.Cond @@ -130,6 +132,8 @@ func (p *Player) Init(maxCacheMB int) error { m.SetOptionString("audio-client-name", p.clientName) } + m.ObserveProperty(0, "metadata", mpv.FORMAT_NODE) + if err := m.Initialize(); err != nil { return fmt.Errorf("error initializing mpv: %s", err.Error()) } @@ -442,6 +446,16 @@ func (p *Player) GetMediaInfo() (MediaInfo, error) { return info, nil } +func (p *Player) ObserveIcyRadioTitle(cb func(string)) { + p.icyTitleCb = cb + p.mpv.ObserveProperty(1, "metadata/icy-title", mpv.FORMAT_STRING) +} + +func (p *Player) UnobserveIcyRadioTitle() { + p.icyTitleCb = nil + p.mpv.UnobserveProperty(1) +} + func (p *Player) getInt64Property(propName string) (int64, error) { playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64) if err != nil { @@ -548,6 +562,11 @@ func (p *Player) eventHandler(ctx context.Context) { p.status.Duration = 0 p.status.TimePos = 0 p.setState(player.Stopped) + case mpv.EVENT_PROPERTY_CHANGE: + if e.Reply_Userdata == 1 && p.icyTitleCb != nil { + p.icyTitleCb(p.mpv.GetPropertyString("metadata/icy-title")) + } + } } } diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 01b413c..ba98b93 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -35,6 +35,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr bp.ExtendBaseWidget(bp) pm.OnSongChange(bp.onSongChange) + pm.OnRadioMetadataChange(bp.onRadioMetadataChange) pm.OnWaveformImgUpdate(bp.updateWaveformImg) pm.OnPlayTimeUpdate(func(cur, total float64, _ bool) { fyne.Do(func() { @@ -143,6 +144,16 @@ func (bp *BottomPanel) onSongChange(song mediaprovider.MediaItem, _ *mediaprovid }) } +func (bp *BottomPanel) onRadioMetadataChange(radioName, title, artist string) { + fyne.Do(func() { + bp.NowPlaying.Update(&mediaprovider.Track{ + Title: title, + ArtistNames: []string{artist}, + Album: radioName, + }) + }) +} + func (bp *BottomPanel) updateWaveformImg(img *backend.WaveformImage) { fyne.Do(func() { bp.Controls.UpdateWaveformImg(img) diff --git a/ui/mainwindow.go b/ui/mainwindow.go index e5665c1..7e11c4e 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -109,6 +109,15 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { fyne.Do(func() { m.UpdateOnTrackChange(item) }) }) + app.PlaybackManager.OnRadioMetadataChange(func(radioName, title, artist string) { + fyne.Do(func() { + if title != "" { + m.Window.SetTitle(fmt.Sprintf("%s – %s · %s", title, artist, res.DisplayName)) + } else { + m.Window.SetTitle(fmt.Sprintf("%s · %s", radioName, res.DisplayName)) + } + }) + }) app.PlaybackManager.OnQueueChange(func() { fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetPlayQueue()) }) })