Files
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

311 lines
9.5 KiB
Go

// Package ipc provides Unix socket IPC for remote playback control of cliamp.
// The protocol is newline-delimited JSON over a Unix domain socket.
package ipc
import "time"
// Compile-time interface check.
var _ Dispatcher = DispatcherFunc(nil)
// Request is the JSON command sent by the client.
type Request struct {
Cmd string `json:"cmd"`
Value float64 `json:"value,omitempty"`
Playlist string `json:"playlist,omitempty"`
Path string `json:"path,omitempty"`
Name string `json:"name,omitempty"`
Band int `json:"band,omitempty"`
Sub string `json:"sub,omitempty"`
Args []string `json:"args,omitempty"`
Provider string `json:"provider,omitempty"`
Query string `json:"query,omitempty"`
Artist string `json:"artist,omitempty"`
Album string `json:"album,omitempty"`
Sort string `json:"sort,omitempty"`
Offset int `json:"offset,omitempty"`
Index int `json:"index,omitempty"`
To int `json:"to,omitempty"`
Limit int `json:"limit,omitempty"`
NewName string `json:"new_name,omitempty"`
Track *TrackInfo `json:"track,omitempty"`
Topics []string `json:"topics,omitempty"`
}
// Response is the JSON response sent by the server.
type Response struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
State string `json:"state,omitempty"`
Track *TrackInfo `json:"track,omitempty"`
Position float64 `json:"position,omitempty"`
Duration float64 `json:"duration,omitempty"`
Volume float64 `json:"volume,omitempty"`
Playlist string `json:"playlist,omitempty"`
Index int `json:"index,omitempty"`
Total int `json:"total,omitempty"`
Visualizer string `json:"visualizer,omitempty"`
Shuffle *bool `json:"shuffle,omitempty"`
Repeat string `json:"repeat,omitempty"`
Mono *bool `json:"mono,omitempty"`
Speed float64 `json:"speed,omitempty"`
EQPreset string `json:"eq_preset,omitempty"`
Device string `json:"device,omitempty"`
Output string `json:"output,omitempty"`
Items []string `json:"items,omitempty"`
Theme *ThemeInfo `json:"theme,omitempty"`
Bands []float64 `json:"bands,omitempty"`
EQBands []float64 `json:"eq_bands,omitempty"`
Tracks []TrackInfo `json:"tracks,omitempty"`
Playlists []PlaylistInfo `json:"playlists,omitempty"`
Providers []ProviderInfo `json:"providers,omitempty"`
Artists []ArtistInfo `json:"artists,omitempty"`
Albums []AlbumInfo `json:"albums,omitempty"`
Sorts []SortInfo `json:"sorts,omitempty"`
Lyrics []LyricLine `json:"lyrics,omitempty"`
History []HistoryInfo `json:"history,omitempty"`
Devices []DeviceInfo `json:"devices,omitempty"`
}
// ThemeInfo carries the active theme name and its resolved hex colors.
// Empty hex fields mean the default (ANSI fallback) theme is active.
type ThemeInfo struct {
Name string `json:"name"`
BG string `json:"bg,omitempty"`
Accent string `json:"accent,omitempty"`
Fg string `json:"fg,omitempty"`
BrightFg string `json:"bright_fg,omitempty"`
Green string `json:"green,omitempty"`
Yellow string `json:"yellow,omitempty"`
Red string `json:"red,omitempty"`
}
// PluginDispatcher is the hook the IPC server calls to forward plugin.call and
// plugin.commands requests to the Lua plugin manager. Optional — if nil, those
// subcommands return an error.
type PluginDispatcher interface {
EmitCommand(plugin, command string, args []string) (string, error)
CommandList() []string
}
// TrackInfo is the track metadata in a status response.
type TrackInfo struct {
Title string `json:"title,omitempty"`
Artist string `json:"artist,omitempty"`
Album string `json:"album,omitempty"`
Genre string `json:"genre,omitempty"`
Path string `json:"path"`
AlbumArtURL string `json:"album_art_url,omitempty"`
Year int `json:"year,omitempty"`
TrackNumber int `json:"track_number,omitempty"`
DurationSecs int `json:"duration_secs,omitempty"`
Index int `json:"index,omitempty"`
QueuePosition int `json:"queue_position,omitempty"`
Stream bool `json:"stream,omitempty"`
StreamTitle string `json:"stream_title,omitempty"`
Station string `json:"station,omitempty"`
Realtime bool `json:"realtime,omitempty"`
Bookmark bool `json:"bookmark,omitempty"`
Unplayable bool `json:"unplayable,omitempty"`
}
type PlaylistInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Section string `json:"section,omitempty"`
TrackCount int `json:"track_count,omitempty"`
DurationSecs int `json:"duration_secs,omitempty"`
Favoritable bool `json:"favoritable,omitempty"`
Favorite bool `json:"favorite,omitempty"`
}
type ProviderInfo struct {
Key string `json:"key"`
Name string `json:"name"`
Searchable bool `json:"searchable"`
BrowseArtists bool `json:"browse_artists,omitempty"`
BrowseAlbums bool `json:"browse_albums,omitempty"`
Catalog bool `json:"catalog,omitempty"`
}
type ArtistInfo struct {
ID string `json:"id"`
Name string `json:"name"`
AlbumCount int `json:"album_count,omitempty"`
}
type AlbumInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Artist string `json:"artist,omitempty"`
ArtistID string `json:"artist_id,omitempty"`
Year int `json:"year,omitempty"`
TrackCount int `json:"track_count,omitempty"`
Genre string `json:"genre,omitempty"`
}
type SortInfo struct {
ID string `json:"id"`
Label string `json:"label"`
}
type LyricLine struct {
Start float64 `json:"start"`
Text string `json:"text"`
}
type HistoryInfo struct {
Track TrackInfo `json:"track"`
PlayedAt string `json:"played_at"`
}
type DeviceInfo struct {
Name string `json:"name"`
Active bool `json:"active"`
}
// DispatcherFunc adapts a plain function to the Dispatcher interface.
type DispatcherFunc func(msg any)
// Send implements Dispatcher.
func (f DispatcherFunc) Send(msg any) { f(msg) }
// IPC-specific messages sent to the TUI via prog.Send().
// For shared types (NextMsg, PrevMsg, StopMsg, PlayPauseMsg), see internal/playback.
// PlayMsg requests playback to start (unpause only, not toggle).
type PlayMsg struct{}
// PauseMsg requests playback to pause (pause only, not toggle).
type PauseMsg struct{}
// VolumeMsg requests a relative volume change in dB.
type VolumeMsg struct{ DB float64 }
// SeekMsg requests a relative seek.
type SeekMsg struct{ Offset time.Duration }
// LoadMsg requests loading a playlist by name.
// Reply receives the result so the client can report errors.
type LoadMsg struct {
Playlist string
Reply chan Response
}
// QueueMsg requests queuing a file path for playback.
type QueueMsg struct{ Path string }
// ThemeMsg requests changing the TUI theme by name.
// Reply receives confirmation or error if theme not found.
type ThemeMsg struct {
Name string
Reply chan Response
}
// VisMsg requests changing the active visualizer by name.
// If Name is "next", the visualizer cycles to the next mode.
// Reply receives confirmation or error if mode not found.
type VisMsg struct {
Name string
Reply chan Response
}
// ShuffleMsg requests toggling or setting shuffle mode.
// If Name is "on"/"off", it sets the mode explicitly; "toggle" toggles.
type ShuffleMsg struct {
Name string
Reply chan Response
}
// RepeatMsg requests setting or cycling the repeat mode.
// Name is "off", "all", "one", or "cycle".
type RepeatMsg struct {
Name string
Reply chan Response
}
// MonoMsg requests toggling or setting mono mode.
// If Name is "on"/"off", it sets the mode explicitly; "toggle" toggles.
type MonoMsg struct {
Name string
Reply chan Response
}
// SpeedMsg requests setting the playback speed.
type SpeedMsg struct {
Speed float64
Reply chan Response
}
// EQMsg requests setting EQ preset by name or a single band's gain.
// If Band >= 0, sets that band to Value dB. Otherwise applies preset Name.
type EQMsg struct {
Name string
Band int
Value float64
Reply chan Response
}
// DeviceMsg requests switching the audio output device or listing devices.
// If Name is "list", returns available devices. Otherwise switches to named device.
type DeviceMsg struct {
Name string
Reply chan Response
}
// StatusRequestMsg asks the TUI for current state.
// The TUI writes the response to Reply and closes the channel.
type StatusRequestMsg struct {
Reply chan Response
}
// BandsRequestMsg asks the TUI for the current visualizer band values
// (smoothed) and the active visualizer mode name. Lightweight compared to
// StatusRequestMsg — intended for high-rate polling from external widgets.
type BandsRequestMsg struct {
Reply chan Response
}
type QueueRequestMsg struct {
Op string
Index int
To int
Track *TrackInfo
Reply chan Response
}
type LibraryRequestMsg struct {
Op string
Provider string
Playlist string
Query string
Artist string
Album string
Sort string
Offset int
Limit int
Index int
NewName string
Track *TrackInfo
Reply chan Response
}
type LyricsRequestMsg struct {
Reply chan Response
}
type HistoryRequestMsg struct {
Op string
Limit int
Reply chan Response
}
type URLRequestMsg struct {
URL string
Reply chan Response
}
type SaveRequestMsg struct {
Reply chan Response
}