Update Discord badge in README.md#4
Merged
Merged
Conversation
duysqubix
approved these changes
Oct 7, 2023
gjermundgaraba
pushed a commit
to gjermundgaraba/beep
that referenced
this pull request
Apr 11, 2026
* 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 gopxl#4. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
gjermundgaraba
pushed a commit
to gjermundgaraba/beep
that referenced
this pull request
Apr 11, 2026
* 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 gopxl#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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.