0.6.1: Fix Spotify OAuth scope missing user-library-read

connect.py's OAuth scope request (playlist-read-private
playlist-read-collaborative) didn't match what sldl's own built-in login
flow requests (same two, plus user-library-read). The narrower scope alembic
granted let sldl refresh a valid access token, but Spotify then returned 403
Forbidden loading the playlist regardless of ownership, which sldl turns
into an unhandled-exception crash (exit 134) instead of a clean scope error.
Found by reproducing a friend's crash: his playlist was his own, connected
via his own OAuth, correct redirect URI -- ruling out the ownership/
visibility explanation and pointing at the scope mismatch instead.

Also:
- run-playlist.sh: detect exit 134 specifically and log a pointed hint
  (distinguishing the Forbidden-loading-playlist case from any other
  unhandled sldl exception) instead of just "sldl finished with exit code
  134" with no context.
- README: document that anyone who connected Spotify before this version
  needs to click Connect Spotify again -- existing refresh tokens carry the
  old, narrower scope and don't upgrade themselves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
andrew
2026-07-15 08:07:30 -06:00
parent 29d6249e88
commit 31af52cc2f
3 changed files with 31 additions and 1 deletions
+10
View File
@@ -213,6 +213,8 @@ That's it, done once. alembic stores the resulting OAuth refresh token (encrypte
If you happen to be running a Spotify app created *before* the 2025/2026 change, plain Client ID/Secret still works and this step is optional — but connecting is harmless either way, so there's no reason not to. If you happen to be running a Spotify app created *before* the 2025/2026 change, plain Client ID/Secret still works and this step is optional — but connecting is harmless either way, so there's no reason not to.
> **If you connected Spotify before version 0.6.1:** click **Connect Spotify** again. Versions before 0.6.1 requested a narrower OAuth scope than sldl actually needs, which could get you a token that refreshes fine but then gets 403 Forbidden loading a playlist (see Troubleshooting). Reconnecting grants the correct scope; your old connection doesn't upgrade itself.
## Adding your first playlist ## Adding your first playlist
1. Go to **Playlists****Add playlist**. 1. Go to **Playlists****Add playlist**.
@@ -288,6 +290,14 @@ Your Spotify app can't read the playlist with app-only (client-credentials) acce
The fix: go to **Settings → Credentials** and click **Connect Spotify** to complete the OAuth login (see "Connecting your Spotify account (OAuth)" above). This mints a user token that works regardless of when your app was created. If you haven't already, add the redirect URI `https://<your-host>/connect/spotify/callback` in your Spotify app's dashboard first, or the connect step itself will fail with a redirect_uri mismatch. The fix: go to **Settings → Credentials** and click **Connect Spotify** to complete the OAuth login (see "Connecting your Spotify account (OAuth)" above). This mints a user token that works regardless of when your app was created. If you haven't already, add the redirect URI `https://<your-host>/connect/spotify/callback` in your Spotify app's dashboard first, or the connect step itself will fail with a redirect_uri mismatch.
**A playlist's job log shows `sldl crashed (exit 134)` mentioning a 403 Forbidden, even though you've connected Spotify.**
Two possible causes, in order of likelihood:
1. **You connected before version 0.6.1.** Earlier versions requested a narrower OAuth scope than sldl actually needs, so the token refreshes fine but then gets 403 loading a playlist regardless of ownership. Fix: go to **Settings → Credentials** and click **Connect Spotify** again to grant the correct scope.
2. **The connected account genuinely can't see this playlist.** Once connected, playlist reads are scoped to what that account can actually see — its own playlists, ones it collaborates on, or ones marked Public — not any arbitrary playlist by ID the way app-only access could. Fix: make sure the playlist is owned by the connected account, or set it to Public on Spotify (Playlist → ⋯ → Make public).
Either way, re-run the playlist after fixing it.
**Dedup found something that isn't actually a duplicate.** **Dedup found something that isn't actually a duplicate.**
Use the "Keep both" button on that pair. alembic will remember your decision and won't flag that exact pair again. Use the "Keep both" button on that pair. alembic will remember your decision and won't flag that exact pair again.
+7 -1
View File
@@ -25,7 +25,13 @@ router = APIRouter(tags=["spotify-connect"])
AUTHORIZE_URL = "https://accounts.spotify.com/authorize" AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
TOKEN_URL = "https://accounts.spotify.com/api/token" TOKEN_URL = "https://accounts.spotify.com/api/token"
SCOPES = "playlist-read-private playlist-read-collaborative" # Matches exactly what sldl's own built-in OAuth login flow requests (seen in
# its printed authorize URL: user-library-read + these two) -- granting a
# narrower scope than sldl expects gets a token sldl can refresh fine but
# then gets 403 Forbidden from Spotify partway through loading a playlist,
# which sldl turns into an unhandled-exception crash (exit 134) rather than a
# clean scope error.
SCOPES = "playlist-read-private playlist-read-collaborative user-library-read"
def _callback_redirect_uri(request: Request) -> str: def _callback_redirect_uri(request: Request) -> str:
+14
View File
@@ -79,6 +79,20 @@ timeout --kill-after=30s "$SLDL_TIMEOUT" \
|| SLDL_EXIT=$? || SLDL_EXIT=$?
if [[ "$SLDL_EXIT" -eq 124 || "$SLDL_EXIT" -eq 137 ]]; then if [[ "$SLDL_EXIT" -eq 124 || "$SLDL_EXIT" -eq 137 ]]; then
log "ERROR: sldl exceeded ${SLDL_TIMEOUT}s and was killed (likely a hang)" log "ERROR: sldl exceeded ${SLDL_TIMEOUT}s and was killed (likely a hang)"
elif [[ "$SLDL_EXIT" -eq 134 ]]; then
# sldl aborts (SIGABRT) on ANY unhandled .NET exception rather than failing
# cleanly. The one case we've actually seen in the wild: a valid, freshly-
# refreshed user token still gets 403 Forbidden loading the playlist,
# because Spotify scopes OAuth playlist reads to what the CONNECTED account
# can see (its own playlists, ones it collaborates on, or ones marked
# Public) -- unlike client-credentials, which could read any public
# playlist regardless of whose app it was. Give a pointed hint when that's
# the visible cause; otherwise just flag that sldl crashed outright.
if grep -q "APIException: Forbidden" "$LOG"; then
log "ERROR: sldl crashed (exit 134) -- Spotify returned 403 loading this playlist. The connected Spotify account can't see it: make sure it's owned by, or shared/followed by, whichever account is connected via /connect/spotify, or set it to Public on Spotify."
else
log "ERROR: sldl crashed (exit 134, unhandled exception) -- see the log above for the exception detail"
fi
fi fi
log "sldl finished with exit code $SLDL_EXIT" log "sldl finished with exit code $SLDL_EXIT"