From 783839349c0dd24da80cf8b4da22763e23de51eb Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 10 Jun 2026 15:45:45 -0700 Subject: [PATCH] Automations: per-profile playlist source reads in auto-sync (part 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The playlist source registry built the Spotify/Tidal adapters with a client GETTER (resolved fresh on every read), but web_server passed `lambda: `. Swapped those to get_spotify_client_for_profile / get_tidal_client_for_profile. Combined with part 1 (the engine running each automation as its owner), an auto-sync pipeline now reads its source playlist through the OWNER's account: - interactive sync → the user's session profile, - background automation → the automation owner (via core.profile_context), - admin / profile 1 → the global client, so the admin's existing auto-sync pipelines pull exactly as before. The adapters re-resolve per read, so a singleton registry is fine. Deezer/Qobuz getters left global (their playlist login is tangled with downloads — deferred). Tests: the Spotify/Tidal source adapters resolve the global client under admin and re-resolve through the profile context per call (unconnected → safe global fallback). 27 endpoint/profile tests pass. --- tests/test_credentials_endpoints.py | 26 ++++++++++++++++++++++++++ web_server.py | 10 ++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/test_credentials_endpoints.py b/tests/test_credentials_endpoints.py index 9faa037a..592b3973 100644 --- a/tests/test_credentials_endpoints.py +++ b/tests/test_credentials_endpoints.py @@ -315,3 +315,29 @@ def test_real_session_still_wins_over_background(client, nonadmin_profile): assert body['is_admin'] is False # it's the non-admin session, not 999/admin finally: reset_background_profile(tok) + + +# ── Part 2: the playlist SOURCE adapters read per-profile (sync handlers) ────── +# bootstrap now passes get_*_client_for_profile as the source adapters' +# client_getter; these prove that composition resolves per the current profile +# context and stays on the global client for admin (the existing pipelines). + +def test_spotify_source_adapter_resolves_per_profile(): + from core.playlists.sources.spotify import SpotifyPlaylistSource + src = SpotifyPlaylistSource(web_server.get_spotify_client_for_profile) + # admin / no override -> the global client (admin pipelines unchanged) + assert src._client() is web_server.spotify_client + # a background owner override flows through the resolver, re-resolved per + # call (unconnected profile -> safe global fallback, never a frozen client) + from core.profile_context import set_background_profile, reset_background_profile + tok = set_background_profile(424242) + try: + assert src._client() is web_server.spotify_client + finally: + reset_background_profile(tok) + + +def test_tidal_source_adapter_resolves_per_profile(): + from core.playlists.sources.tidal import TidalPlaylistSource + src = TidalPlaylistSource(web_server.get_tidal_client_for_profile) + assert src._client() is web_server.tidal_client # admin -> global, unchanged diff --git a/web_server.py b/web_server.py index 344d77da..54c47f33 100644 --- a/web_server.py +++ b/web_server.py @@ -1138,8 +1138,14 @@ def _register_automation_handlers(): return match_mb_tracks(tracks, _mb_match_deps) _playlist_source_registry = build_playlist_source_registry( - spotify_client_getter=lambda: spotify_client, - tidal_client_getter=lambda: tidal_client, + # Per-profile source reads: the adapter calls these fresh on every read, + # so they resolve the CURRENT profile's account (their session + # interactively, their automation's owner in the background — via + # core.profile_context). Admin/profile 1 → the global client, so the + # admin's existing auto-sync pipelines are unchanged. (Deezer/Qobuz stay + # global for now — their playlist login is tangled with downloads.) + spotify_client_getter=get_spotify_client_for_profile, + tidal_client_getter=get_tidal_client_for_profile, qobuz_client_getter=_get_qobuz_client_for_sync, deezer_client_getter=_get_deezer_client, youtube_parser=parse_youtube_playlist,