Extract and display radio ICY metadata (#845)

* extract and display radio ICY metadata

* fix crash when casting radio station
This commit is contained in:
Drew Weymouth
2026-02-05 07:51:15 -08:00
committed by GitHub
parent 08c8c6e421
commit 3c46889014
8 changed files with 120 additions and 9 deletions
+9
View File
@@ -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))
+21 -6
View File
@@ -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)
+27 -3
View File
@@ -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,
+19
View File
@@ -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")
}
+5
View File
@@ -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)
+19
View File
@@ -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"))
}
}
}
}
+11
View File
@@ -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)
+9
View File
@@ -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()) })
})