* feat(spotify): return albums from search
/v1/search was asked for type=track,episode, so an album could never appear
in the results. Searching an artist returned whichever of their tracks
Spotify ranked highest, and there was no way to reach a record as a record.
Albums are now requested too and lead the results, as an album placeholder:
a Track carrying the album's name, artist and year, marked through
ProviderMeta so the UI can tell it apart without knowing which provider
produced it. Placeholders are not streamable, because spotify:album: URIs
are not something go-librespot can play, so SpotifyProvider now implements
provider.AlbumTrackLoader to expand a chosen one into its tracks.
/v1/albums/{id}/tracks returns simplified track objects without the album
they belong to, so the album's own metadata is fetched once and filled in
on every track for display.
* feat(ui): play a whole album from the search results
Enter, a and q on an album expand it through AlbumTrackLoader and then act
on the full record, matching what they already did for a single track:
Enter starts it now, a appends it, q queues it next. Like playTrackImmediate
they add rather than replace, so a queue built up over an evening survives
picking an album.
The overlay stays open while the expansion runs, showing "Loading album...":
closing it would bump the request generation and drop the response. The
in-flight flag is separate from the playlist fetch's so the results screen
only claims to be loading an album when it is.
p is refused on an album with an explanation. The playlist picker adds one
track, an album is many, and Spotify has no single call to add a record to
a playlist.
* feat(ui): group search results into labeled sections
With albums and tracks in one flat list an album read exactly like one of
its own tracks. The results now carry "Albums" and "Tracks" separators in
the same style the playlist already uses for album headers, and the label
repeats at the top of the viewport when it opens mid-section.
Separators take rows of their own, so scrolling counts rendered rows rather
than results, the way albumSeparatorRows does for the playlist. Without it
the cursor could sit below the bottom of the window.
* 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.
* feat(tomlutil): add ParseNamedSections for multi-section documents
* feat(resolve): add AudioFiles and TracksFromPaths helpers
* feat(playlist): add DirSourced flag to Track
* feat(local): support [[dir]] directory sources in playlists
Playlists can now reference directories with [[dir]] sections instead of
listing every track. Directory sources are scanned at load time, so new
files appear and removed files disappear automatically.
- parsePlaylistDoc keeps explicit tracks and dir sources in document order
- expand resolves dirs into tracks, marking them DirSourced; explicit
[[track]] entries always shadow a directory scan of the same path
- savePlaylist preserves [[dir]] sections and skips DirSourced tracks
- bookmarking a dir-sourced track materializes it as an explicit entry so
the bookmark persists
- RemoveTrack refuses dir-sourced tracks; AddTracks dedupes against them
- Playlists()/SearchTracks operate on the expanded view
- CreateDirPlaylist, AddDirSource (deduped), DirSources added
* feat(cli): add --dir flags and playlist dirs subcommand
playlist create and add accept repeatable --dir flags that reference a
directory as a [[dir]] source, and a new 'playlist dirs' subcommand lists
them. --dir cannot be combined with --ssh. enrich skips dir-sourced tracks
and sort notes that they reload in scan order.
* feat(ui): guard edits on dir-sourced playlist tracks
* docs: document [[dir]] directory sources
* docs: show playlist file layout and multi-file pickup
* docs: show adding files/directories across one or many playlists
* fix: address code review findings for directory playlists
- Save playlists with interleaved [[track]]/[[dir]] section order instead of
flattening dirs first, so removals, reorders, enrichment, and bookmark
materialization keep each section's original position.
- Remove UI tracks by matching the persisted explicit track by path, so a
rescan between load and save cannot remove the wrong track.
- Render playlist documents in memory before the atomic rename so a short
write can never truncate an existing playlist.
- Validate all inputs (audio paths, directory sources) before persisting:
create and add fail without leaving partially-written playlists behind.
- Persist directory sources as one atomic batch (AddDirSources).
- Skip unreadable entries during recursive directory scans instead of
aborting the whole scan.
- Wrap directory operations with contextual errors; document directory
sources on the site.
- Regression tests for section-order preservation, atomic batch validation,
no-partial-playlist-on-failure, and unreadable-subdir scans.
* fix: resolve remaining code review findings
- tomlutil: flush and clear state on unrecognized array-table headers so
fields cannot leak into the previous section
- local: propagate playlist read errors instead of rewriting the file
without its [[dir]] sections
- cmd: use plural helpers for the created-playlist message and wrap
playlist load errors in playlist bookmark
- resolve: wrap filesystem errors with operation context
- docs: describe the .toml discovery rule accurately and label the
directory-tree fence
* fix: keep leftover track insertion positions aligned
Two leftovers materialized in one save could land in the wrong slot:
each insertion shifts later sections, so directory positions tracked in
dirPos must be re-aligned after every insertion. Replace the supplier
scan with a pure path check so saves never re-walk the filesystem, and
skip the unreadable-subdir test on Windows where os.Chmod maps to the
read-only attribute instead of Unix permissions.
* fix: persist cross-playlist tracks as explicit entries
A track added from a directory-backed playlist carried its DirSourced
flag into the destination playlist. savePlaylist then dropped it (the
destination has no owning [[dir]] section), so the track was reported as
added but silently lost. Clear the flag on incoming tracks in the
AddTracks merge. Clarify that the playlist listing omits unknown
durations (browser already hides them) and still walks directory sources
to count files.
* fix: only treat supported audio files as dir-supplied
dirSuppliesFile now validates the candidate extension against
player.SupportedExts before the path-containment checks, so non-audio
files added as explicit tracks (e.g. cover.jpg under a [[dir]]) are
appended at the end instead of being inserted before the directory
section.
* test: table-driven coverage for dirSuppliesFile predicate
* test: fix Windows path assertions in dir tests
- Normalize ExpandPath's env-expanded result with filepath.Clean before
comparing: on Windows the raw expansion mixes / and \ separators.
- Assert TestSavePlaylistPreservesDirsAndSkipsDirTracks against the parsed
document instead of raw text: the writer escapes backslashes via %q, so
substring matching of a Windows temp path never matched.
PlaybackReporter and ProgressReporter now return an error. The UI logs it at
the three fire-and-forget call sites, so jellyfin, emby, and navidrome gain
the observability audiobookshelf had — they were dropping their client errors
silently. Player state is read on the UI goroutine and passed into the
closures, so the reports stay race-free.
SearchTracks now returns early on a cancelled context instead of running
one request per library and per hit. Adds Audiobookshelf to the Ctrl+F,
N-browse, and setup-wizard provider lists, documents the three new
capabilities in the automatic-UI list, and asserts the keymap entry's
label and flag.
- ReportNowPlaying no longer posts a zero position, which previously
clobbered the server's stored resume point the instant a track
started before the stream actually began.
- Clear the armed provider resume state when the newly loaded track
list no longer contains the armed path, so finishing then reloading
a book doesn't seek back into the middle of it.
- Tracks() builds only the book or episode track list it needs instead
of discarding one unconditionally.
- ResumeTarget skips arming a seek for a podcast episode whose
duration is unknown, since the player would compute a zero byte
offset and restart it from the beginning anyway.
- Items() caps pagination at 1000 pages as insurance against a server
reporting total: 0 while still returning full pages.
Fetches first 20 tracks from YT music when list=URL is parsed and
plays,remaining tracks are fetched in the background and added in
batches of 20.
Added the --expand-playlist/--no-expand-playlist CLI flags and the
expand_playlist key in the configs. This is switched on by default
Read embedded lyrics (LRC or plain text) and cover art from local file
tags at play time. Lyrics are preferred over network fetch when present;
album art is cached by content hash under ~/.local/share/cliamp/album-art/
and published via mpris:artUrl (Linux) and MPNowPlayingInfoCenter (macOS).