From 919cd1a2b29b7e3901c0066893541874ce856e18 Mon Sep 17 00:00:00 2001 From: elmerohueso Date: Mon, 20 Apr 2026 20:02:41 -0600 Subject: [PATCH 1/7] update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a4e0f72a..59d00f35 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ logs/*.log.* # Auto-downloaded binaries bin/ + +# Development compose/config files +*.dev.yml From 6344f250fce9ff227dc9b9c700373fdcab8284f4 Mon Sep 17 00:00:00 2001 From: elmerohueso Date: Mon, 20 Apr 2026 20:54:10 -0600 Subject: [PATCH 2/7] buttons to configure plex or view the current configuration --- plex_pin_auth.py | 104 +++++++++++++++++++++++++++++++++++++++++ webui/index.html | 47 +++++++++++-------- webui/static/script.js | 45 ++++++++++++++++++ 3 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 plex_pin_auth.py diff --git a/plex_pin_auth.py b/plex_pin_auth.py new file mode 100644 index 00000000..cd52b96a --- /dev/null +++ b/plex_pin_auth.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Test script to authenticate with Plex using PIN-based login. +This mimics the app's /api/plex/pin/start and /api/plex/pin/status flows. +""" + +import sys +import time +import traceback +from plexapi.myplex import MyPlexAccount, MyPlexPinLogin + + +def main(): + print('Starting Plex PIN login flow...') + try: + pinlogin = MyPlexPinLogin(oauth=False) + except Exception as exc: + print(f'Failed to create MyPlexPinLogin: {exc}', file=sys.stderr) + traceback.print_exc() + return 1 + + pin = getattr(pinlogin, 'pin', None) + if not pin: + print('Failed to obtain PIN from Plex', file=sys.stderr) + return 1 + + print(f'\nPlease go to the following URL in your browser and enter the PIN: {pin}') + print(f' https://plex.tv/link') + print('Then return here and wait for authentication to complete.') + + while True: + if getattr(pinlogin, 'expired', False): + print('\nPIN has expired. Please rerun this script to start again.', file=sys.stderr) + return 2 + + try: + if pinlogin.checkLogin(): + token = getattr(pinlogin, 'token', None) + if not token: + print('Login succeeded but no token was returned.', file=sys.stderr) + return 3 + + print('\nLogin complete!') + print(f'Plex token: {token}') + + try: + account = MyPlexAccount(token=token) + except Exception as exc: + print(f'Failed to create MyPlexAccount: {exc}', file=sys.stderr) + traceback.print_exc() + return 4 + + print(f'Authenticated Plex username: {getattr(account, "username", None)}') + print(f'Authenticated Plex title: {getattr(account, "title", None)}') + + print('\nFetching available Plex resources...') + try: + resources = account.resources() + except Exception as exc: + print(f'Failed to fetch resources: {exc}', file=sys.stderr) + traceback.print_exc() + return 5 + + server_resources = [r for r in resources if 'server' in (getattr(r, 'provides', '') or '')] + if server_resources: + print(f'Found {len(server_resources)} Plex server resource(s):') + for idx, resource in enumerate(server_resources, 1): + print(f'[{idx}] {getattr(resource, "name", None)} ({getattr(resource, "product", None)})') + connections = getattr(resource, 'connections', []) or [] + for conn in connections: + uri = getattr(conn, 'uri', None) + local = getattr(conn, 'local', False) + relay = getattr(conn, 'relay', False) + print(f' - {uri} {'(LOCAL)' if local else ''}{' (RELAY)' if relay else ''}') + + local_conn = None + for resource in server_resources: + for conn in getattr(resource, 'connections', []) or []: + if getattr(conn, 'local', False): + local_conn = conn + break + if local_conn: + break + + if local_conn: + print(f'\nSelected local server URI: {getattr(local_conn, "uri", None)}') + else: + print('\nNo local server connection found. Use one of the above URIs manually.') + else: + print('No Plex server resources were found for this account.') + + return 0 + + print('PIN not yet authorized. Polling again in 5 seconds...') + except Exception as exc: + print(f'Error checking PIN login status: {exc}', file=sys.stderr) + traceback.print_exc() + return 6 + + time.sleep(5) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/webui/index.html b/webui/index.html index f1c63959..6c5a279b 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4318,26 +4318,35 @@
-
- - +
+
+ + +
+
+ + +
+ +
+ + + +
-
- - -
- -
- - +
+
+ + +
diff --git a/webui/static/script.js b/webui/static/script.js index 8b7cdc96..4035b1b4 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -6026,6 +6026,15 @@ async function loadSettingsData() { // Populate Plex settings document.getElementById('plex-url').value = settings.plex?.base_url || ''; document.getElementById('plex-token').value = settings.plex?.token || ''; + const hasPlexConfig = Boolean(settings.plex?.base_url || settings.plex?.token); + const plexViewConfigButton = document.getElementById('plex-view-config-button'); + const plexConfigureButton = document.getElementById('plex-configure-button'); + if (plexViewConfigButton) { + plexViewConfigButton.style.display = hasPlexConfig ? '' : 'none'; + } + if (plexConfigureButton) { + plexConfigureButton.style.display = hasPlexConfig ? 'none' : ''; + } // Populate Jellyfin settings document.getElementById('jellyfin-url').value = settings.jellyfin?.base_url || ''; @@ -6416,6 +6425,36 @@ function updateMediaServerFields() { } } +function showPlexConfiguration() { + const plexConfig = document.getElementById('plex-configuration'); + const plexSetup = document.getElementById('plex-setup'); + if (plexConfig) plexConfig.style.display = ''; + if (plexSetup) plexSetup.style.display = 'none'; +} + +function clearPlexConfiguration() { + const plexUrl = document.getElementById('plex-url'); + const plexToken = document.getElementById('plex-token'); + const plexConfig = document.getElementById('plex-configuration'); + const plexSetup = document.getElementById('plex-setup'); + const plexViewConfigButton = document.getElementById('plex-view-config-button'); + const plexConfigureButton = document.getElementById('plex-configure-button'); + + if (plexUrl) plexUrl.value = ''; + if (plexToken) plexToken.value = ''; + if (plexConfig) plexConfig.style.display = 'none'; + if (plexSetup) plexSetup.style.display = ''; + if (plexViewConfigButton) plexViewConfigButton.style.display = 'none'; + if (plexConfigureButton) plexConfigureButton.style.display = ''; + + if (typeof saveSettings === 'function') { + saveSettings(true); + } + if (typeof showToast === 'function') { + showToast('Plex configuration cleared', 'success'); + } +} + function toggleServer(serverType) { // Update toggle buttons document.getElementById('plex-toggle').classList.remove('active'); @@ -6430,6 +6469,12 @@ function toggleServer(serverType) { document.getElementById('navidrome-container').classList.toggle('hidden', serverType !== 'navidrome'); document.getElementById('soulsync-container')?.classList.toggle('hidden', serverType !== 'soulsync'); + // Show Plex setup when Plex is selected; otherwise hide both Plex panels + const plexConfig = document.getElementById('plex-configuration'); + const plexSetup = document.getElementById('plex-setup'); + if (plexConfig) plexConfig.style.display = serverType === 'plex' ? 'none' : ''; + if (plexSetup) plexSetup.style.display = serverType === 'plex' ? '' : 'none'; + // Load Plex music libraries when switching to Plex if (serverType === 'plex') { loadPlexMusicLibraries(); From 6a27e7930cc1f155239b7c993eb1bbbfac963ca4 Mon Sep 17 00:00:00 2001 From: elmerohueso Date: Mon, 20 Apr 2026 21:22:44 -0600 Subject: [PATCH 3/7] plex oauth via pin/code --- web_server.py | 116 +++++++++++++++++++++++++++++++++ webui/index.html | 21 +++++- webui/static/script.js | 141 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 271 insertions(+), 7 deletions(-) diff --git a/web_server.py b/web_server.py index d888fda3..36139198 100644 --- a/web_server.py +++ b/web_server.py @@ -64,6 +64,7 @@ if not pp_logger.handlers: pp_logger.propagate = False from core.spotify_client import SpotifyClient, Playlist as SpotifyPlaylist, Track as SpotifyTrack, _is_globally_rate_limited as _spotify_rate_limited from core.plex_client import PlexClient +from plexapi.myplex import MyPlexAccount, MyPlexPinLogin from core.jellyfin_client import JellyfinClient from core.navidrome_client import NavidromeClient from core.soulseek_client import SoulseekClient @@ -175,6 +176,10 @@ app.secret_key = _init_flask_secret_key() # --- WebSocket (Socket.IO) Setup --- socketio = SocketIO(app, async_mode='threading', cors_allowed_origins='*') +# Plex PIN auth requests stored in memory for polling +_plex_pin_requests = {} +_plex_pin_requests_lock = threading.Lock() + # --- Profile Context (before_request hook) --- @app.before_request def _set_profile_context(): @@ -7261,6 +7266,117 @@ def detect_media_server_endpoint(): add_activity_item("", "Auto-Detect Failed", f"No {server_type} server found", "Now") return jsonify({"success": False, "error": f"No {server_type} server found on common local addresses."}) +@app.route('/api/plex/pin/start', methods=['POST']) +def start_plex_pin_auth(): + try: + pinlogin = MyPlexPinLogin(oauth=False) + except Exception as e: + logger.error(f'Failed to start Plex PIN auth: {e}') + return jsonify({"success": False, "error": str(e)}), 500 + + pin_code = getattr(pinlogin, 'pin', None) + if not pin_code: + return jsonify({"success": False, "error": 'Failed to generate Plex PIN code.'}), 500 + + request_id = str(uuid.uuid4()) + with _plex_pin_requests_lock: + _plex_pin_requests[request_id] = { + 'pinlogin': pinlogin, + 'created_at': time.time(), + 'expires_at': getattr(pinlogin, 'expires_at', None) + } + + expires_in = None + expires_at = getattr(pinlogin, 'expires_at', None) + if expires_at: + try: + expires_in = int((expires_at - datetime.now(timezone.utc)).total_seconds()) + except Exception: + expires_in = None + + return jsonify({ + "success": True, + "request_id": request_id, + "code": str(pin_code), + "auth_url": "https://plex.tv/link", + "expires_in": expires_in + }) + + +@app.route('/api/plex/pin/status', methods=['GET']) +def get_plex_pin_status(): + request_id = request.args.get('request_id') + if not request_id: + return jsonify({"success": False, "error": 'request_id is required'}), 400 + + with _plex_pin_requests_lock: + entry = _plex_pin_requests.get(request_id) + + if not entry: + return jsonify({"success": False, "error": 'Invalid or expired PIN request id.'}), 400 + + pinlogin = entry.get('pinlogin') + if not pinlogin: + return jsonify({"success": False, "error": 'Invalid PIN login state.'}), 500 + + try: + if getattr(pinlogin, 'expired', False): + with _plex_pin_requests_lock: + _plex_pin_requests.pop(request_id, None) + return jsonify({"success": False, "expired": True, "error": 'PIN code expired.'}) + + if pinlogin.checkLogin(): + token = getattr(pinlogin, 'token', None) + if not token: + raise ValueError('Plex token was not returned after authorization.') + + try: + account = MyPlexAccount(token=token) + resources = account.resources() + except Exception as e: + logger.error(f'Failed to fetch Plex account resources: {e}') + return jsonify({"success": False, "error": f'Plex authorization succeeded but failed to resolve server resources: {e}'}), 500 + + server_resources = [r for r in resources if 'server' in (getattr(r, 'provides', '') or '').lower()] + if not server_resources: + return jsonify({"success": False, "error": 'No Plex server resources found for this account.'}), 500 + + local_conn = None + relay_conn = None + for resource in server_resources: + for conn in getattr(resource, 'connections', []) or []: + if getattr(conn, 'local', False): + local_conn = conn + break + if getattr(conn, 'relay', False) and relay_conn is None: + relay_conn = conn + if local_conn: + break + + chosen_conn = local_conn or relay_conn + if not chosen_conn: + chosen_conn = getattr(server_resources[0], 'connections', [None])[0] + + found_url = getattr(chosen_conn, 'uri', None) if chosen_conn else None + with _plex_pin_requests_lock: + _plex_pin_requests.pop(request_id, None) + + if not found_url: + return jsonify({"success": False, "error": 'Plex authorized, but no usable server connection URI was found.'}), 500 + + return jsonify({ + "success": True, + "found_url": found_url, + "token": token, + "status": 'Plex authorization complete.' + }) + + return jsonify({"success": False, "status": 'Waiting for Plex authorization. Enter the PIN on plex.tv/link.'}) + except Exception as e: + logger.error(f'Error checking Plex PIN status: {e}') + return jsonify({"success": False, "error": str(e)}), 500 + + @app.route('/api/plex/music-libraries', methods=['GET']) def get_plex_music_libraries(): """Get list of all available music libraries from Plex""" diff --git a/webui/index.html b/webui/index.html index 6c5a279b..8287bc5d 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4343,10 +4343,27 @@
-
- +
+ +
+
diff --git a/webui/static/script.js b/webui/static/script.js index 4035b1b4..64a96155 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -6028,12 +6028,20 @@ async function loadSettingsData() { document.getElementById('plex-token').value = settings.plex?.token || ''; const hasPlexConfig = Boolean(settings.plex?.base_url || settings.plex?.token); const plexViewConfigButton = document.getElementById('plex-view-config-button'); - const plexConfigureButton = document.getElementById('plex-configure-button'); + const plexLinkToPlexButton = document.getElementById('plex-link-to-plex-button'); + const plexManualConfigButton = document.getElementById('plex-manual-config-button'); + const plexPinAuthFlow = document.getElementById('plex-pin-auth-flow'); if (plexViewConfigButton) { plexViewConfigButton.style.display = hasPlexConfig ? '' : 'none'; } - if (plexConfigureButton) { - plexConfigureButton.style.display = hasPlexConfig ? 'none' : ''; + if (plexLinkToPlexButton) { + plexLinkToPlexButton.style.display = hasPlexConfig ? 'none' : ''; + } + if (plexManualConfigButton) { + plexManualConfigButton.style.display = hasPlexConfig ? 'none' : ''; + } + if (plexPinAuthFlow) { + plexPinAuthFlow.style.display = 'none'; } // Populate Jellyfin settings @@ -6425,27 +6433,146 @@ function updateMediaServerFields() { } } +let _plexPinAuthRequestId = null; +let _plexPinAuthPollInterval = null; + function showPlexConfiguration() { + stopPlexPinAuthPolling(); const plexConfig = document.getElementById('plex-configuration'); const plexSetup = document.getElementById('plex-setup'); + const plexPinAuthFlow = document.getElementById('plex-pin-auth-flow'); if (plexConfig) plexConfig.style.display = ''; if (plexSetup) plexSetup.style.display = 'none'; + if (plexPinAuthFlow) plexPinAuthFlow.style.display = 'none'; +} + +async function startPlexPinAuth() { + const setupButtons = document.getElementById('plex-setup-buttons'); + const authFlow = document.getElementById('plex-pin-auth-flow'); + const statusEl = document.getElementById('plex-pin-status'); + if (setupButtons) setupButtons.style.display = 'none'; + if (authFlow) authFlow.style.display = ''; + if (statusEl) statusEl.textContent = 'Starting Plex authorization...'; + + try { + showLoadingOverlay('Starting Plex authorization...'); + const response = await fetch('/api/plex/pin/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + const result = await response.json(); + if (!result.success) { + throw new Error(result.error || 'Failed to start Plex PIN flow'); + } + + _plexPinAuthRequestId = result.request_id; + const pinCodeEl = document.getElementById('plex-pin-code'); + if (pinCodeEl) pinCodeEl.textContent = result.code || ''; + if (statusEl) { + statusEl.textContent = result.expires_in + ? `Enter this code at plex.tv/link. Code expires in ${result.expires_in} seconds.` + : 'Enter this code at plex.tv/link. Waiting for authorization...'; + } + + startPlexPinAuthPolling(); + } catch (error) { + console.error('Plex PIN auth start failed:', error); + showToast(error.message || 'Failed to start Plex authorization', 'error'); + cancelPlexPinAuth(); + } finally { + hideLoadingOverlay(); + } +} + +function startPlexPinAuthPolling() { + stopPlexPinAuthPolling(); + if (!_plexPinAuthRequestId) return; + _plexPinAuthPollInterval = setInterval(pollPlexPinAuthStatus, 5000); + pollPlexPinAuthStatus(); +} + +function stopPlexPinAuthPolling() { + if (_plexPinAuthPollInterval) { + clearInterval(_plexPinAuthPollInterval); + _plexPinAuthPollInterval = null; + } +} + +async function pollPlexPinAuthStatus() { + if (!_plexPinAuthRequestId) return; + try { + const response = await fetch(`/api/plex/pin/status?request_id=${encodeURIComponent(_plexPinAuthRequestId)}`); + const result = await response.json(); + const statusEl = document.getElementById('plex-pin-status'); + + if (!result.success && result.expired) { + if (statusEl) statusEl.textContent = 'PIN code expired. Generate a new code to continue.'; + stopPlexPinAuthPolling(); + return; + } + + if (result.success) { + stopPlexPinAuthPolling(); + if (statusEl) statusEl.textContent = 'Authorization complete! Saving Plex configuration...'; + document.getElementById('plex-url').value = result.found_url || ''; + document.getElementById('plex-token').value = result.token || ''; + if (typeof saveSettings === 'function') { + saveSettings(true); + } + showToast('Plex successfully linked', 'success'); + showPlexConfiguration(); + return; + } + + if (result.status) { + if (statusEl) statusEl.textContent = result.status; + return; + } + + if (result.error) { + if (statusEl) statusEl.textContent = result.error; + return; + } + } catch (error) { + console.error('Error polling Plex PIN status:', error); + const statusEl = document.getElementById('plex-pin-status'); + if (statusEl) statusEl.textContent = 'Unable to contact Plex auth status. Retrying...'; + } +} + +function cancelPlexPinAuth() { + stopPlexPinAuthPolling(); + _plexPinAuthRequestId = null; + const setupButtons = document.getElementById('plex-setup-buttons'); + const authFlow = document.getElementById('plex-pin-auth-flow'); + if (setupButtons) setupButtons.style.display = ''; + if (authFlow) authFlow.style.display = 'none'; +} + +function restartPlexPinAuth() { + cancelPlexPinAuth(); + startPlexPinAuth(); } function clearPlexConfiguration() { + cancelPlexPinAuth(); const plexUrl = document.getElementById('plex-url'); const plexToken = document.getElementById('plex-token'); const plexConfig = document.getElementById('plex-configuration'); const plexSetup = document.getElementById('plex-setup'); + const plexSetupButtons = document.getElementById('plex-setup-buttons'); const plexViewConfigButton = document.getElementById('plex-view-config-button'); - const plexConfigureButton = document.getElementById('plex-configure-button'); + const plexLinkToPlexButton = document.getElementById('plex-link-to-plex-button'); + const plexManualConfigButton = document.getElementById('plex-manual-config-button'); if (plexUrl) plexUrl.value = ''; if (plexToken) plexToken.value = ''; if (plexConfig) plexConfig.style.display = 'none'; if (plexSetup) plexSetup.style.display = ''; + if (plexSetupButtons) plexSetupButtons.style.display = ''; if (plexViewConfigButton) plexViewConfigButton.style.display = 'none'; - if (plexConfigureButton) plexConfigureButton.style.display = ''; + if (plexLinkToPlexButton) plexLinkToPlexButton.style.display = ''; + if (plexManualConfigButton) plexManualConfigButton.style.display = ''; if (typeof saveSettings === 'function') { saveSettings(true); @@ -19805,6 +19932,10 @@ window.testConnection = testConnection; window.autoDetectPlex = autoDetectPlex; window.autoDetectJellyfin = autoDetectJellyfin; window.autoDetectSlskd = autoDetectSlskd; +window.startPlexPinAuth = startPlexPinAuth; +window.cancelPlexPinAuth = cancelPlexPinAuth; +window.restartPlexPinAuth = restartPlexPinAuth; +window.showPlexConfiguration = showPlexConfiguration; window.toggleServer = toggleServer; window.authenticateSpotify = authenticateSpotify; window.authenticateTidal = authenticateTidal; From d36f18f5d7e27bc3d40de06cdc603d66ab27bb0e Mon Sep 17 00:00:00 2001 From: elmerohueso Date: Mon, 20 Apr 2026 21:34:29 -0600 Subject: [PATCH 4/7] pin auth should disable fields and show library --- web_server.py | 12 ++++++++++++ webui/static/script.js | 18 +++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/web_server.py b/web_server.py index 36139198..083af906 100644 --- a/web_server.py +++ b/web_server.py @@ -7377,6 +7377,18 @@ def get_plex_pin_status(): return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/api/plex/clear-library', methods=['POST']) +def clear_plex_library_preference(): + try: + from database.music_database import MusicDatabase + db = MusicDatabase() + db.set_preference('plex_music_library', '') + return jsonify({"success": True, "message": "Plex library preference cleared."}) + except Exception as e: + logger.error(f"Error clearing Plex library preference: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + @app.route('/api/plex/music-libraries', methods=['GET']) def get_plex_music_libraries(): """Get list of all available music libraries from Plex""" diff --git a/webui/static/script.js b/webui/static/script.js index 64a96155..ef1d6463 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -6436,14 +6436,19 @@ function updateMediaServerFields() { let _plexPinAuthRequestId = null; let _plexPinAuthPollInterval = null; -function showPlexConfiguration() { +function showPlexConfiguration(disableFields = false) { stopPlexPinAuthPolling(); const plexConfig = document.getElementById('plex-configuration'); const plexSetup = document.getElementById('plex-setup'); const plexPinAuthFlow = document.getElementById('plex-pin-auth-flow'); + const plexUrl = document.getElementById('plex-url'); + const plexToken = document.getElementById('plex-token'); + if (plexConfig) plexConfig.style.display = ''; if (plexSetup) plexSetup.style.display = 'none'; if (plexPinAuthFlow) plexPinAuthFlow.style.display = 'none'; + if (plexUrl) plexUrl.disabled = disableFields; + if (plexToken) plexToken.disabled = disableFields; } async function startPlexPinAuth() { @@ -6517,10 +6522,11 @@ async function pollPlexPinAuthStatus() { document.getElementById('plex-url').value = result.found_url || ''; document.getElementById('plex-token').value = result.token || ''; if (typeof saveSettings === 'function') { - saveSettings(true); + await saveSettings(true); } showToast('Plex successfully linked', 'success'); - showPlexConfiguration(); + showPlexConfiguration(true); + await testConnection('plex'); return; } @@ -6574,6 +6580,12 @@ function clearPlexConfiguration() { if (plexLinkToPlexButton) plexLinkToPlexButton.style.display = ''; if (plexManualConfigButton) plexManualConfigButton.style.display = ''; + try { + await fetch('/api/plex/clear-library', { method: 'POST' }); + } catch (e) { + console.warn('Failed to clear Plex library preference:', e); + } + if (typeof saveSettings === 'function') { saveSettings(true); } From 3e90de7a47acb2aebef21962ba6083c53772a8aa Mon Sep 17 00:00:00 2001 From: elmerohueso Date: Mon, 20 Apr 2026 21:57:21 -0600 Subject: [PATCH 5/7] clean up buttons for manually configuring plex --- web_server.py | 2 ++ webui/index.html | 8 ++++-- webui/static/script.js | 63 ++++++++++++++++++++++++++++-------------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/web_server.py b/web_server.py index 083af906..117c74c6 100644 --- a/web_server.py +++ b/web_server.py @@ -7383,6 +7383,8 @@ def clear_plex_library_preference(): from database.music_database import MusicDatabase db = MusicDatabase() db.set_preference('plex_music_library', '') + if plex_client: + plex_client.music_library = None return jsonify({"success": True, "message": "Plex library preference cleared."}) except Exception as e: logger.error(f"Error clearing Plex library preference: {e}") diff --git a/webui/index.html b/webui/index.html index 8287bc5d..d60dc5f5 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4322,10 +4322,16 @@
+
+ +
+
+ +
- -
diff --git a/webui/static/script.js b/webui/static/script.js index ef1d6463..685f888f 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -5987,6 +5987,25 @@ function updateLossyBitrateOptions() { } } +function updatePlexConfigurationButtons() { + const plexUrl = document.getElementById('plex-url'); + const plexToken = document.getElementById('plex-token'); + const hasPlexConfig = Boolean((plexUrl?.value || '').trim() || (plexToken?.value || '').trim()); + const plexViewConfigButton = document.getElementById('plex-view-config-button'); + const plexLinkToPlexButton = document.getElementById('plex-link-to-plex-button'); + const plexManualConfigButton = document.getElementById('plex-manual-config-button'); + const plexUrlActions = document.getElementById('plex-url-actions'); + const plexTokenActions = document.getElementById('plex-token-actions'); + const plexPinAuthFlow = document.getElementById('plex-pin-auth-flow'); + + if (plexViewConfigButton) plexViewConfigButton.style.display = hasPlexConfig ? '' : 'none'; + if (plexLinkToPlexButton) plexLinkToPlexButton.style.display = hasPlexConfig ? 'none' : ''; + if (plexManualConfigButton) plexManualConfigButton.style.display = hasPlexConfig ? 'none' : ''; + if (plexUrlActions) plexUrlActions.style.display = hasPlexConfig ? 'none' : 'flex'; + if (plexTokenActions) plexTokenActions.style.display = hasPlexConfig ? 'none' : 'flex'; + if (plexPinAuthFlow) plexPinAuthFlow.style.display = 'none'; +} + async function loadSettingsData() { try { const response = await fetch(API.settings); @@ -6024,25 +6043,13 @@ async function loadSettingsData() { }); // Populate Plex settings - document.getElementById('plex-url').value = settings.plex?.base_url || ''; - document.getElementById('plex-token').value = settings.plex?.token || ''; - const hasPlexConfig = Boolean(settings.plex?.base_url || settings.plex?.token); - const plexViewConfigButton = document.getElementById('plex-view-config-button'); - const plexLinkToPlexButton = document.getElementById('plex-link-to-plex-button'); - const plexManualConfigButton = document.getElementById('plex-manual-config-button'); - const plexPinAuthFlow = document.getElementById('plex-pin-auth-flow'); - if (plexViewConfigButton) { - plexViewConfigButton.style.display = hasPlexConfig ? '' : 'none'; - } - if (plexLinkToPlexButton) { - plexLinkToPlexButton.style.display = hasPlexConfig ? 'none' : ''; - } - if (plexManualConfigButton) { - plexManualConfigButton.style.display = hasPlexConfig ? 'none' : ''; - } - if (plexPinAuthFlow) { - plexPinAuthFlow.style.display = 'none'; - } + const plexUrlInput = document.getElementById('plex-url'); + const plexTokenInput = document.getElementById('plex-token'); + if (plexUrlInput) plexUrlInput.value = settings.plex?.base_url || ''; + if (plexTokenInput) plexTokenInput.value = settings.plex?.token || ''; + if (plexUrlInput) plexUrlInput.addEventListener('input', updatePlexConfigurationButtons); + if (plexTokenInput) plexTokenInput.addEventListener('input', updatePlexConfigurationButtons); + updatePlexConfigurationButtons(); // Populate Jellyfin settings document.getElementById('jellyfin-url').value = settings.jellyfin?.base_url || ''; @@ -6443,12 +6450,17 @@ function showPlexConfiguration(disableFields = false) { const plexPinAuthFlow = document.getElementById('plex-pin-auth-flow'); const plexUrl = document.getElementById('plex-url'); const plexToken = document.getElementById('plex-token'); + const plexLibraryContainer = document.getElementById('plex-library-selector-container'); if (plexConfig) plexConfig.style.display = ''; if (plexSetup) plexSetup.style.display = 'none'; if (plexPinAuthFlow) plexPinAuthFlow.style.display = 'none'; if (plexUrl) plexUrl.disabled = disableFields; if (plexToken) plexToken.disabled = disableFields; + if (plexLibraryContainer && !disableFields) { + plexLibraryContainer.style.display = 'none'; + } + updatePlexConfigurationButtons(); } async function startPlexPinAuth() { @@ -6560,7 +6572,7 @@ function restartPlexPinAuth() { startPlexPinAuth(); } -function clearPlexConfiguration() { +async function clearPlexConfiguration() { cancelPlexPinAuth(); const plexUrl = document.getElementById('plex-url'); const plexToken = document.getElementById('plex-token'); @@ -6580,6 +6592,17 @@ function clearPlexConfiguration() { if (plexLinkToPlexButton) plexLinkToPlexButton.style.display = ''; if (plexManualConfigButton) plexManualConfigButton.style.display = ''; + const plexLibraryContainer = document.getElementById('plex-library-selector-container'); + const plexLibrarySelect = document.getElementById('plex-music-library'); + if (plexLibrarySelect) { + plexLibrarySelect.innerHTML = ''; + } + if (plexLibraryContainer) { + plexLibraryContainer.style.display = 'none'; + } + + updatePlexConfigurationButtons(); + try { await fetch('/api/plex/clear-library', { method: 'POST' }); } catch (e) { From e3dd5727d8133f87e0b3e60bbe786a452bdc834a Mon Sep 17 00:00:00 2001 From: elmerohueso Date: Mon, 20 Apr 2026 22:00:18 -0600 Subject: [PATCH 6/7] Cancel button when manually configuring plex --- webui/index.html | 4 ++-- webui/static/script.js | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/webui/index.html b/webui/index.html index d60dc5f5..e8d14fc7 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4343,13 +4343,13 @@
- +
- +