From 53477768cbdf076365f82ecaafb5fd0a50840b48 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:08:20 -0700 Subject: [PATCH] Complete per-profile service credentials feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Tidal per-profile OAuth with token storage on profile row. Auth initiation stores profile_id in PKCE state, callback detects it and stores encrypted tokens per-profile instead of globally. Personal settings modal now shows Spotify, Tidal, Server Library, and ListenBrainz sections for non-admin profiles. Admin sees only ListenBrainz (unchanged). Server library selection wired into playlist sync via _apply_profile_library. Full per-profile support: Spotify (credentials + OAuth + playlists), Tidal (OAuth + token storage), server library (Plex/Jellyfin/Navidrome), ListenBrainz (existing). All backwards compatible — upgrading users see zero change. --- web_server.py | 36 +++++++++++++++++++++++++++++++++--- webui/static/script.js | 28 +++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/web_server.py b/web_server.py index a6c74ab4..81c67b72 100644 --- a/web_server.py +++ b/web_server.py @@ -5958,6 +5958,11 @@ def auth_tidal(): print(f"🔐 Stored PKCE - verifier: {temp_tidal_client.code_verifier[:20]}... challenge: {temp_tidal_client.code_challenge[:20]}...") + # Store profile_id for per-profile auth + profile_id = request.args.get('profile_id', '') + with tidal_oauth_lock: + tidal_oauth_state["profile_id"] = profile_id if profile_id and profile_id != '1' else None + # Create OAuth URL import urllib.parse params = { @@ -6216,17 +6221,42 @@ def tidal_callback(): # Create a temporary client for the token exchange temp_tidal_client = TidalClient() - # Restore PKCE values and redirect_uri from the auth request + # Restore PKCE values, redirect_uri, and profile_id from the auth request + profile_id_for_tidal = None with tidal_oauth_lock: temp_tidal_client.code_verifier = tidal_oauth_state["code_verifier"] temp_tidal_client.code_challenge = tidal_oauth_state["code_challenge"] if "redirect_uri" in tidal_oauth_state: temp_tidal_client.redirect_uri = tidal_oauth_state["redirect_uri"] + profile_id_for_tidal = tidal_oauth_state.get("profile_id") success = temp_tidal_client.fetch_token_from_code(auth_code) - + if success: - # Re-initialize the main global tidal_client instance with the new token + # Per-profile: store tokens on profile, don't touch global client + if profile_id_for_tidal: + try: + profile_id_int = int(profile_id_for_tidal) + db = get_database() + # Store Tidal tokens on the profile + from config.settings import config_manager as _cm + enc_access = _cm._encrypt_value(temp_tidal_client.access_token) if temp_tidal_client.access_token else None + enc_refresh = _cm._encrypt_value(temp_tidal_client.refresh_token) if temp_tidal_client.refresh_token else None + with db._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + UPDATE profiles + SET tidal_access_token = ?, tidal_refresh_token = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """, (enc_access, enc_refresh, profile_id_int)) + conn.commit() + add_activity_item("✅", "Tidal Auth Complete", f"Profile {profile_id_int} authenticated with Tidal", "Now") + return "
Your personal Tidal account is now connected. You can close this window.
" + except Exception as profile_err: + print(f"⚠️ Per-profile Tidal auth failed, falling back to global: {profile_err}") + + # Global: Re-initialize the main global tidal_client instance with the new token tidal_client = TidalClient() if tidal_enrichment_worker: tidal_enrichment_worker.client = tidal_client diff --git a/webui/static/script.js b/webui/static/script.js index 58aafcfa..a08b209d 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1253,9 +1253,10 @@ async function openPersonalSettings() { // Build sections body.innerHTML = ''; - // Spotify + server library per-profile only shown for non-admin profiles + // Spotify + Tidal + server library per-profile only shown for non-admin profiles if (currentProfile && !currentProfile.is_admin) { renderPersonalSettingsSpotify(body, spotifyData); + renderPersonalSettingsTidal(body); try { const libRes = await fetch('/api/profiles/me/server-library'); const libData = await libRes.json(); @@ -1376,6 +1377,31 @@ async function authenticatePersonalSpotify() { window.open('/auth/spotify?profile_id=' + (currentProfile?.id || ''), '_blank'); } +function renderPersonalSettingsTidal(body) { + const section = document.createElement('div'); + section.id = 'ps-tidal-section'; + section.innerHTML = ` +