From 77b8d7dd1f653349689109d2908d939735589a6e Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Tue, 2 Jun 2026 22:50:04 -0700 Subject: [PATCH] SpotipyFree integration confirmed working (236 tracks live); deps + meta tweak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Verified end-to-end: fetch_public_playlist_full pulled all 236 tracks of the test playlist via SpotipyFree (the library handles the client-auth that 429'd the raw approach). Name + tracks correct. - requirements.txt: declare spotipyFree>=1.1.2 as a normal pip dependency (like spotDL, also MIT — aggregation, not vendored) + websockets (a transitive dep SpotipyFree/spotapi needs that pip doesn't pull automatically). Code still soft-imports + falls back to embed, so it's never a hard runtime requirement. - meta fetch uses limit=1 (name/owner only) so we don't pull the whole list twice. 9 tests green. --- core/spotify_public_api.py | 4 +++- requirements.txt | 17 +++++++++-------- tests/test_spotify_public_api.py | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/spotify_public_api.py b/core/spotify_public_api.py index 49a7c504..d0dc49c1 100644 --- a/core/spotify_public_api.py +++ b/core/spotify_public_api.py @@ -77,7 +77,9 @@ def fetch_public_playlist_full( meta: Dict[str, Any] = {} try: - meta = client.playlist(spotify_id) or {} + # limit=1: we only want name/owner here — tracks come from the paginated + # playlist_items call below, so don't pull the whole list twice. + meta = client.playlist(spotify_id, limit=1) or {} except Exception as e: # metadata is nice-to-have; tracks are the point logger.debug("playlist metadata fetch failed (%s); continuing", e) name = meta.get('name', 'Unknown') diff --git a/requirements.txt b/requirements.txt index 49a405f6..148d3c0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,11 +66,12 @@ flask-socketio==5.6.1 gunicorn==26.0.0 simple-websocket==1.1.0 -# ── Optional: full public-playlist link imports (no Spotify credentials) ── -# The "Spotify link" tab scrapes Spotify's embed widget, which caps at ~100 -# tracks. SpotipyFree pulls the full list with no login. It is GPL-3.0, so it is -# intentionally NOT installed by default (SoulSync is MIT — bundling GPL would -# force the project to GPL). To enable full link imports, install it yourself: -# pip install SpotipyFree -# Without it, the link tab simply falls back to the ~100-track embed result. -# spotipyFree>=1.1.2,<2 +# Full public-playlist link imports (no Spotify credentials). The "Spotify link" +# tab scrapes Spotify's embed widget, which caps at ~100 tracks; SpotipyFree +# (the no-creds spotipy drop-in spotDL uses) pulls the full list. It is GPL-3.0, +# used here as a normal pip dependency — aggregation, exactly like spotDL (also +# MIT). It is NOT vendored into SoulSync's own code, so the project stays MIT. +# The code soft-imports it and falls back to the embed scraper (~100 tracks) if +# it's missing or fails, so this is never a hard runtime requirement. +spotipyFree>=1.1.2,<2 +websockets>=12 # required by SpotipyFree/spotapi, not pulled in automatically diff --git a/tests/test_spotify_public_api.py b/tests/test_spotify_public_api.py index 6629aabc..e4c37487 100644 --- a/tests/test_spotify_public_api.py +++ b/tests/test_spotify_public_api.py @@ -43,7 +43,7 @@ class _FakeClient: def __init__(self, total, *, fail_items=False): self.total, self.fail_items = total, fail_items - def playlist(self, pid): + def playlist(self, pid, limit=-1, offset=0, *args, **kwargs): return {'name': 'My Playlist', 'owner': {'display_name': 'Owner'}} def _page(self, offset):