Extract and display radio ICY metadata (#845)
* extract and display radio ICY metadata * fix crash when casting radio station
This commit is contained in:
@@ -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() {
|
a.PlaybackManager.OnSeek(func() {
|
||||||
playbackStatus := a.PlaybackManager.PlaybackStatus()
|
playbackStatus := a.PlaybackManager.PlaybackStatus()
|
||||||
smtc.UpdatePosition(int(playbackStatus.TimePos*1000), int(playbackStatus.Duration*1000))
|
smtc.UpdatePosition(int(playbackStatus.TimePos*1000), int(playbackStatus.Duration*1000))
|
||||||
|
|||||||
+21
-6
@@ -69,15 +69,28 @@ func InitMPMediaHandler(playbackManager *PlaybackManager, artURLLookup func(trac
|
|||||||
mpMediaEventRecipient = mp
|
mpMediaEventRecipient = mp
|
||||||
C.register_os_remote_commands()
|
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
|
// Asynchronously because artwork fetching can take time
|
||||||
var meta *mediaprovider.MediaItemMetadata
|
var meta *mediaprovider.MediaItemMetadata
|
||||||
if track != nil {
|
if item != nil {
|
||||||
m := track.Metadata()
|
m := item.Metadata()
|
||||||
meta = &m
|
meta = &m
|
||||||
}
|
}
|
||||||
go mp.updateMetadata(meta)
|
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() {
|
mp.playbackManager.OnStopped(func() {
|
||||||
C.set_os_playback_state_stopped()
|
C.set_os_playback_state_stopped()
|
||||||
@@ -103,16 +116,18 @@ func InitMPMediaHandler(playbackManager *PlaybackManager, artURLLookup func(trac
|
|||||||
func (mp *MPMediaHandler) updateMetadata(meta *mediaprovider.MediaItemMetadata) {
|
func (mp *MPMediaHandler) updateMetadata(meta *mediaprovider.MediaItemMetadata) {
|
||||||
var title, artist, artURL string
|
var title, artist, artURL string
|
||||||
var duration int
|
var duration int
|
||||||
if meta != nil && meta.ID != "" {
|
if meta != nil {
|
||||||
title = meta.Name
|
title = meta.Name
|
||||||
|
artist = strings.Join(meta.Artists, ", ")
|
||||||
|
duration = int(meta.Duration.Seconds())
|
||||||
|
}
|
||||||
|
if meta != nil && meta.ID != "" {
|
||||||
var err error
|
var err error
|
||||||
if artURL, err = mp.artURLLookup(meta.CoverArtID); err != nil {
|
if artURL, err = mp.artURLLookup(meta.CoverArtID); err != nil {
|
||||||
if meta.CoverArtID != "" {
|
if meta.CoverArtID != "" {
|
||||||
log.Printf("error fetching art url: %s", err.Error())
|
log.Printf("error fetching art url: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
artist = strings.Join(meta.Artists, ", ")
|
|
||||||
duration = int(meta.Duration.Seconds())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cTitle := C.CString(title)
|
cTitle := C.CString(title)
|
||||||
|
|||||||
+27
-3
@@ -43,6 +43,11 @@ type MPRISHandler struct {
|
|||||||
pm *PlaybackManager
|
pm *PlaybackManager
|
||||||
s *server.Server
|
s *server.Server
|
||||||
evt *events.EventHandler
|
evt *events.EventHandler
|
||||||
|
|
||||||
|
// current radio metadata
|
||||||
|
radioStationName string
|
||||||
|
radioIcyTitle string
|
||||||
|
radioIcyArtist string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMPRISHandler(playerName string, pm *PlaybackManager) *MPRISHandler {
|
func NewMPRISHandler(playerName string, pm *PlaybackManager) *MPRISHandler {
|
||||||
@@ -66,6 +71,14 @@ func NewMPRISHandler(playerName string, pm *PlaybackManager) *MPRISHandler {
|
|||||||
m.evt.Player.OnTitle()
|
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) {
|
pm.OnVolumeChange(func(vol int) {
|
||||||
if m.connErr == nil {
|
if m.connErr == nil {
|
||||||
m.evt.Player.OnVolume()
|
m.evt.Player.OnVolume()
|
||||||
@@ -276,12 +289,23 @@ func (m *MPRISHandler) Metadata() (types.Metadata, error) {
|
|||||||
artURL = u
|
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{
|
mprisMeta := types.Metadata{
|
||||||
TrackId: dbus.ObjectPath(trackObjPath),
|
TrackId: dbus.ObjectPath(trackObjPath),
|
||||||
Length: secondsToMicroseconds(status.Duration),
|
Length: secondsToMicroseconds(status.Duration),
|
||||||
Title: meta.Name,
|
Title: title,
|
||||||
Album: meta.Album,
|
Album: album,
|
||||||
Artist: meta.Artists,
|
Artist: artists,
|
||||||
DiscNumber: discNumber,
|
DiscNumber: discNumber,
|
||||||
TrackNumber: trackNumber,
|
TrackNumber: trackNumber,
|
||||||
UserRating: float64(userRating) / 5,
|
UserRating: float64(userRating) / 5,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
@@ -103,6 +104,8 @@ type playbackEngine struct {
|
|||||||
onStopped []func()
|
onStopped []func()
|
||||||
onPlaying []func()
|
onPlaying []func()
|
||||||
onQueueChange []func()
|
onQueueChange []func()
|
||||||
|
|
||||||
|
onRadioMetadataChange []func(radioName, title, artist string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlaybackEngine(
|
func NewPlaybackEngine(
|
||||||
@@ -698,6 +701,22 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
|
|||||||
url = filepath
|
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 == "" {
|
if url == "" {
|
||||||
return errors.New("no stream URL")
|
return errors.New("no stream URL")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,11 @@ func (p *PlaybackManager) OnSongChange(cb func(nowPlaying mediaprovider.MediaIte
|
|||||||
p.engine.onSongChange = append(p.engine.onSongChange, cb)
|
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.
|
// 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)) {
|
func (p *PlaybackManager) OnPlayTimeUpdate(cb func(curTime float64, totalTime float64, seeked bool)) {
|
||||||
p.engine.onPlayTimeUpdate = append(p.engine.onPlayTimeUpdate, cb)
|
p.engine.onPlayTimeUpdate = append(p.engine.onPlayTimeUpdate, cb)
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ type Player struct {
|
|||||||
peaksEnabled bool
|
peaksEnabled bool
|
||||||
pauseFade bool
|
pauseFade bool
|
||||||
|
|
||||||
|
icyTitleCb func(string)
|
||||||
|
|
||||||
fileLoadedLock sync.Mutex
|
fileLoadedLock sync.Mutex
|
||||||
fileLoadedSig *sync.Cond
|
fileLoadedSig *sync.Cond
|
||||||
|
|
||||||
@@ -130,6 +132,8 @@ func (p *Player) Init(maxCacheMB int) error {
|
|||||||
m.SetOptionString("audio-client-name", p.clientName)
|
m.SetOptionString("audio-client-name", p.clientName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.ObserveProperty(0, "metadata", mpv.FORMAT_NODE)
|
||||||
|
|
||||||
if err := m.Initialize(); err != nil {
|
if err := m.Initialize(); err != nil {
|
||||||
return fmt.Errorf("error initializing mpv: %s", err.Error())
|
return fmt.Errorf("error initializing mpv: %s", err.Error())
|
||||||
}
|
}
|
||||||
@@ -442,6 +446,16 @@ func (p *Player) GetMediaInfo() (MediaInfo, error) {
|
|||||||
return info, nil
|
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) {
|
func (p *Player) getInt64Property(propName string) (int64, error) {
|
||||||
playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64)
|
playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -548,6 +562,11 @@ func (p *Player) eventHandler(ctx context.Context) {
|
|||||||
p.status.Duration = 0
|
p.status.Duration = 0
|
||||||
p.status.TimePos = 0
|
p.status.TimePos = 0
|
||||||
p.setState(player.Stopped)
|
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"))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
|
|||||||
bp.ExtendBaseWidget(bp)
|
bp.ExtendBaseWidget(bp)
|
||||||
|
|
||||||
pm.OnSongChange(bp.onSongChange)
|
pm.OnSongChange(bp.onSongChange)
|
||||||
|
pm.OnRadioMetadataChange(bp.onRadioMetadataChange)
|
||||||
pm.OnWaveformImgUpdate(bp.updateWaveformImg)
|
pm.OnWaveformImgUpdate(bp.updateWaveformImg)
|
||||||
pm.OnPlayTimeUpdate(func(cur, total float64, _ bool) {
|
pm.OnPlayTimeUpdate(func(cur, total float64, _ bool) {
|
||||||
fyne.Do(func() {
|
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) {
|
func (bp *BottomPanel) updateWaveformImg(img *backend.WaveformImage) {
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
bp.Controls.UpdateWaveformImg(img)
|
bp.Controls.UpdateWaveformImg(img)
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
|||||||
app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
||||||
fyne.Do(func() { m.UpdateOnTrackChange(item) })
|
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() {
|
app.PlaybackManager.OnQueueChange(func() {
|
||||||
fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetPlayQueue()) })
|
fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetPlayQueue()) })
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user