bd8d5d07c0d10de8b829c2359bacc79e9ccd5d18
35 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd8d5d07c0 |
Fix review findings: concurrency, plugin sandbox, provider bugs, and dedup (#254)
* fix(playlist): synchronize Playlist for concurrent plugin/UI access Lua plugin callbacks run on goroutines (hooks, timers, keybinds) and read playlist state through StateProvider closures, while the Bubbletea loop mutates the same *Playlist. The struct had no synchronization, so reads of p.tracks/p.order/p.pos raced with Next/Prev/Add/Remove/shuffle, and Current()'s unguarded p.tracks[idx] could panic on a stale index. Add a sync.Mutex and guard every exported method. Reads and writes are now serialized; no exported method calls another, so defer-unlock cannot deadlock. Fixes the high-severity track/player getter race and the playlist-mutation data race. * fix(luaplugin): block SSRF in cliamp.http doHTTP passed plugin-supplied URLs straight to the HTTP client with no validation, letting a plugin reach loopback, RFC1918, and link-local 169.254.169.254 (cloud metadata), and follow redirects into those targets. Add an ssrfGuard on the dialer Control hook so every connection attempt (including redirects and DNS-rebound hosts) is checked against the resolved IP and rejected if non-public, and reject non-http(s) schemes up front. A per-plugin network permission gate remains a possible follow-up; it is omitted here to avoid breaking existing plugins that legitimately use http. * fix(pluginmgr): sandbox plugin VM during metadata extraction extractMetadata ran DoFile on the entire plugin file with full stdlib, so any top-level os.execute/io call ran unsandboxed the moment a user listed installed plugins. Apply the same luaplugin.Sandbox used by the runtime before executing the file. Exports luaplugin.Sandbox for reuse. * fix(luaplugin): error on oversized fs.read instead of silent truncation fs.read used os.ReadFile (whole file into memory) then sliced to 1MB, returning a silently truncated value as success and not bounding memory for huge files. Stream through io.LimitReader(maxSize+1) and return an explicit error when the file exceeds the limit. * fix(luaplugin): make write allowlist symlink-safe isWriteAllowed validated paths lexically: the strings.Contains(abs, "..") check was dead (filepath.Abs already cleans "..", and it false-positived on filenames like "..bar"), and a symlink planted inside an allowed dir could redirect a write outside it. Resolve symlinks on the deepest existing ancestor of the target and on the allow dirs themselves before the prefix check. Also fixes the same gap in the exec cwd check, which reuses this function. * fix(luaplugin): bound timer callbacks with hookTimeout timer.after/every called CallByParam directly with no context timeout, so a runaway callback held the plugin mutex forever, blocking every other hook, keybind, and visualizer for that plugin. Add a shared callBounded helper that applies hookTimeout (mirroring invokeHook) and use it from both timer paths. * fix(luaplugin): bound visualizer render/init/destroy with hookTimeout RenderVis runs on the UI render loop and called CallByParam with no timeout while holding the plugin mutex; a hanging render() froze the UI with no escape. InitVis/DestroyVis had the same gap. Route all three through callBounded so a misbehaving visualizer times out and render falls back to the previous frame. * fix(luaplugin): wait for async hook goroutines before closing LStates Emit spawned untracked goroutines that call CallByParam on a plugin LState. Close stopped timers/execs then closed every LState without waiting, so a UI event dispatched just before shutdown could run against an already-closed LState (p.L.Close does not take the plugin mutex). Track Emit goroutines in a WaitGroup, set a closing flag under mu to reject late dispatch, and Wait before closing the LStates. * fix(ui): guard spectrum visualizers against zero-width panel renderFirework/renderBubbles/renderSakura compute dotCols = PanelWidth*2 and then mod by dotCols/dotRows/len(bands); on a narrow terminal PanelWidth can be 0, so seed % uint64(dotCols) panicked and crashed the TUI. Add the same dotRows<4 || dotCols<4 early-out the braille-grid renderers already use. * fix(history): do not clobber history on a transient load error Record discarded loadLocked's error and proceeded as if history were empty, so a one-off read failure (permission/I-O glitch) made saveLocked atomically rewrite the file with only the new entry, destroying all prior rows. Propagate the error instead, leaving the on-disk file untouched. * fix(history): detect write errors before committing history file saveLocked ignored errors from writeEntry's formatted writes, so a failure mid-write (e.g. ENOSPC) could still rename a truncated temp file over the real history. Thread writes through an errWriter and abort the rename when a write fails. * fix(lyrics): stop cleanQuery from erasing titles with label words The noise regex matched official/lyric/audio/video as bare substrings followed by .*, so 'Videotape', 'Audioslave', and 'Video Games' were cleaned to empty or truncated queries, breaking lyric lookups. Restrict bare-label stripping to genuine trailing labels (dash suffix, or 'official video/audio' and 'lyric(s) video' phrases). Add regression test cases. * fix(jellyfin,emby): synchronize client token/userID/album cache Client.token, userID, and albumCache were lazily read and written from concurrent tea.Cmd goroutines (Playlists, album browse, SearchTracks all run in their own goroutines) with no synchronization, a data race on the lazy auth writes and the cached slice. Add a mutex guarding those fields, taken only around field access and never across network I/O, with double-checked auth so at most a redundant (not racy) auth can occur. * fix(ipc): close in-flight connections on shutdown handleConn's read loop only observed the 60s per-request read deadline, never s.done, and Close closed only the listener. A still-connected client (e.g. a vis-bands polling client) kept its handler goroutine alive, so Close blocked on wg.Wait for up to 60s. Track live connections and close them in Close so their scanner.Scan unblocks immediately; addConn shares the conn mutex with the done check to close the accept-during-shutdown race. * refactor(ipc): route all reply-waits through waitReply The reply/timeout/shutdown select was open-coded in load/theme/vis/bands/ status and waitReply was used elsewhere with a generic 'timeout' message. Parameterize waitReply with a label and timeout and use it everywhere, which also gives the previously-generic commands specific timeout messages. * fix(ipc): handle accept errors instead of spinning silently acceptLoop swallowed every non-shutdown Accept error and retried at 10/s, treating a permanently closed listener as transient. Return on net.ErrClosed and log other errors before backing off. * fix(ui): notify Lua plugins on jump-to-time seek The jump enter handler called notifyPlayback (MPRIS only) plus notifier.Seeked, skipping plugin notification. Use finishSeek, which calls notifyAll, so a jump seek emits the playback-state event to plugins like every other completed seek. Seek stays synchronous (immediate seek), matching the existing test. * fix(resolve): bound startup feed sniff with a short timeout Args runs on the startup path and called sniffFeedURL, which did a HEAD on the 30s feed/M3U client, so one slow CDN could stall cliamp startup for up to 30s during pure URL classification. Give the sniff a dedicated 5s client. * fix(pluginmgr): reject empty download body An empty 200 response left download returning a non-nil empty slice, so Install wrote a 0-byte plugin file. Treat an empty body as a download error so the next candidate URL is tried (or the install fails cleanly). * fix(pluginmgr): reject raw URLs with no usable filename A raw URL ending in '/' made path.Base yield '.' or '/', producing a degenerate plugin filename. Return a clear error instead. * fix(config): let --low-power=false override config-enabled low power The override only set LowPower when the flag was true, so --low-power=false could not disable a config.toml low_power=true. Assign the flag value directly, matching the other boolean overrides. * fix(main): log MPRIS/NowPlaying init failure instead of dropping it wireMediaCtl's error was silently discarded in TUI mode, so a dbus/MPRIS setup failure left media-control silently disabled with no trace. Log it to the app log (not stderr, which would corrupt the TUI). * docs(plugins): remove unimplemented player getters cliamp.player.eq_preset(), visualizer(), and theme() were documented but never registered in the player API (calling them errors). Implementing them would require exposing model state to plugin goroutines, which would add a data race; remove them from the docs so the reference matches the API. * fix(luaplugin): reject partial bands table in set_eq_preset A bands table missing entries silently zeroed the unset bands. Require all 10 values and raise an arg error otherwise so partial input can't corrupt the EQ curve. * fix(luaplugin): track EmitKey goroutines and gate on shutdown EmitKey spawned fire-and-forget goroutines per keypress that were untracked and not gated on shutdown, so a keypress during Close could call into a closed LState. Track them in the manager WaitGroup and skip dispatch once closing, matching Emit. * fix(mediactl): apply MPRIS volume changes in order Each volume Change spawned its own goroutine to send SetVolumeMsg, so rapid changes could be delivered out of order and an older volume win. send (prog.Send) is goroutine-safe and non-blocking, so call it directly. * fix(mediactl): reject stale-track MPRIS SetPosition trackid was a fixed constant for every track, so SetPosition ignored its trackID argument and would seek whatever track became current after the client read its position. Assign a unique track object path per track change and ignore SetPosition when its trackID does not match the current track. * fix(radio): write favorites atomically save() truncated the real favorites file with os.Create and ignored every write error, so a partial write could lose all favorites. Build the content in memory, write a temp file, and rename so a failed write can't corrupt the existing file and the error surfaces to the caller. * fix(radio): propagate save errors from ToggleFavorite ToggleFavorite discarded the error from Add/Remove (which persist favorites), so a failed save was silently swallowed. Return it to the caller. * refactor(spotify): drop pointless lock around local slice append The mutex around appending the Your Music entry only touched the function-local 'all' slice, guarding nothing shared (the other locks in the loop legitimately guard trackCache). * fix(spotify): return cloned playlist list, not the cache slice Playlists returned the cached slice directly (both on cache hit and after building it), so a caller mutating the result corrupted the cache. Return a clone on both paths. * fix(spotify): return cloned cached tracks Tracks handed out the cached track slice directly, so a caller mutating it corrupted the per-playlist cache. Clone on both the cache-hit and freshly built return paths. (The snapshot-based invalidation within the playlist-list TTL is an intentional caching tradeoff and is left as-is.) * fix(spotify): don't sleep before giving up on rate limit On the final retry the loop slept the full backoff (up to 128s) and then returned the rate-limited error without making another request. Break out immediately on the last attempt. * fix(jellyfin): guard AlbumList against negative offset A negative offset passed the offset>=len check and then panicked at out[offset:end]. Clamp offset to 0 first, matching the Emby client. * docs(playlist): correct MoveQueue comment MoveQueue swaps any two positions, not only adjacent ones. * refactor(ui): simplify dead math.Min in flame tier mapping math.Min(0.65, 0.55) is a constant 0.55; replace with the literal and drop the now-unused math import. * refactor(netease): compare app code against its own constant resp.Code is NetEase's application-level status, not an HTTP status; it was compared against http.StatusOK which only coincidentally shares the value 200. Introduce neteaseCodeOK and use it at all four comparison sites. * fix(resolve): strip audio extension from derived title humanizeBasename left a trailing extension (e.g. .mp3) in the title derived from a URL basename. Drop a recognized audio extension first, leaving non-media dotted suffixes untouched. * fix(resolve): bound RSS feed read size resolveFeed decoded the response body with no size limit. Wrap it in an io.LimitReader (32 MB) so an oversized or malicious feed can't exhaust memory. * fix(navidrome): return cloned cache slices Playlists and Tracks handed out the internal cached slices, so a caller mutating the result corrupted the cache. Return slices.Clone on the cache-hit and freshly built return paths. * fix(player): include ffmpeg stderr in decode error decodeFFmpeg wrapped the exec error but dropped ffmpeg's stderr, where the actual failure reason is written. Surface ExitError.Stderr in the message. * refactor(player): forward speedStreamer.Err directly beep.Streamer already declares Err() error, so the local errorer type assertion was redundant. Call ss.s.Err() directly, matching the eq and volume streamers. * fix(ui): don't drop queued seek/lyric cmds on reconnect When the scheduled stream reconnect fired, the tick handler returned early with only playTrack + tick, discarding the seekCmd/lyricCmd computed earlier in the same tick. Fold them into the reconnect batch. * fix(ui): nav list footer counts reflect committed filter The footer only showed the filtered count while the search input bar was open (searching); once a filter was committed with Enter it fell back to the full count. Add navFilteredTotal and use the filtered count whenever a query is active, across the artist, album, and track footers. * refactor: remove dead internal/control package internal/control duplicated the message types in internal/playback and had no importers (only its own test); the live code uses internal/playback. Remove it. * fix(providers): implement Refresh for self-hosted providers Navidrome, Plex, Jellyfin, and Emby cache playlist/track (and album) data but none implemented playlist.Refresher, so the UI refresh key was a silent no-op for them. Add Refresh to clear the caches (including the jellyfin/emby client album cache) so the next fetch hits the server. * refactor: share minimal-TOML section parser The [[section]] parse loop was duplicated three times (radio loadStations, radio loadFavoriteStations, local loadTOML). Extract tomlutil.ParseSections and rewrite all three against it. Behavior is preserved, including the single-section-type assumption of the original parsers. * refactor: extract shared Emby/Jellyfin client into internal/embyapi The emby and jellyfin clients were ~95% identical (~700 duplicated lines) and had already drifted (jellyfin lacked the negative-offset guard and error wrapping). Extract one shared Client into internal/embyapi, isolating the real differences behind a dialect: auth header scheme (Emby Authorization vs Jellyfin X-Emby-Authorization), ping endpoint, user-id discovery, error prefix, and metadata key. emby and jellyfin become thin wrappers (NewClient, IsStreamURL, type aliases) plus their providers. Client tests are consolidated in embyapi: shared behavior once, dialect specifics per dialect. The client now uses a per- instance http.Client (SetHTTPClient) instead of a package global, which is what makes it testable from the provider packages. |
||
|
|
16a79bbea2 |
spotify: add built-in fallback client_id, friendlier search error
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
Bring-your-own-client_id has been the only path to Spotify since the integration landed, but Spotify's Nov 27 2024 dev-mode quota change silently broke /v1/search for every freshly-registered developer app. Users in #205 and #214 hit a misleading 400 'Invalid limit' that no amount of parameter tweaking could resolve — the restriction is on the app, not the request. Ship the librespot keymaster client_id (the same fallback ncspot and spotify-player use) as a built-in alternative. Spotify's loopback exception lets it work with cliamp's existing :19872 redirect URI, and it predates the Nov 27 cutoff so /v1/search keeps working. Setup wizard now prompts for which client_id to use, leading with the recommendation to register your own app (private rate-limit quota) and offering the shared built-in for users who specifically need search to work despite the dev-mode restriction. The trade-off is documented in the picker intro, docs/spotify.md, config.toml.example, and the site. Also surface a friendlier search error: when Spotify returns the canonical 400 'Invalid limit' on /v1/search, rewrite it to explain that the user's client_id is too new for catalog access and how to fix it. The rewrite only fires on the exact misleading-error signature, so unrelated 400s pass through. Drop the now-unused userCountry cache and market= query param from SearchTracks. The market parameter never actually fixed anything (the real cause was dev-mode); user OAuth tokens carry account country implicitly. /v1/me is still called once per session for userID, used by playlistAccessible to filter non-owned playlists for users on their own dev-mode app. Refs #205, #214 |
||
|
|
da13d0e0e3 |
spotify: use real ISO market from /v1/me on search
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
The previous fix passed market=from_token on /v1/search, but Spotify's Web API no longer accepts that legacy magic value and rejects it with the same misleading 400 'Invalid limit' error it returns for missing market on accounts in regions where one is required. Resolve the user's ISO 3166-1 alpha-2 country from /v1/me (cached alongside userID) and pass it as market. Fall back to omitting the parameter when /v1/me fails — the OAuth user token still scopes results to the account's country in most regions. The /v1/me lookup is attempted at most once per session via a meFetched flag; failures are cached too so a network blip during first search doesn't trigger a request per keystroke. Fixes #205 |
||
|
|
4b6ed1c5da |
spotify: pass market=from_token on /v1/search
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
Spotify's Web API returns misleading 400 'Invalid limit' errors on /v1/search for tokens whose home market is one of the regions where search refuses to run without an explicit market parameter (Pakistan, Bangladesh, and similar). Scoping results to the token's home market via market=from_token resolves it. The session.go comment already documents the misleading-error pattern for this endpoint; this is the regional flavor of the same misdirection. Fixes #205 |
||
|
|
7a68eee191 |
Spotify: stop rapid skipping from triggering browser re-auth (#212)
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> |
||
|
|
cf38560434 |
Drop spclient fallback for Spotify Web API calls
The spclient/login5 token from librespot is not accepted by the Spotify Web API on /v1/search and /v1/me/playlists — Spotify returns misleading errors like 'Invalid limit' or 429 instead of a clean auth failure, which made issue #205 hard to diagnose. When there is no OAuth2 Web API token source, fail fast with ErrNeedsAuth and a clear 'run cliamp spotify reset' message rather than attempting the call with the wrong token. spclient is still used for playback (its actual purpose). Also clamp SearchTracks limit to 1..50 defensively. |
||
|
|
3b2f57aac4 |
Improve provider playlists/song views
Provider playlists pane: - Mark the currently loaded playlist with a ▶ prefix and active style - Group rows under section headers when providers populate Section; Spotify now buckets playlists into Library / Your playlists / Followed - Refresh with Ctrl+R; status message confirms - Empty state names the provider and offers a remediation hint - Show 'Name · 12 tracks · 1h 23m' on rows when data is available Provider browser overlay (N): - Right-aligned per-track durations and a 'N tracks · 47:22' subtitle - Move the / filter input under the title (matches keymap overlay) - Cursor wraps top↔bottom on every screen - Clearer help labels: Play from here / Queue this / Replace queue / Append all Cross-cutting: - Replace 'Loading X...' text with a time-driven braille-dot spinner used everywhere a list is loading - Quick-switch (S/N/P/J/Y/L/R) now works from inside the nav browser and the playlist manager, not just the main pane |
||
|
|
ab56af6a7a |
fix(spotify): cap track page size at 50 to match API limit (#203)
* fix(spotify): cap track page size at 50 to match API limit
Spotify Web API /v1/playlists/{id}/items silently caps `limit` at 50.
The previous spotifyTrackPageSize=100 caused the pagination loop to
advance offset by 100 while the server returned only 50 items per page,
skipping every other 50-item window.
For an 1804-track playlist this fetched 18*50 + 4 = 904 tracks instead
of all 1804, presenting as `[1/904]` in the TUI.
Adds a regression test asserting spotifyTrackPageSize <= 50.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(spotify): drop dead min(50, limit) and tighten test docstring
After capping spotifyTrackPageSize at 50, the min(50, limit) wrapper on
the /v1/me/tracks branch always returned the limit unchanged — drop it.
Also collapse the regression test's docstring: the WHY already lives at
the constant in provider.go, and the prior version pinned to specific
[1/904]/[1/1804] track counts that rot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spotify): make test docstrings follow Go convention
Both test functions in external/spotify/provider_test.go now carry
doc comments starting with the function name, satisfying CodeRabbit's
docstring coverage check (was 50%, threshold 80%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(spotify): convert page-size guard to table-driven form
Per repo coding guidelines (CLAUDE.md "Favor table-driven tests") and
CodeRabbit review on PR #203, restructure the single-case invariant
test to follow the table-driven pattern used elsewhere in the file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c430f581bf |
Simplify Spotify credentials helpers
Three small cleanups from review: 1. Move CredsPath / DeleteCreds into an untagged creds.go so the Windows stub no longer needs its own copy. Drops the credsPath wrapper and the duplicate path-build/remove logic. 2. Change DeleteCreds to (bool, error) so the reset subcommand can distinguish "removed" from "did not exist" without a separate os.Stat round-trip — deleteCreds already swallowed ErrNotExist, so the pre-check was redundant. 3. Trim two over-narration comments. The fallbackMaxAttempts intent is now a single inline note; the invalid_grant block comment is reduced to one line about why we delete on this signal. |
||
|
|
babcdc862b |
Add 'cliamp spotify reset' subcommand
Exposes the credential cleanup that the auto-detection in newSessionFromStored already performs internally, as an explicit recovery action for users who hit a state the detector misses. Removes ~/.config/cliamp/spotify_credentials.json, prints the path that was removed, and tells the user to relaunch and sign in. Updates the Spotify and CLI docs with the new command and refines the user-facing error messages on stale-auth code paths to point at 'cliamp spotify reset' as a concrete next step. |
||
|
|
ed78ae0472 |
Fix treat 429 as auth failure when using spclient fallback token
When the OAuth2 silent refresh fails, the Spotify session continues with the librespot spclient token as a Web API fallback. Spotify rate-limits that token aggressively on /v1/me and friends, so the existing 8-retry exponential backoff (~4 minutes) just produces misleading "rate limited" warnings without ever recovering. Detect the fallback-token state via Session.usingFallbackToken() and cap the retry budget at 2 attempts in that mode. After that, return a playlist.ErrNeedsAuth-wrapped error so the existing sign-in UX in ui/model takes over instead of more silent retries. Also elevate the silent-refresh-failed log line from UserWarn to UserError and tell the user that calls will fail until re-auth. |
||
|
|
90ca8b5d31 |
File-based logging with intent-based applog facade (#192)
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
* feat: file-based logging with intent-based applog facade Closes #176. Adds a slog-backed file logger at ~/.config/cliamp/cliamp.log with a configurable level (log_level config key, --log-level CLI flag) and refactors applog into an intent-based facade with three tiers: - Debug/Info/Warn/Error: file only - Status: footer only (transient UI feedback) - UserWarn/UserError: both file and footer The footer ring buffer is preserved unchanged; the file sink is layered behind an atomic.Pointer[*slog.Logger] so log calls stay lock-free. Migrated all 11 spotify call sites: failures saving credentials and the auth-callback server error to UserError, reconnect/rate-limit warnings to UserWarn, 're-authenticated successfully' to Info+Status. Plugin-side logging and log rotation deferred to follow-ups. * docs(site): add diagnostic logging feature card Keeps site/index.html in sync with docs/configuration.md after the log_level config key was added in abfdc37. * Address CodeRabbit review - config: silently fall back to default for invalid log_level in TOML, matching the loader's behavior for other keys (volume, repeat, etc.) - spotify: extract duplicate re-auth message literal into a const - main: return applied level from initLogging so the startup log records the level that's actually in effect, not the raw config string * Drop Enabled gate from UserWarn/UserError The footer needs the formatted string regardless of file-log level, so the gate was paying for two fmt.Sprintf sites and a branch in exchange for skipping a sub-nanosecond slog dispatch. Diagnostic-only methods (Debug/Info/Warn/Error/logf) keep the gate where it actually avoids the Sprintf cost. * applog: use t.Cleanup for test logger close Resolves three errcheck violations from `defer closeFn()` by switching to `t.Cleanup(func() { _ = closeFn() })`. The explicit underscore documents the discard intent and t.Cleanup is the idiomatic place to register test resource teardown. |
||
|
|
59db044a86 |
dedupe bitrate/sample-rate clamping, inline spotify unavailable check
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
|
||
|
|
bf77ca203e |
fix: skip unavailable Spotify tracks during playback (#173)
* 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 |
||
|
|
1b53660a63 |
refactor: go 1.26 modernize (#178)
* refactor: go 1.26 modernize * coderabbit pr fixes |
||
|
|
5f3586f062 |
feat(spotify): configurable bitrate (#177)
* feat(spotify): configurable bitrate * coderabbit pr fixes * coderabbit pr fixes |
||
|
|
85e816b4e7 |
Cache Spotify playlist list to reduce API calls
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
Playlists() results are cached for 5 minutes to avoid redundant API calls when switching providers. Mutations (add track, create playlist) invalidate the cache. |
||
|
|
235f348840 |
Route spotify stderr messages to in-app log footer
Replace fmt.Fprintf(os.Stderr) calls in the spotify provider with applog.Printf() so messages appear styled in the TUI footer instead of corrupting the alternate screen buffer. Log entries auto-expire after 6 seconds. |
||
|
|
d89186f371 | Fix lint issues | ||
|
|
d09af49c9f |
Decouple providers behind capability interfaces (#154)
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
* Decouple providers behind capability interfaces * Add docs to provider vel * Simplify, refactor and remove dead code * Completely decouple from the ui * Simplify * Dead code cleanup |
||
|
|
53d6516bc0 | Simplify | ||
|
|
19633d6f96 | Add spotify playlist and search | ||
|
|
d593f576b8 |
Add Spotify 'Your Music' support (#116)
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
|
||
|
|
ccb1770fed |
Refactor
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
|
||
|
|
13c6ffff8d |
Fix OAuth port leak by adding context cancellation and defer lis.Close() to prevent address already in use errors when retrying YouTube/Spotify authentication. (#110)
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
|
||
|
|
0aad59437c | Extract magic numbers into named constants and add comments for intentionally ignored errors | ||
|
|
e8b4d860cd |
Fix 403 errors for Spotify playlists saved from other users (#94)
* Fix 403 errors for Spotify playlists saved from other users The Spotify API returns 403 when listing tracks for playlists owned by other users, even when those playlists appear in /v1/me/playlists (e.g. playlists you have saved/followed from other users). Two changes: - Filter /v1/me/playlists to only show playlists owned by the current user or marked as collaborative. Requires a single /v1/me call to get the user's ID (cached after first fetch). - Show a clear error message when 403 occurs instead of the raw HTTP error, to help users who encounter it despite the filter. Fixes #89 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Extract playlistAccessible helper and add unit tests Refactor the playlist filtering condition into a named function playlistAccessible() to make it independently testable without any HTTP calls or mocking infrastructure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address PR review feedback - Move 403 error message from webAPI() into Tracks() where the context is known; avoids misleading message for /v1/me or other endpoints - Clear cached userID in Close(), ensureSession(), and Authenticate() to prevent stale user ID filtering after re-authentication Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
758557fa0d |
feat: add Windows build support (Spotify behind build tags) (#77)
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
* feat: add Windows build support (Spotify behind build tags) - Add //go:build !windows to provider.go, session.go, streamer.go - Add stub_windows.go with no-op Spotify implementation - Add windows/amd64 to release CI matrix (CGO_ENABLED=0) Tested: builds and runs on Windows 11 amd64 (cliamp test --version OK) Linux/macOS CI unchanged (still CGO_ENABLED=1 with native libs) * fix: address review feedback on Windows stub - Move //go:build constraint before package doc comment (Go requirement) - NewStreamer now returns errSpotifyUnavailable instead of nil, nil to prevent nil dereference in callers - Update New() doc comment to accurately describe nil return behavior |
||
|
|
fd8dd62eb2 |
Fix to be compliant with the new spotify api
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-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
|
||
|
|
c34553de95 |
Dont have commited the client id as it will prompt you to log in multiple times for different client ids.
come back to this solution later |
||
|
|
815ff6d6c4 |
fix(spotify): February 2026 API migration (#63)
* Add Spotify provider using go-librespot for native playback Integrates Spotify streaming directly into cliamp's Beep audio pipeline, giving full EQ, visualizer, and gapless playback support for Spotify Premium accounts. Architecture: - external/spotify/streamer.go: Bridges go-librespot AudioSource (interleaved float32) to beep.StreamSeekCloser ([2]float64 pairs) - external/spotify/session.go: OAuth2 authentication with credential persistence in ~/.config/cliamp/spotify_credentials.json - external/spotify/provider.go: playlist.Provider using Spotify Web API for playlists/tracks, go-librespot player.NewStream for audio - player/player.go: StreamerFactory hook for custom URI schemes - player/pipeline.go: spotify:track:xxx URI detection and routing - config/config.go: [spotify] section with enabled flag Enable with `enabled = true` under `[spotify]` in config.toml. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve Spotify OAuth flow: print auth URL, open browser, allow Enter to retry * Add retry with backoff on 429 rate limits, improve auth UX messaging * Add stderr logging for 429 retries, increase timeout to 5min, bump max retries to 8 * Bypass spclient for Web API calls — use direct HTTP with Bearer token to avoid Client-Token header rate limits * Own OAuth2 flow: capture Web API token, auto-close browser tab, use SpotifyTokenCredentials The spclient's internal login5 token gets aggressively rate-limited (429 with Retry-After: 86400) when used against api.spotify.com. Root cause: it's not a standard Web API token. Fix: Run our own OAuth2 flow with the same client_id/scopes, capture the access_token for Web API calls, and pass it to go-librespot via SpotifyTokenCredentials for session auth. Also: custom callback server serves HTML with window.close() script, fixing the browser tab not auto-closing after auth. * Use registered Spotify Developer app client_id for Web API The go-librespot internal client_id (65b708073fc0480ea92a077233ca87bd) is shared across all librespot users and gets aggressively rate-limited for Web API calls (Retry-After: 86400). Now requires a registered Spotify Developer app: - client_id in config.toml [spotify] section - Fixed callback port 19872 for redirect URI - OAuth2 PKCE flow (no client_secret needed) Config: [spotify] enabled = true client_id = "your-client-id" Spotify Developer app redirect URI: http://127.0.0.1:19872/login * Remove internal Spotify scopes that cause 'Illegal scope' with registered apps * Minimal OAuth2 scopes — only playlist-read + streaming + user-read-private * Fix TUI freeze: stop stdin reader goroutine after auth completes The bufio.Scanner goroutine for Enter-to-retry kept reading stdin after OAuth completed, stealing input from Bubbletea's raw terminal handler. Replaced with raw os.Stdin.Read + authDone channel to stop cleanly. * Fix stored credential sessions: do fresh OAuth2 for Web API token on each launch The spclient's login5 token gets 429'd on Web API. On second launch (stored credentials), we were trying refreshWebAPIToken which tested the spclient token — always fails. Now does a quick OAuth2 PKCE flow on each launch to get a fresh Web API token. Falls back to full interactive auth if the token flow fails. * Fix nil CountryCode panic when playing Spotify tracks go-librespot's Player.getUnrestrictedTrack dereferences CountryCode to check media restrictions. We never set it, causing a nil pointer panic. Default to 'US'. * Fix pause/resume restarting track; expand scopes; dynamic country code - Set Stream=false on Spotify tracks — they're seekable, not live streams. The TUI's togglePlayPause treats Stream=true as live (stop+replay on resume), which was restarting songs from the beginning. - Expanded OAuth2 scopes to full standard Web API set (playlist modify, library modify, playback state, recently played, top tracks, follows). Internal Spotify scopes that cause 'Illegal scope' are documented and excluded. - Fetch user's country from /v1/me for accurate media restriction checks instead of hardcoding 'US'. * Dynamic playlist height, persist OAuth2 refresh token - Playlist view now fills available terminal height instead of being capped at 5 items. Recalculates on window resize. 'x' key toggles between compact (5) and full height. Minimum 3 items. - Persist OAuth2 refresh token in spotify_credentials.json. On subsequent launches, silently refresh the Web API token without opening a browser. Falls back to interactive auth if refresh fails. - Extract spotifyOAuthConfig() helper shared by doWebAPIAuth and silentTokenRefresh. * dynamic height * Fix UTF-8 encoding in OAuth callback page (✅ rendered as ✅) Add <meta charset="utf-8"> to both callback HTML pages so the checkmark emoji renders correctly in all browsers. * Fix dynamic playlist height: account for frame padding + 2-line controls Previous calculation used 12 + vis.Rows for fixed UI lines, but missed: - Frame padding (2 lines from Padding(1,3)) - Controls render as 2 lines (VOL + EQ), not 1 Corrected to 17 + vis.Rows. Playlist no longer overflows the terminal. * Fix playlist scroll: account for album separator lines in visible window Album separators (── Album Name (Year) ──) are rendered between tracks from different albums, taking extra lines. adjustScroll only counted track items, so the cursor would move past the visible area before scrolling kicked in. Now counts rendered lines (tracks + separators) to determine when to scroll. * Dynamic frame width: use full terminal width instead of fixed 80 chars The frame and panelWidth were hardcoded to 80/74 chars, causing the help bar to wrap to the next line on wider terminals. Now dynamically sizes to the terminal width on WindowSizeMsg. Album separators, seek bar, controls, and visualizer all scale to the available width. * Fix scroll with mixed album separators: count actual rendered lines Previous fix assumed every track had an album separator. Now uses renderedLineCount() helper that accurately counts lines (tracks + separators) for any range. adjustScroll walks backward from cursor to find the right scroll offset when mixed separator/no-separator tracks are present. * Measure actual UI height instead of counting lines manually The manual fixed-line count (17 + vis.Rows) was wrong — missed various multi-line renders, frame padding, etc. Now renders all non-playlist sections into a probe frame and uses lipgloss.Height() to measure the actual pixel height. Guarantees plVisible matches the real available space regardless of theme, controls layout, or status lines. * Fix x key toggle: properly toggle between compact (5) and dynamic max Previous code had dynMax == plVisible always (broken comparison). Now toggles between 5 and full dynamic height using same probe measurement as WindowSizeMsg. * Add 1-line buffer to dynamic playlist height Probe measurement was consistently 1-2 lines optimistic, causing the bottom of the playlist to clip. Add a 1-line safety margin. * Bump playlist height buffer to -2 lines * Fix playlist height: plVisible is rendered lines, not track count Root cause: renderPlaylist() did `visible := min(m.plVisible, len(tracks))` which conflated rendered line count with track count. When plVisible=15 and len(tracks)=8, visible was capped to 8 — but 8 tracks from different albums render as 16 lines (8 separators + 8 tracks), overflowing. Fix: - Remove min(plVisible, len(tracks)) — plVisible is the rendered line budget - Remove scroll clamping that used track count as line count - Add budget check before separator+track pair: if only 1 line left but need 2 (separator + track), break instead of overflowing - Remove -2 magic buffer from probe measurement — it's now exact - plVisible = height - lipgloss.Height(probeFrame) + 1 (no fudge factors) * Address PR review feedback from Gemini Code Assist - Use strconv.Atoi instead of fmt.Sscanf for year parsing - Add comment explaining Stream: false (pause/resume requires it) - Handle io.ReadAll error on non-OK HTTP response bodies - Log saveCreds errors instead of silently discarding - Log http.Serve errors (filter net.ErrClosed for clean shutdown) - Log country code fetch errors with stderr fallback - Add TODO for configurable bitrate * Add Spotify provider using go-librespot for native playback Integrates Spotify streaming directly into cliamp's Beep audio pipeline, giving full EQ, visualizer, and gapless playback support for Spotify Premium accounts. Architecture: - external/spotify/streamer.go: Bridges go-librespot AudioSource (interleaved float32) to beep.StreamSeekCloser ([2]float64 pairs) - external/spotify/session.go: OAuth2 authentication with credential persistence in ~/.config/cliamp/spotify_credentials.json - external/spotify/provider.go: playlist.Provider using Spotify Web API for playlists/tracks, go-librespot player.NewStream for audio - player/player.go: StreamerFactory hook for custom URI schemes - player/pipeline.go: spotify:track:xxx URI detection and routing - config/config.go: [spotify] section with enabled flag Enable with `enabled = true` under `[spotify]` in config.toml. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve Spotify OAuth flow: print auth URL, open browser, allow Enter to retry * Add retry with backoff on 429 rate limits, improve auth UX messaging * Add stderr logging for 429 retries, increase timeout to 5min, bump max retries to 8 * Bypass spclient for Web API calls — use direct HTTP with Bearer token to avoid Client-Token header rate limits * Own OAuth2 flow: capture Web API token, auto-close browser tab, use SpotifyTokenCredentials The spclient's internal login5 token gets aggressively rate-limited (429 with Retry-After: 86400) when used against api.spotify.com. Root cause: it's not a standard Web API token. Fix: Run our own OAuth2 flow with the same client_id/scopes, capture the access_token for Web API calls, and pass it to go-librespot via SpotifyTokenCredentials for session auth. Also: custom callback server serves HTML with window.close() script, fixing the browser tab not auto-closing after auth. * Use registered Spotify Developer app client_id for Web API The go-librespot internal client_id (65b708073fc0480ea92a077233ca87bd) is shared across all librespot users and gets aggressively rate-limited for Web API calls (Retry-After: 86400). Now requires a registered Spotify Developer app: - client_id in config.toml [spotify] section - Fixed callback port 19872 for redirect URI - OAuth2 PKCE flow (no client_secret needed) Config: [spotify] enabled = true client_id = "your-client-id" Spotify Developer app redirect URI: http://127.0.0.1:19872/login * Remove internal Spotify scopes that cause 'Illegal scope' with registered apps * Minimal OAuth2 scopes — only playlist-read + streaming + user-read-private * Fix TUI freeze: stop stdin reader goroutine after auth completes The bufio.Scanner goroutine for Enter-to-retry kept reading stdin after OAuth completed, stealing input from Bubbletea's raw terminal handler. Replaced with raw os.Stdin.Read + authDone channel to stop cleanly. * Fix stored credential sessions: do fresh OAuth2 for Web API token on each launch The spclient's login5 token gets 429'd on Web API. On second launch (stored credentials), we were trying refreshWebAPIToken which tested the spclient token — always fails. Now does a quick OAuth2 PKCE flow on each launch to get a fresh Web API token. Falls back to full interactive auth if the token flow fails. * Fix nil CountryCode panic when playing Spotify tracks go-librespot's Player.getUnrestrictedTrack dereferences CountryCode to check media restrictions. We never set it, causing a nil pointer panic. Default to 'US'. * Fix pause/resume restarting track; expand scopes; dynamic country code - Set Stream=false on Spotify tracks — they're seekable, not live streams. The TUI's togglePlayPause treats Stream=true as live (stop+replay on resume), which was restarting songs from the beginning. - Expanded OAuth2 scopes to full standard Web API set (playlist modify, library modify, playback state, recently played, top tracks, follows). Internal Spotify scopes that cause 'Illegal scope' are documented and excluded. - Fetch user's country from /v1/me for accurate media restriction checks instead of hardcoding 'US'. * Dynamic playlist height, persist OAuth2 refresh token - Playlist view now fills available terminal height instead of being capped at 5 items. Recalculates on window resize. 'x' key toggles between compact (5) and full height. Minimum 3 items. - Persist OAuth2 refresh token in spotify_credentials.json. On subsequent launches, silently refresh the Web API token without opening a browser. Falls back to interactive auth if refresh fails. - Extract spotifyOAuthConfig() helper shared by doWebAPIAuth and silentTokenRefresh. * dynamic height * Fix UTF-8 encoding in OAuth callback page (✅ rendered as ✅) Add <meta charset="utf-8"> to both callback HTML pages so the checkmark emoji renders correctly in all browsers. * Fix dynamic playlist height: account for frame padding + 2-line controls Previous calculation used 12 + vis.Rows for fixed UI lines, but missed: - Frame padding (2 lines from Padding(1,3)) - Controls render as 2 lines (VOL + EQ), not 1 Corrected to 17 + vis.Rows. Playlist no longer overflows the terminal. * Fix playlist scroll: account for album separator lines in visible window Album separators (── Album Name (Year) ──) are rendered between tracks from different albums, taking extra lines. adjustScroll only counted track items, so the cursor would move past the visible area before scrolling kicked in. Now counts rendered lines (tracks + separators) to determine when to scroll. * Dynamic frame width: use full terminal width instead of fixed 80 chars The frame and panelWidth were hardcoded to 80/74 chars, causing the help bar to wrap to the next line on wider terminals. Now dynamically sizes to the terminal width on WindowSizeMsg. Album separators, seek bar, controls, and visualizer all scale to the available width. * Fix scroll with mixed album separators: count actual rendered lines Previous fix assumed every track had an album separator. Now uses renderedLineCount() helper that accurately counts lines (tracks + separators) for any range. adjustScroll walks backward from cursor to find the right scroll offset when mixed separator/no-separator tracks are present. * Measure actual UI height instead of counting lines manually The manual fixed-line count (17 + vis.Rows) was wrong — missed various multi-line renders, frame padding, etc. Now renders all non-playlist sections into a probe frame and uses lipgloss.Height() to measure the actual pixel height. Guarantees plVisible matches the real available space regardless of theme, controls layout, or status lines. * Fix x key toggle: properly toggle between compact (5) and dynamic max Previous code had dynMax == plVisible always (broken comparison). Now toggles between 5 and full dynamic height using same probe measurement as WindowSizeMsg. * Add 1-line buffer to dynamic playlist height Probe measurement was consistently 1-2 lines optimistic, causing the bottom of the playlist to clip. Add a 1-line safety margin. * Bump playlist height buffer to -2 lines * Fix playlist height: plVisible is rendered lines, not track count Root cause: renderPlaylist() did `visible := min(m.plVisible, len(tracks))` which conflated rendered line count with track count. When plVisible=15 and len(tracks)=8, visible was capped to 8 — but 8 tracks from different albums render as 16 lines (8 separators + 8 tracks), overflowing. Fix: - Remove min(plVisible, len(tracks)) — plVisible is the rendered line budget - Remove scroll clamping that used track count as line count - Add budget check before separator+track pair: if only 1 line left but need 2 (separator + track), break instead of overflowing - Remove -2 magic buffer from probe measurement — it's now exact - plVisible = height - lipgloss.Height(probeFrame) + 1 (no fudge factors) * Address PR review feedback from Gemini Code Assist - Use strconv.Atoi instead of fmt.Sscanf for year parsing - Add comment explaining Stream: false (pause/resume requires it) - Handle io.ReadAll error on non-OK HTTP response bodies - Log saveCreds errors instead of silently discarding - Log http.Serve errors (filter net.ErrClosed for clean shutdown) - Log country code fetch errors with stderr fallback - Add TODO for configurable bitrate * Add Spotify docs to README, update install instructions for fork - Added Spotify section with setup instructions (Developer app, config, OAuth) - Updated install methods: go install, pre-built binaries, build from source - Removed Homebrew tap update from release workflow (fork-specific) * Auto-refresh Web API token: replace static string with TokenSource The webAPIToken was set once during init and never refreshed. Spotify access tokens expire after 1 hour, causing all Web API calls to fail. Now uses oauth2.TokenSource which automatically refreshes the token using the stored refresh token when it expires. No more browser re-authentication after 1 hour — tokens refresh transparently in the background. * Show shuffle (z) and repeat (r) keys in bottom help bar They were only visible in the Ctrl+K keymap overlay but not in the always-visible bottom controls line. * Persist shuffle/repeat state to config on toggle Saves shuffle and repeat preferences to config.toml when toggled via z/r keys, so they survive restarts across all providers. * Move shuffle/repeat to top of Ctrl+K keymap overlay They were at positions 23-24, below the 12-line visible window. Moved them right after volume controls so they're visible without scrolling. * Responsive help bar: drop low-priority hints when terminal is narrow Each help hint has a priority. When the combined width exceeds the terminal width, the lowest-priority hints are dropped first: 100 Spc(⏯) 95 Q(Quit) 90 <>(Trk) 80 +-(Vol) 70 ←→(Seek) 60 Ctrl+K(Keys) 50 Tab(Focus) 40 /(Search) 30 a(Queue) 20 z(Shfl) 20 r(Rpt) On a narrow terminal you still see play/pause, track nav, vol, and quit. On wide terminals everything shows. * Handle config.Save errors for shuffle/repeat Show a status message via saveMsg when persisting fails instead of silently ignoring the error. Addresses Gemini Code Assist review feedback. * fix: don't auto-play when selecting Spotify playlist during playback When a track is already playing, selecting a new playlist now loads the tracks without stopping playback or auto-playing. Users can browse playlists freely while listening. Auto-play only triggers when nothing is currently playing. * fix: auto re-auth on Spotify AES key / session errors When go-librespot fails to retrieve an audio key (e.g. code 2 from Spotify's AP server due to expired/revoked session), the provider now: 1. Detects auth-related errors (KeyProviderError, DeadlineExceeded) 2. Tears down the dead session and clears stored credentials 3. Triggers a fresh OAuth2 interactive flow automatically 4. Retries the stream once with the new session This prevents users from getting stuck in an error loop — no manual credential deletion or CLI commands needed. Adds Session.Reconnect() for hot-swapping the session/player, and deleteCreds() to clear stale stored credentials. * fix: avoid nil window in Reconnect (address review) Create the new session before tearing down the old one so s.sess and s.player are never nil while the mutex is unlocked. Old session/player are closed after the atomic swap completes. Addresses Gemini review comment on PR #4. * auto commit * auto commit * feat(spotify): fallback client ID pool for zero-config setup Users no longer need to register a Spotify Developer app. When no client_id is configured, a random ID is selected from a built-in fallback pool to spread rate-limit load across apps. - Add external/spotify/fallback.go with FallbackClientID() - Add SpotifyConfig.ResolveClientID() with user > fallback priority - SpotifyConfig.IsSet() now returns true when enabled (even without client_id) - Update config.toml.example with Spotify section docs * migrate: Spotify Feb 2026 API changes Spotify deprecated several endpoints and renamed fields (effective Mar 9, 2026 for existing dev mode apps). See: https://developer.spotify.com/documentation/web-api/tutorials/february-2026-migration-guide Changes: - Playlist tracks endpoint: /playlists/{id}/tracks → /playlists/{id}/items - Playlist response field: track → item (with backwards-compat fallback) - Playlist metadata field: tracks.total → items.total (with fallback) - Remove GET /v1/me call for country (field removed from API), default to US - Remove user-read-email scope (email no longer returned by GET /me) - Remove unused 'io' import from session.go - Document removed fields in scope comments (popularity, available_markets, external_ids, country, followers, product) * perf(spotify): reduce API calls with fields filtering and snapshot caching Three optimizations to reduce rate limit pressure: 1. Use 'fields' parameter on both /me/playlists and /playlists/{id}/items to request only the fields we actually parse. Smaller payloads, lower API cost per call. 2. Cache playlist tracks by snapshot_id. When Playlists() is called, we store each playlist's snapshot_id. If the snapshot hasn't changed on the next Tracks() call, we return cached results without hitting the API at all. 3. Include snapshot_id in playlist list fields so cache invalidation is automatic — changed playlists get re-fetched, unchanged ones don't. Ref: https://developer.spotify.com/documentation/web-api/concepts/rate-limits * fix(spotify): skip session when no client ID is available Don't attempt OAuth flow if both user config and fallback pool are empty — prevents the broken authorize URL with empty client_id. * auto commit * chore: remove build/release changes — keep only Spotify API migration Revert .github/workflows/release.yml and install.sh to upstream/main and remove the added .goreleaser.yml so this branch contains only the Spotify February 2026 API migration changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
a2c59fffd2 |
Revert back to item
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-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
|
||
|
|
2453e7e27f | Bug fix (provider.go:126): Changed json:item to json:track — this was preventing ALL track metadata from deserializing. This is likely why playlists appeared empty. | ||
|
|
c89d0225c7 |
use Spotify /items endpoint replacing deprecated /tracks for playlist retrieval
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-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
|
||
|
|
e55101617c |
feat: Spotify playback via go-librespot (#36)
* Add Spotify provider using go-librespot for native playback Integrates Spotify streaming directly into cliamp's Beep audio pipeline, giving full EQ, visualizer, and gapless playback support for Spotify Premium accounts. Architecture: - external/spotify/streamer.go: Bridges go-librespot AudioSource (interleaved float32) to beep.StreamSeekCloser ([2]float64 pairs) - external/spotify/session.go: OAuth2 authentication with credential persistence in ~/.config/cliamp/spotify_credentials.json - external/spotify/provider.go: playlist.Provider using Spotify Web API for playlists/tracks, go-librespot player.NewStream for audio - player/player.go: StreamerFactory hook for custom URI schemes - player/pipeline.go: spotify:track:xxx URI detection and routing - config/config.go: [spotify] section with enabled flag Enable with `enabled = true` under `[spotify]` in config.toml. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve Spotify OAuth flow: print auth URL, open browser, allow Enter to retry * Add retry with backoff on 429 rate limits, improve auth UX messaging * Add stderr logging for 429 retries, increase timeout to 5min, bump max retries to 8 * Bypass spclient for Web API calls — use direct HTTP with Bearer token to avoid Client-Token header rate limits * Own OAuth2 flow: capture Web API token, auto-close browser tab, use SpotifyTokenCredentials The spclient's internal login5 token gets aggressively rate-limited (429 with Retry-After: 86400) when used against api.spotify.com. Root cause: it's not a standard Web API token. Fix: Run our own OAuth2 flow with the same client_id/scopes, capture the access_token for Web API calls, and pass it to go-librespot via SpotifyTokenCredentials for session auth. Also: custom callback server serves HTML with window.close() script, fixing the browser tab not auto-closing after auth. * Use registered Spotify Developer app client_id for Web API The go-librespot internal client_id (65b708073fc0480ea92a077233ca87bd) is shared across all librespot users and gets aggressively rate-limited for Web API calls (Retry-After: 86400). Now requires a registered Spotify Developer app: - client_id in config.toml [spotify] section - Fixed callback port 19872 for redirect URI - OAuth2 PKCE flow (no client_secret needed) Config: [spotify] enabled = true client_id = "your-client-id" Spotify Developer app redirect URI: http://127.0.0.1:19872/login * Remove internal Spotify scopes that cause 'Illegal scope' with registered apps * Minimal OAuth2 scopes — only playlist-read + streaming + user-read-private * Fix TUI freeze: stop stdin reader goroutine after auth completes The bufio.Scanner goroutine for Enter-to-retry kept reading stdin after OAuth completed, stealing input from Bubbletea's raw terminal handler. Replaced with raw os.Stdin.Read + authDone channel to stop cleanly. * Fix stored credential sessions: do fresh OAuth2 for Web API token on each launch The spclient's login5 token gets 429'd on Web API. On second launch (stored credentials), we were trying refreshWebAPIToken which tested the spclient token — always fails. Now does a quick OAuth2 PKCE flow on each launch to get a fresh Web API token. Falls back to full interactive auth if the token flow fails. * Fix nil CountryCode panic when playing Spotify tracks go-librespot's Player.getUnrestrictedTrack dereferences CountryCode to check media restrictions. We never set it, causing a nil pointer panic. Default to 'US'. * Fix pause/resume restarting track; expand scopes; dynamic country code - Set Stream=false on Spotify tracks — they're seekable, not live streams. The TUI's togglePlayPause treats Stream=true as live (stop+replay on resume), which was restarting songs from the beginning. - Expanded OAuth2 scopes to full standard Web API set (playlist modify, library modify, playback state, recently played, top tracks, follows). Internal Spotify scopes that cause 'Illegal scope' are documented and excluded. - Fetch user's country from /v1/me for accurate media restriction checks instead of hardcoding 'US'. * Dynamic playlist height, persist OAuth2 refresh token - Playlist view now fills available terminal height instead of being capped at 5 items. Recalculates on window resize. 'x' key toggles between compact (5) and full height. Minimum 3 items. - Persist OAuth2 refresh token in spotify_credentials.json. On subsequent launches, silently refresh the Web API token without opening a browser. Falls back to interactive auth if refresh fails. - Extract spotifyOAuthConfig() helper shared by doWebAPIAuth and silentTokenRefresh. * dynamic height * Fix UTF-8 encoding in OAuth callback page (✅ rendered as ✅) Add <meta charset="utf-8"> to both callback HTML pages so the checkmark emoji renders correctly in all browsers. * Fix dynamic playlist height: account for frame padding + 2-line controls Previous calculation used 12 + vis.Rows for fixed UI lines, but missed: - Frame padding (2 lines from Padding(1,3)) - Controls render as 2 lines (VOL + EQ), not 1 Corrected to 17 + vis.Rows. Playlist no longer overflows the terminal. * Fix playlist scroll: account for album separator lines in visible window Album separators (── Album Name (Year) ──) are rendered between tracks from different albums, taking extra lines. adjustScroll only counted track items, so the cursor would move past the visible area before scrolling kicked in. Now counts rendered lines (tracks + separators) to determine when to scroll. * Dynamic frame width: use full terminal width instead of fixed 80 chars The frame and panelWidth were hardcoded to 80/74 chars, causing the help bar to wrap to the next line on wider terminals. Now dynamically sizes to the terminal width on WindowSizeMsg. Album separators, seek bar, controls, and visualizer all scale to the available width. * Fix scroll with mixed album separators: count actual rendered lines Previous fix assumed every track had an album separator. Now uses renderedLineCount() helper that accurately counts lines (tracks + separators) for any range. adjustScroll walks backward from cursor to find the right scroll offset when mixed separator/no-separator tracks are present. * Measure actual UI height instead of counting lines manually The manual fixed-line count (17 + vis.Rows) was wrong — missed various multi-line renders, frame padding, etc. Now renders all non-playlist sections into a probe frame and uses lipgloss.Height() to measure the actual pixel height. Guarantees plVisible matches the real available space regardless of theme, controls layout, or status lines. * Fix x key toggle: properly toggle between compact (5) and dynamic max Previous code had dynMax == plVisible always (broken comparison). Now toggles between 5 and full dynamic height using same probe measurement as WindowSizeMsg. * Add 1-line buffer to dynamic playlist height Probe measurement was consistently 1-2 lines optimistic, causing the bottom of the playlist to clip. Add a 1-line safety margin. * Bump playlist height buffer to -2 lines * Fix playlist height: plVisible is rendered lines, not track count Root cause: renderPlaylist() did `visible := min(m.plVisible, len(tracks))` which conflated rendered line count with track count. When plVisible=15 and len(tracks)=8, visible was capped to 8 — but 8 tracks from different albums render as 16 lines (8 separators + 8 tracks), overflowing. Fix: - Remove min(plVisible, len(tracks)) — plVisible is the rendered line budget - Remove scroll clamping that used track count as line count - Add budget check before separator+track pair: if only 1 line left but need 2 (separator + track), break instead of overflowing - Remove -2 magic buffer from probe measurement — it's now exact - plVisible = height - lipgloss.Height(probeFrame) + 1 (no fudge factors) * Address PR review feedback from Gemini Code Assist - Use strconv.Atoi instead of fmt.Sscanf for year parsing - Add comment explaining Stream: false (pause/resume requires it) - Handle io.ReadAll error on non-OK HTTP response bodies - Log saveCreds errors instead of silently discarding - Log http.Serve errors (filter net.ErrClosed for clean shutdown) - Log country code fetch errors with stderr fallback - Add TODO for configurable bitrate * Add Spotify provider using go-librespot for native playback Integrates Spotify streaming directly into cliamp's Beep audio pipeline, giving full EQ, visualizer, and gapless playback support for Spotify Premium accounts. Architecture: - external/spotify/streamer.go: Bridges go-librespot AudioSource (interleaved float32) to beep.StreamSeekCloser ([2]float64 pairs) - external/spotify/session.go: OAuth2 authentication with credential persistence in ~/.config/cliamp/spotify_credentials.json - external/spotify/provider.go: playlist.Provider using Spotify Web API for playlists/tracks, go-librespot player.NewStream for audio - player/player.go: StreamerFactory hook for custom URI schemes - player/pipeline.go: spotify:track:xxx URI detection and routing - config/config.go: [spotify] section with enabled flag Enable with `enabled = true` under `[spotify]` in config.toml. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve Spotify OAuth flow: print auth URL, open browser, allow Enter to retry * Add retry with backoff on 429 rate limits, improve auth UX messaging * Add stderr logging for 429 retries, increase timeout to 5min, bump max retries to 8 * Bypass spclient for Web API calls — use direct HTTP with Bearer token to avoid Client-Token header rate limits * Own OAuth2 flow: capture Web API token, auto-close browser tab, use SpotifyTokenCredentials The spclient's internal login5 token gets aggressively rate-limited (429 with Retry-After: 86400) when used against api.spotify.com. Root cause: it's not a standard Web API token. Fix: Run our own OAuth2 flow with the same client_id/scopes, capture the access_token for Web API calls, and pass it to go-librespot via SpotifyTokenCredentials for session auth. Also: custom callback server serves HTML with window.close() script, fixing the browser tab not auto-closing after auth. * Use registered Spotify Developer app client_id for Web API The go-librespot internal client_id (65b708073fc0480ea92a077233ca87bd) is shared across all librespot users and gets aggressively rate-limited for Web API calls (Retry-After: 86400). Now requires a registered Spotify Developer app: - client_id in config.toml [spotify] section - Fixed callback port 19872 for redirect URI - OAuth2 PKCE flow (no client_secret needed) Config: [spotify] enabled = true client_id = "your-client-id" Spotify Developer app redirect URI: http://127.0.0.1:19872/login * Remove internal Spotify scopes that cause 'Illegal scope' with registered apps * Minimal OAuth2 scopes — only playlist-read + streaming + user-read-private * Fix TUI freeze: stop stdin reader goroutine after auth completes The bufio.Scanner goroutine for Enter-to-retry kept reading stdin after OAuth completed, stealing input from Bubbletea's raw terminal handler. Replaced with raw os.Stdin.Read + authDone channel to stop cleanly. * Fix stored credential sessions: do fresh OAuth2 for Web API token on each launch The spclient's login5 token gets 429'd on Web API. On second launch (stored credentials), we were trying refreshWebAPIToken which tested the spclient token — always fails. Now does a quick OAuth2 PKCE flow on each launch to get a fresh Web API token. Falls back to full interactive auth if the token flow fails. * Fix nil CountryCode panic when playing Spotify tracks go-librespot's Player.getUnrestrictedTrack dereferences CountryCode to check media restrictions. We never set it, causing a nil pointer panic. Default to 'US'. * Fix pause/resume restarting track; expand scopes; dynamic country code - Set Stream=false on Spotify tracks — they're seekable, not live streams. The TUI's togglePlayPause treats Stream=true as live (stop+replay on resume), which was restarting songs from the beginning. - Expanded OAuth2 scopes to full standard Web API set (playlist modify, library modify, playback state, recently played, top tracks, follows). Internal Spotify scopes that cause 'Illegal scope' are documented and excluded. - Fetch user's country from /v1/me for accurate media restriction checks instead of hardcoding 'US'. * Dynamic playlist height, persist OAuth2 refresh token - Playlist view now fills available terminal height instead of being capped at 5 items. Recalculates on window resize. 'x' key toggles between compact (5) and full height. Minimum 3 items. - Persist OAuth2 refresh token in spotify_credentials.json. On subsequent launches, silently refresh the Web API token without opening a browser. Falls back to interactive auth if refresh fails. - Extract spotifyOAuthConfig() helper shared by doWebAPIAuth and silentTokenRefresh. * dynamic height * Fix UTF-8 encoding in OAuth callback page (✅ rendered as ✅) Add <meta charset="utf-8"> to both callback HTML pages so the checkmark emoji renders correctly in all browsers. * Fix dynamic playlist height: account for frame padding + 2-line controls Previous calculation used 12 + vis.Rows for fixed UI lines, but missed: - Frame padding (2 lines from Padding(1,3)) - Controls render as 2 lines (VOL + EQ), not 1 Corrected to 17 + vis.Rows. Playlist no longer overflows the terminal. * Fix playlist scroll: account for album separator lines in visible window Album separators (── Album Name (Year) ──) are rendered between tracks from different albums, taking extra lines. adjustScroll only counted track items, so the cursor would move past the visible area before scrolling kicked in. Now counts rendered lines (tracks + separators) to determine when to scroll. * Dynamic frame width: use full terminal width instead of fixed 80 chars The frame and panelWidth were hardcoded to 80/74 chars, causing the help bar to wrap to the next line on wider terminals. Now dynamically sizes to the terminal width on WindowSizeMsg. Album separators, seek bar, controls, and visualizer all scale to the available width. * Fix scroll with mixed album separators: count actual rendered lines Previous fix assumed every track had an album separator. Now uses renderedLineCount() helper that accurately counts lines (tracks + separators) for any range. adjustScroll walks backward from cursor to find the right scroll offset when mixed separator/no-separator tracks are present. * Measure actual UI height instead of counting lines manually The manual fixed-line count (17 + vis.Rows) was wrong — missed various multi-line renders, frame padding, etc. Now renders all non-playlist sections into a probe frame and uses lipgloss.Height() to measure the actual pixel height. Guarantees plVisible matches the real available space regardless of theme, controls layout, or status lines. * Fix x key toggle: properly toggle between compact (5) and dynamic max Previous code had dynMax == plVisible always (broken comparison). Now toggles between 5 and full dynamic height using same probe measurement as WindowSizeMsg. * Add 1-line buffer to dynamic playlist height Probe measurement was consistently 1-2 lines optimistic, causing the bottom of the playlist to clip. Add a 1-line safety margin. * Bump playlist height buffer to -2 lines * Fix playlist height: plVisible is rendered lines, not track count Root cause: renderPlaylist() did `visible := min(m.plVisible, len(tracks))` which conflated rendered line count with track count. When plVisible=15 and len(tracks)=8, visible was capped to 8 — but 8 tracks from different albums render as 16 lines (8 separators + 8 tracks), overflowing. Fix: - Remove min(plVisible, len(tracks)) — plVisible is the rendered line budget - Remove scroll clamping that used track count as line count - Add budget check before separator+track pair: if only 1 line left but need 2 (separator + track), break instead of overflowing - Remove -2 magic buffer from probe measurement — it's now exact - plVisible = height - lipgloss.Height(probeFrame) + 1 (no fudge factors) * Address PR review feedback from Gemini Code Assist - Use strconv.Atoi instead of fmt.Sscanf for year parsing - Add comment explaining Stream: false (pause/resume requires it) - Handle io.ReadAll error on non-OK HTTP response bodies - Log saveCreds errors instead of silently discarding - Log http.Serve errors (filter net.ErrClosed for clean shutdown) - Log country code fetch errors with stderr fallback - Add TODO for configurable bitrate * Add Spotify docs to README, update install instructions for fork - Added Spotify section with setup instructions (Developer app, config, OAuth) - Updated install methods: go install, pre-built binaries, build from source - Removed Homebrew tap update from release workflow (fork-specific) * Auto-refresh Web API token: replace static string with TokenSource The webAPIToken was set once during init and never refreshed. Spotify access tokens expire after 1 hour, causing all Web API calls to fail. Now uses oauth2.TokenSource which automatically refreshes the token using the stored refresh token when it expires. No more browser re-authentication after 1 hour — tokens refresh transparently in the background. * Show shuffle (z) and repeat (r) keys in bottom help bar They were only visible in the Ctrl+K keymap overlay but not in the always-visible bottom controls line. * Persist shuffle/repeat state to config on toggle Saves shuffle and repeat preferences to config.toml when toggled via z/r keys, so they survive restarts across all providers. * Move shuffle/repeat to top of Ctrl+K keymap overlay They were at positions 23-24, below the 12-line visible window. Moved them right after volume controls so they're visible without scrolling. * Responsive help bar: drop low-priority hints when terminal is narrow Each help hint has a priority. When the combined width exceeds the terminal width, the lowest-priority hints are dropped first: 100 Spc(⏯) 95 Q(Quit) 90 <>(Trk) 80 +-(Vol) 70 ←→(Seek) 60 Ctrl+K(Keys) 50 Tab(Focus) 40 /(Search) 30 a(Queue) 20 z(Shfl) 20 r(Rpt) On a narrow terminal you still see play/pause, track nav, vol, and quit. On wide terminals everything shows. * Handle config.Save errors for shuffle/repeat Show a status message via saveMsg when persisting fails instead of silently ignoring the error. Addresses Gemini Code Assist review feedback. * fix: don't auto-play when selecting Spotify playlist during playback When a track is already playing, selecting a new playlist now loads the tracks without stopping playback or auto-playing. Users can browse playlists freely while listening. Auto-play only triggers when nothing is currently playing. * fix: auto re-auth on Spotify AES key / session errors When go-librespot fails to retrieve an audio key (e.g. code 2 from Spotify's AP server due to expired/revoked session), the provider now: 1. Detects auth-related errors (KeyProviderError, DeadlineExceeded) 2. Tears down the dead session and clears stored credentials 3. Triggers a fresh OAuth2 interactive flow automatically 4. Retries the stream once with the new session This prevents users from getting stuck in an error loop — no manual credential deletion or CLI commands needed. Adds Session.Reconnect() for hot-swapping the session/player, and deleteCreds() to clear stale stored credentials. * fix: avoid nil window in Reconnect (address review) Create the new session before tearing down the old one so s.sess and s.player are never nil while the mutex is unlocked. Old session/player are closed after the atomic swap completes. Addresses Gemini review comment on PR #4. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |