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).
Re-runs the cohesion heuristic as tracks are added or queued so headers adapt to playlist growth, while preserving the user's ctrl+h preference via a manual override flag. The Add path uses running counters so the cost stays O(k) per call instead of O(N) per Add.
The heuristic ran on every Add path (including each YTDL batch of 20
tracks), which scaled O(N^2) on incremental loads and silently
overrode the user's Ctrl+H toggle whenever a track was queued. Recompute
only on Replace and on first population; preserve the user's choice
otherwise.
Also dedupe the " · Album" suffix logic into a small helper, and pull
the cohesion ratio into a named constant.
* Implement sticky album headers and hide redundant album suffixes
* Add blank album separator when album ends and next track lacks album info
* Add keybind 'H' to toggle album headers
* Centralize album grouping and header logic
* Change 'toggle headers' keybind from H to ctrl+h
* Use modern Go iterators for playlist rendering and album grouping
* Support ctrl+h to toggle album headers in playlist manager and provider browser
* Add cohesion heuristic to auto-toggle album headers
- File browser bulk-add now emits selected paths in directory listing
order instead of map iteration order, so albums play in track order.
- Add Playlist.Remove plus `x` keybinding to drop the highlighted track
from the casual playlist without restarting cliamp.
Skipping tracks aggressively in a Spotify playlist would frequently pop
open a browser to re-authenticate, even though the librespot session was
still valid. Two issues caused this, plus a contention bug that made the
first easier to hit:
1. isAuthError treated any context.DeadlineExceeded as an auth failure.
When skipping fast, the per-stream 30s context (or a wrapped
DeadlineExceeded surfaced from librespot's chunk fetch when it was
interrupted) was misclassified, kicking off the reconnect path.
Reclassify: deadline/cancellation are NOT auth signals; only
KeyProviderError is.
2. NewStreamer's reconnect path ended in ReconnectInteractive on the
second failure, which always opens a browser. Replace the interactive
fallback with returning playlist.ErrNeedsAuth so the UI can surface a
sign-in prompt rather than yanking the user into a browser tab
mid-skip. Silent reconnect from cached creds is still attempted once.
3. Session.NewStream held s.mu across the librespot network call, so
concurrent NewStream / webApi calls serialized and were more likely
to hit the 30s timeout under rapid skipping. Snapshot s.player under
the lock and call NewStream lock-free.
Tests:
- New TestIsAuthError covers nil, plain errors, deadline/cancellation
(wrapped + bare), and KeyProviderError (wrapped + bare).
Docs:
- docs/spotify.md notes the new behavior: rapid skipping never opens a
browser; sign-in prompts surface in the UI instead.
spotify: stop rapid skipping from triggering browser re-auth
Skipping tracks aggressively in a Spotify playlist would frequently pop
open a browser to re-authenticate, even though the librespot session was
still valid. Two issues caused this, plus a contention bug that made the
first easier to hit:
1. isAuthError treated any context.DeadlineExceeded as an auth failure.
When skipping fast, the per-stream 30s context (or a wrapped
DeadlineExceeded surfaced from librespot's chunk fetch when it was
interrupted) was misclassified, kicking off the reconnect path.
Reclassify: deadline/cancellation are NOT auth signals; only
KeyProviderError is.
2. NewStreamer's reconnect path ended in ReconnectInteractive on the
second failure, which always opens a browser. Replace the interactive
fallback with returning playlist.ErrNeedsAuth so the UI can surface a
sign-in prompt rather than yanking the user into a browser tab
mid-skip. Silent reconnect from cached creds is still attempted once.
3. Session.NewStream held s.mu across the librespot network call, so
concurrent NewStream / webApi calls serialized and were more likely
to hit the 30s timeout under rapid skipping. Snapshot s.player under
the lock and call NewStream lock-free.
Tests:
- New TestIsAuthError covers nil, plain errors, deadline/cancellation
(wrapped + bare), and KeyProviderError (wrapped + bare).
Docs:
- docs/spotify.md notes the new behavior: rapid skipping never opens a
browser; sign-in prompts surface in the UI instead.
spotify: stop rapid skipping from triggering browser re-auth
Skipping tracks aggressively in a Spotify playlist would frequently pop
open a browser to re-authenticate, even though the librespot session was
still valid. Two issues caused this, plus a contention bug that made the
first easier to hit:
1. isAuthError treated any context.DeadlineExceeded as an auth failure.
When skipping fast, the per-stream 30s context (or a wrapped
DeadlineExceeded surfaced from librespot's chunk fetch when it was
interrupted) was misclassified, kicking off the reconnect path.
Reclassify: deadline/cancellation are NOT auth signals; only
KeyProviderError is.
2. NewStreamer's reconnect path ended in ReconnectInteractive on the
second failure, which always opens a browser. Replace the interactive
fallback with returning playlist.ErrNeedsAuth so the UI can surface a
sign-in prompt rather than yanking the user into a browser tab
mid-skip. Silent reconnect from cached creds is still attempted once.
3. Session.NewStream held s.mu across the librespot network call, so
concurrent NewStream / webApi calls serialized and were more likely
to hit the 30s timeout under rapid skipping. Snapshot s.player under
the lock and call NewStream lock-free.
Tests:
- New TestIsAuthError covers nil, plain errors, deadline/cancellation
(wrapped + bare), and KeyProviderError (wrapped + bare).
Docs:
- docs/spotify.md notes the new behavior: rapid skipping never opens a
browser; sign-in prompts surface in the UI instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
Search results overlays now support Enter (play now), a (append), and
q (queue next) on the selected track, instead of only routing to a
Spotify cloud-playlist add. The cloud-playlist add moves to p.
YouTube/SoundCloud net search gains a results picker (ytsearch10 /
scsearch10) using the same keymap, replacing the previous one-shot
ytsearch1 auto-queue. Adds shared playTrackImmediate / appendTrack /
queueTrackNext helpers and closeNetSearch / closeSpotSearch helpers
that drop cached result slices on close.
playlist.IsYTSearch generalises the ytsearch:/ytsearchN:/scsearch:/
scsearchN: prefix check used by IsURL, IsYouTubeURL, and IsYTDL.
* fix(playlist): skip unavailable Spotify tracks during playback
Mark Spotify tracks as unplayable when the API reports restrictions,
skip them in next/prev/activate playback flows, and surface their
state in the playlist UI.
Add coverage for playlist navigation and playback behavior around
unavailable tracks.
* fixes and whatnot