soulsync/core/metadata
BoulderBadgeDad 37c2b9b569 spotify: playlist-write client + single-source OAuth scope (#945, increment 2)
For exporting a mirrored playlist back to Spotify:

- The OAuth scope string was duplicated verbatim in 5 places (spotify_client, the per-profile
  registry, and 3 web_server callbacks) — a drift hazard where the authorize URL and token
  exchange could request different scopes and silently re-prompt/deny. Extracted ONE
  SPOTIFY_OAUTH_SCOPE constant and pointed all 5 at it, and added playlist-modify-public/private
  there. Existing users re-auth once to grant write; reads are unaffected.
- SpotifyClient.create_or_update_playlist(name, track_ids, existing_id=None): creates a playlist
  owned by the authed user, or replaces an existing one's tracks in place (idempotent re-export).
  Chunks at Spotify's 100-track cap. A pre-scope token gets a clear "reconnect Spotify" message
  instead of a raw 403. Returns {success, playlist_id, url, added, error}.

6 tests: create-new adds tracks, update replaces (no create), >100 chunking, empty → error (no API
calls), not-authed → error, insufficient-scope → reconnect message. 268 spotify/oauth tests green,
ruff clean. Additive — read paths and existing tokens unchanged.

Next: Deezer write via the ARL gw-light gateway, then the export-job branch + endpoint + modal.
2026-06-28 21:12:53 -07:00
..
__init__.py
album_mbid_cache.py
album_tracks.py
art_apply.py Cover Art Filler: write cover.jpg to the RESOLVED folder, not the raw DB path (Sokhi — the actual bug) 2026-06-08 18:25:50 -07:00
art_lookup.py
art_preservation.py
art_sources.py
artist_album_cache.py Cache artist album lists across metadata sources 2026-06-11 22:33:04 +02:00
artist_image.py
artist_resolution.py
artwork.py Special-edition cover art: prefer the pinned release own cover over the release-group representative 2026-06-21 22:47:09 -07:00
caa_art.py Special-edition cover art: prefer the pinned release own cover over the release-group representative 2026-06-21 22:47:09 -07:00
cache.py
canonical_resolver.py
canonical_version.py canonical: recognize musicbrainz as a readable album source (PR #929 follow-up) 2026-06-26 19:04:24 -07:00
common.py Import: atomic tag saves so an interrupted/OOM save can't destroy the file (#819) 2026-06-08 13:46:28 -07:00
completion.py
discography.py
discography_filters.py discography 'add to wishlist': batch the per-track ownership check (fixes ~15-30s/track) 2026-06-27 19:51:28 -07:00
enrichment.py Multi-disc albums: never write a disc-less track (floor disc to >=1) 2026-06-22 09:30:04 -07:00
lookup.py
lyrics.py
multi_source_search.py
registry.py spotify: playlist-write client + single-source OAuth scope (#945, increment 2) 2026-06-28 21:12:53 -07:00
release_dates.py
release_type.py
relevance.py spotify search endpoint: plain query, not field-scoped — fixes pool-fix "no results" 2026-06-28 19:48:04 -07:00
service.py
similar_artists.py
source.py Multi-disc: file the track in the disc folder that matches its tag (Sokhi) 2026-06-22 10:56:53 -07:00
status.py
track_number_format.py
types.py