Fix: auto-sync capped public Spotify playlists at 100 tracks (#838)

Regression in 2.6.9. The spotify_public source adapter (used by auto-sync /
refresh_mirrored) called scrape_spotify_embed() directly — the embed widget only
exposes ~100 tracks — instead of fetch_spotify_public(), the wrapper the rest of
the app uses, which pulls the full list via the paginated public API and only
falls back to the embed on failure. So initial discovery got the whole playlist
but every auto-sync re-fetch truncated it to 100.

Switched the adapter to fetch_spotify_public (same return shape — drop-in). Albums
still resolve via the embed (already whole); on any failure it falls back to the
embed exactly as before.

Test: the adapter returns all 150 tracks when the full fetch yields 150 (was
capped at 100). 29 adapter tests pass.
This commit is contained in:
BoulderBadgeDad 2026-06-10 18:31:43 -07:00
parent 56bdcee434
commit a15fe15842
2 changed files with 38 additions and 2 deletions

View file

@ -35,14 +35,19 @@ class SpotifyPublicPlaylistSource(PlaylistSource):
"""``playlist_id`` is a Spotify URL or ``open.spotify.com`` URI."""
from core.spotify_public_scraper import (
parse_spotify_url,
scrape_spotify_embed,
fetch_spotify_public,
)
parsed = parse_spotify_url(playlist_id)
if not parsed:
return None
data = scrape_spotify_embed(parsed["type"], parsed["id"])
# #838: use the full-fetch wrapper (paginates past the embed widget's
# ~100-track cap), NOT scrape_spotify_embed directly. The rest of the app
# already uses fetch_spotify_public; the adapter calling the capped embed
# scraper is why auto-sync truncated >100-track playlists to 100 while the
# initial discovery got the whole thing. Same return shape, so drop-in.
data = fetch_spotify_public(parsed["type"], parsed["id"])
if not isinstance(data, dict) or data.get("error"):
return None

View file

@ -915,3 +915,34 @@ def test_mirror_dict_spotify_public_emits_spotify_hint():
extra = _json.loads(d["extra_data"])
assert extra["discovered"] is False
assert extra["spotify_hint"]["id"] == "sptrk"
def test_spotify_public_adapter_paginates_past_100(monkeypatch):
"""#838: auto-sync truncated >100-track playlists to 100 because the adapter
called the embed scraper (100) directly instead of the full-fetch wrapper.
With the wrapper, a playlist whose full fetch returns 150 tracks keeps all 150."""
src = SpotifyPublicPlaylistSource()
monkeypatch.setattr(
"core.spotify_public_scraper.parse_spotify_url",
lambda url: {"type": "playlist", "id": "big"},
)
full = {
"id": "big", "type": "playlist", "name": "Big PL", "subtitle": "owner",
"url": "https://open.spotify.com/playlist/big", "url_hash": "bighash",
"tracks": [
{"id": f"t{i}", "name": f"Song {i}", "artists": [{"name": "A"}],
"duration_ms": 1000, "is_explicit": False, "track_number": i + 1}
for i in range(150)
],
}
# The full paginated path succeeds → wrapper returns all 150 (no embed cap).
monkeypatch.setattr(
"core.spotify_public_api.fetch_public_playlist_full",
lambda spotify_id: full,
)
detail = src.get_playlist("https://open.spotify.com/playlist/big")
assert detail is not None
assert len(detail.tracks) == 150, "pre-#838 the adapter capped this at 100"
assert detail.meta.track_count == 150