Files
bjarneo--cliamp/daemon_stream_title_test.go
Cory Shaw 0c9c30d1c2 feat(ipc): report live radio metadata in status (#319)
* feat(ipc): report live radio metadata in status

A stream's playlist entry only carries the station name, so status
reported that with no artist while the TUI showed the actual song.
Stations broadcast now-playing text inline as ICY metadata (the
StreamTitle field of the SHOUTcast/Icecast protocol), which the UI polls
but never reached the IPC response.

Apply the UI's own resolution in both the TUI and daemon status handlers,
and add TrackInfo.StreamTitle (the raw StreamTitle value) plus
TrackInfo.Station so a client can show station, artist/song, and progress
together.

Resolved at the status handlers rather than in ipcTrackInfo/trackInfo,
since only the now-playing track has stream context.

* fix(ipc): keep the stored title when an ICY tag has no song

A StreamTitle of "Artist - " cuts to an empty title, which blanked the
display instead of falling back to the station name. Guard the split in
resolveTrackDisplay (fixing the TUI and Lua events too, not just status)
and in the daemon, and derive Station from the resolved title so both
paths agree.

Also handle a stream title with no artist in the docs jq example.

Addresses review feedback on #319.

* test(ipc): cover the daemon's stream-title contract

Extract the daemon's stream branch into applyStreamTitle so the test
exercises the real implementation rather than a copy of it, and cover the
"Artist - Title", "Artist - ", title-only, no-metadata, and non-stream
cases. statusResponse needs a live player, so the helper is the seam.

Also guard the docs jq example against a missing .track, which printed a
literal "null" when nothing was loaded.

Addresses review feedback on #319.
2026-08-19 20:08:47 +02:00

79 lines
2.3 KiB
Go

package main
import (
"testing"
"github.com/bjarneo/cliamp/ipc"
"github.com/bjarneo/cliamp/playlist"
)
// TestDaemonStreamTitleFields pins the daemon's IPC contract against the TUI's:
// same split, same fallbacks, same Station derivation. statusResponse itself
// needs a live *player.Player, so it calls the extracted helper directly.
func TestDaemonStreamTitleFields(t *testing.T) {
tests := []struct {
name string
streamTitle string
track playlist.Track
wantTitle string
wantArtist string
wantStation string
wantStream string
}{
{
name: "artist and title split on separator",
streamTitle: "Tycho - Awake",
track: playlist.Track{Title: "NCS Trap Stream", Stream: true},
wantTitle: "Awake",
wantArtist: "Tycho",
wantStation: "NCS Trap Stream",
wantStream: "Tycho - Awake",
},
{
name: "empty title after the separator keeps the station",
streamTitle: "Tycho - ",
track: playlist.Track{Title: "Lofi Stream", Stream: true},
wantTitle: "Lofi Stream",
wantStream: "Tycho - ",
},
{
name: "title-only metadata becomes the title",
streamTitle: "Morning Session",
track: playlist.Track{Title: "Lofi Stream", Stream: true},
wantTitle: "Morning Session",
wantStation: "Lofi Stream",
wantStream: "Morning Session",
},
{
name: "no metadata leaves the entry untouched",
track: playlist.Track{Title: "Lofi Stream", Stream: true},
wantTitle: "Lofi Stream",
},
{
name: "non-stream track is never rewritten",
streamTitle: "Tycho - Awake",
track: playlist.Track{Title: "Alien Boy", Artist: "Oliver Tree"},
wantTitle: "Alien Boy",
wantArtist: "Oliver Tree",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
info := ipc.TrackInfo{Title: tc.track.Title, Artist: tc.track.Artist}
applyStreamTitle(&info, tc.track, tc.streamTitle)
for _, f := range []struct{ field, got, want string }{
{"Title", info.Title, tc.wantTitle},
{"Artist", info.Artist, tc.wantArtist},
{"Station", info.Station, tc.wantStation},
{"StreamTitle", info.StreamTitle, tc.wantStream},
} {
if f.got != f.want {
t.Errorf("%s = %q, want %q", f.field, f.got, f.want)
}
}
})
}
}