Merge pull request #342 from elmerohueso/plex-pin-auth
Add Plex pin-based OAuth and re-work Plex configuration dialogs
This commit is contained in:
commit
87ea38bdd9
4 changed files with 423 additions and 21 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -23,5 +23,7 @@ logs/*.log.*
|
|||
# Auto-downloaded binaries
|
||||
bin/
|
||||
|
||||
# Development compose/config files
|
||||
*.dev.yml
|
||||
# Any hidden folders
|
||||
**/.*/
|
||||
|
|
|
|||
130
web_server.py
130
web_server.py
|
|
@ -81,6 +81,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
|
||||
|
|
@ -189,6 +190,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():
|
||||
|
|
@ -7275,6 +7280,131 @@ 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/clear-library', methods=['POST'])
|
||||
def clear_plex_library_preference():
|
||||
try:
|
||||
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}")
|
||||
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"""
|
||||
|
|
|
|||
|
|
@ -4318,26 +4318,56 @@
|
|||
|
||||
<!-- Plex Settings -->
|
||||
<div class="server-config-container" id="plex-container">
|
||||
<div class="form-group">
|
||||
<label>Plex Server URL:</label>
|
||||
<input type="url" id="plex-url" placeholder="http://localhost:32400">
|
||||
<div id="plex-configuration">
|
||||
<div class="form-group">
|
||||
<label>Plex Server URL:</label>
|
||||
<input type="url" id="plex-url" placeholder="http://localhost:32400">
|
||||
<div class="form-actions" id="plex-url-actions" style="justify-content:flex-end; width: 100%; flex-basis: 100%; margin-top: 8px;">
|
||||
<button id="plex-autodetect-button" class="detect-button" onclick="autoDetectPlex()">Auto-detect</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Plex Token:</label>
|
||||
<input type="password" id="plex-token" placeholder="X-Plex-Token">
|
||||
<div class="form-actions" id="plex-token-actions" style="justify-content:flex-end; width: 100%; flex-basis: 100%; margin-top: 8px;">
|
||||
<button id="plex-connect-button" class="test-button" onclick="testConnection('plex')">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="plex-library-selector-container" style="display: none;">
|
||||
<label>Music Library:</label>
|
||||
<select id="plex-music-library" onchange="selectPlexLibrary()">
|
||||
<option value="">Loading...</option>
|
||||
</select>
|
||||
<small style="color: #999; font-size: 0.9em; display: block; margin-top: 5px;">
|
||||
Select which music library to use (doesn't affect config file)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button id="plex-config-action-button" class="detect-button" onclick="clearPlexConfiguration()">Clear Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Plex Token:</label>
|
||||
<input type="password" id="plex-token" placeholder="X-Plex-Token">
|
||||
</div>
|
||||
<div class="form-group" id="plex-library-selector-container" style="display: none;">
|
||||
<label>Music Library:</label>
|
||||
<select id="plex-music-library" onchange="selectPlexLibrary()">
|
||||
<option value="">Loading...</option>
|
||||
</select>
|
||||
<small style="color: #999; font-size: 0.9em; display: block; margin-top: 5px;">
|
||||
Select which music library to use (doesn't affect config file)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="detect-button" onclick="autoDetectPlex()">Auto-detect</button>
|
||||
<button class="test-button" onclick="testConnection('plex')">Test</button>
|
||||
<div id="plex-setup">
|
||||
<div id="plex-setup-buttons" class="form-actions">
|
||||
<button id="plex-link-to-plex-button" class="detect-button" onclick="startPlexPinAuth()">Link to Plex (OAuth)</button>
|
||||
<button id="plex-manual-config-button" class="detect-button" onclick="showPlexConfiguration(false, true)">Manually Configure Plex</button>
|
||||
<button id="plex-view-config-button" class="detect-button view-config-button" onclick="showPlexConfiguration()">View Configuration</button>
|
||||
</div>
|
||||
<div id="plex-pin-auth-flow" style="display: none; padding: 16px; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; margin-top: 12px;">
|
||||
<div class="form-group">
|
||||
<label>Plex PIN Code</label>
|
||||
<div id="plex-pin-code" style="font-size: 1.4em; font-weight: 700; margin-top: 8px;"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div id="plex-pin-instructions" class="setting-help-text" style="margin-bottom: 8px;">
|
||||
Go to <strong>https://plex.tv/link</strong> and enter the code shown above.
|
||||
</div>
|
||||
<div id="plex-pin-status" class="setting-help-text" style="color: #ccc;"></div>
|
||||
</div>
|
||||
<div class="form-actions" style="gap: 10px;">
|
||||
<button class="detect-button" id="plex-pin-refresh-button" onclick="restartPlexPinAuth()">Generate New Code</button>
|
||||
<button class="test-button" onclick="cancelPlexPinAuth()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5991,6 +5991,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);
|
||||
|
|
@ -6028,8 +6047,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 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 || '';
|
||||
|
|
@ -6423,6 +6447,212 @@ function updateMediaServerFields() {
|
|||
}
|
||||
}
|
||||
|
||||
let _plexPinAuthRequestId = null;
|
||||
let _plexPinAuthPollInterval = null;
|
||||
|
||||
function showPlexConfiguration(disableFields = false, isManualConfig = 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');
|
||||
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 && isManualConfig) {
|
||||
plexLibraryContainer.style.display = 'none';
|
||||
}
|
||||
setPlexConfigActionButton(isManualConfig);
|
||||
updatePlexConfigurationButtons();
|
||||
}
|
||||
|
||||
function showPlexSetup() {
|
||||
const plexConfig = document.getElementById('plex-configuration');
|
||||
const plexSetup = document.getElementById('plex-setup');
|
||||
const plexPinAuthFlow = document.getElementById('plex-pin-auth-flow');
|
||||
const plexLibraryContainer = document.getElementById('plex-library-selector-container');
|
||||
|
||||
if (plexConfig) plexConfig.style.display = 'none';
|
||||
if (plexSetup) plexSetup.style.display = '';
|
||||
if (plexPinAuthFlow) plexPinAuthFlow.style.display = 'none';
|
||||
if (plexLibraryContainer) plexLibraryContainer.style.display = 'none';
|
||||
setPlexConfigActionButton(false);
|
||||
}
|
||||
|
||||
function setPlexConfigActionButton(isManualConfig) {
|
||||
const actionButton = document.getElementById('plex-config-action-button');
|
||||
if (!actionButton) return;
|
||||
|
||||
if (isManualConfig) {
|
||||
actionButton.textContent = 'Cancel';
|
||||
actionButton.onclick = showPlexSetup;
|
||||
actionButton.title = 'Cancel manual Plex configuration';
|
||||
} else {
|
||||
actionButton.textContent = 'Clear Configuration';
|
||||
actionButton.onclick = clearPlexConfiguration;
|
||||
actionButton.title = 'Clear saved Plex configuration';
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
await saveSettings(true);
|
||||
}
|
||||
showToast('Plex successfully linked', 'success');
|
||||
showPlexConfiguration(true);
|
||||
await testConnection('plex');
|
||||
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();
|
||||
}
|
||||
|
||||
async 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 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 (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 = '<option value="">Select a music library</option>';
|
||||
}
|
||||
if (plexLibraryContainer) {
|
||||
plexLibraryContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
updatePlexConfigurationButtons();
|
||||
|
||||
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);
|
||||
}
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Plex configuration cleared', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleServer(serverType) {
|
||||
// Update toggle buttons
|
||||
document.getElementById('plex-toggle').classList.remove('active');
|
||||
|
|
@ -6437,6 +6667,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();
|
||||
|
|
@ -19790,6 +20026,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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue