Skip to content

fix: Spotify API 2026 - playlist 403, genres/label KeyErrors, OAuth s…#2631

Open
kichelle-cc wants to merge 2 commits intospotDL:masterfrom
kichelle-cc:fix/spotify-api-2026
Open

fix: Spotify API 2026 - playlist 403, genres/label KeyErrors, OAuth s…#2631
kichelle-cc wants to merge 2 commits intospotDL:masterfrom
kichelle-cc:fix/spotify-api-2026

Conversation

@kichelle-cc
Copy link

Fix: 403 Forbidden on playlist tracks + auth issues (spotdl 4.4.3, Spotify API 2025/2026)

Environment: macOS, spotdl 4.4.3, spotipy, Python 3.12


Problem 1: GET /v1/playlists/{id}/tracks returns 403

Spotify's /tracks endpoint now returns 403 for apps in Development Mode, even with a valid user OAuth token. However, GET /v1/playlists/{id} still works and returns track data under items (newer response shape where each row uses item instead of track).

Fix — patch spotdl/types/playlist.py:

Replace the playlist_items() call with playlist() and read tracks from the embedded page:

market = None
try:
    profile = spotify_client.me()
    if profile:
        market = profile.get("country")
except Exception:
    pass

playlist = spotify_client.playlist(
    url, additional_types=("track",), market=market
)

page = playlist.get("tracks")
if page is None:
    page = playlist.get("items")

def _row_track(row):
    t = row.get("track")
    if t is None:
        item = row.get("item")
        if isinstance(item, dict) and item.get("type") == "track":
            t = item
    return {"track": t}

raw_tracks = [_row_track(r) for r in page.get("items", [])]
while page.get("next"):
    page = spotify_client.next(page)
    if page is None:
        break
    raw_tracks.extend(_row_track(r) for r in page.get("items", []))

Problem 2: genres, label, popularity KeyErrors during song enrichment

Spotify's album and artist API responses sometimes omit these fields for certain tracks, causing Song.from_url() to crash with a KeyError.

Fix — patch spotdl/types/song.py:

# before
genres=raw_album_meta["genres"] + raw_artist_meta["genres"],
publisher=raw_album_meta["label"],
popularity=raw_track_meta["popularity"],

# after
genres=(raw_album_meta.get("genres") or []) + (raw_artist_meta.get("genres") or []),
publisher=raw_album_meta.get("label") or "",
popularity=raw_track_meta.get("popularity", 0),

Problem 3: OAuth scopes insufficient + hardcoded redirect URI

The default scopes (user-library-read, user-follow-read) don't include playlist scopes. The redirect URI is hardcoded to http://127.0.0.1:9900/ which may not work in all environments.

Fix — patch spotdl/utils/spotify.py:

import os  # add to imports

# in the user_auth block:
redirect_uri = os.getenv("SPOTIPY_REDIRECT_URI", "http://127.0.0.1:9900/")
credential_manager = SpotifyOAuth(
    client_id=client_id,
    client_secret=client_secret,
    redirect_uri=redirect_uri,
    scope=(
        "user-library-read,user-follow-read,"
        "playlist-read-private,playlist-read-collaborative,"
        "user-read-private"
    ),
    cache_handler=cache_handler,
    open_browser=not headless,
)

If you can't use the loopback server (port 9900 blocked, running headless, etc.), set SPOTIPY_REDIRECT_URI to any HTTPS URL registered on your Spotify app, then use --headless and paste the full redirect URL when prompted.


Problem 4: yt-dlp too old — Precondition check failed / Signature extraction failed

Downloads stuck at 0% because yt-dlp 2023.x can no longer decrypt YouTube streaming URLs. spotdl 4.4.3 pins yt-dlp<2024.0.0 in its requirements but the pin needs to be overridden.

Fix:

pip install -U yt-dlp  # ignore the version conflict warning from spotdl

Spotify API rate limits

Development Mode apps hit a daily quota (~600–800 calls) since spotdl calls track(), album(), and artist() per song (~3 calls each). On large playlists (100+ tracks) you will get:

Your application has reached a rate/request limit. Retry will occur after: 84333 s

This is a ~24h rolling window reset. Mitigations:

  • Use --threads 1 to reduce burst rate
  • Use --archive /path/to/archive.txt (or set it in ~/.spotdl/config.json) so re-runs skip already-downloaded tracks entirely and avoid redundant API calls

Hope this saves someone a few hours!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants