// ========== SMART ARR CONNECT ========== (function() { injectModal('arr-setup-modal', `

🎬 Smart Arr Connect

Auto-discover and connect your entire media stack.

Scanning for services...
Where to find API keys:
Radarr/Sonarr/Prowlarr: Settings -> General -> Security -> API Key
`); const modal = document.getElementById('arr-setup-modal'); const openBtn = document.getElementById('arr-setup-btn'); const cancelBtn = document.getElementById('arr-setup-cancel'); const connectBtn = document.getElementById('smart-connect-btn'); // Phase elements const phaseDetect = document.getElementById('smart-phase-detect'); const phaseCredentials = document.getElementById('smart-phase-credentials'); const phaseProgress = document.getElementById('smart-phase-progress'); const phaseResults = document.getElementById('smart-phase-results'); const detectResults = document.getElementById('smart-detect-results'); const credentialInputs = document.getElementById('smart-credential-inputs'); const progressSteps = document.getElementById('smart-progress-steps'); const resultsContent = document.getElementById('smart-results-content'); const plexLibraries = document.getElementById('smart-plex-libraries'); const retryBtn = document.getElementById('smart-retry-btn'); let detectedData = null; // Store detection results for smart-connect const serviceIcons = { plex: '🎬', radarr: '🎬', sonarr: '📺', prowlarr: '🔍', seerr: '📋' }; const serviceLabels = { plex: 'Plex', radarr: 'Radarr (Movies)', sonarr: 'Sonarr (TV)', prowlarr: 'Prowlarr (Indexers)', seerr: 'Seerr' }; function showPhase(phase) { phaseDetect.style.display = phase === 'detect' ? 'block' : 'none'; phaseCredentials.style.display = phase === 'credentials' ? 'block' : 'none'; phaseProgress.style.display = phase === 'progress' ? 'block' : 'none'; phaseResults.style.display = phase === 'results' ? 'block' : 'none'; } function statusBadge(status) { const colors = { connected: { bg: 'var(--ok-fg)', icon: '✓', text: 'Connected' }, needs_key: { bg: '#f39c12', icon: '🔑', text: 'Needs API Key' }, not_found: { bg: 'var(--muted)', icon: '—', text: 'Not Found' }, error: { bg: 'var(--bad-fg)', icon: '✗', text: 'Error' } }; const c = colors[status] || colors.not_found; return `${c.icon} ${c.text}`; } // Phase 1: Smart Detection async function smartDetect() { showPhase('detect'); detectResults.style.display = 'none'; try { const response = await fetch('/api/v1/arr/smart-detect'); detectedData = await response.json(); if (!detectedData.success) { detectResults.innerHTML = `
Detection failed: ${escapeHtml(detectedData.error)}
`; detectResults.style.display = 'block'; return; } // Render detection results let html = '
'; for (const [svc, info] of Object.entries(detectedData.services)) { const icon = serviceIcons[svc] || '📦'; const label = serviceLabels[svc] || svc; const source = info.source ? `${escapeHtml(info.source)}` : ''; const version = info.version ? `v${escapeHtml(info.version)}` : ''; const keySaved = (info.hasApiKey || info.hasToken) && info.status === 'connected' ? 'Key saved' : ''; html += `
${icon}
${label}
${source} ${version} ${keySaved}
${statusBadge(info.status)}
`; } html += '
'; // Summary const s = detectedData.summary; html += `
${escapeHtml(String(s.fullyConnected))}/${escapeHtml(String(s.totalDetected + (5 - s.totalDetected)))} services detected · ${escapeHtml(String(s.fullyConnected))} connected${s.needsApiKey > 0 ? ` · ${escapeHtml(String(s.needsApiKey))} needs API key` : ''}
`; detectResults.innerHTML = html; detectResults.style.display = 'block'; // Build credential inputs for services that need keys buildCredentialInputs(detectedData); // Auto-advance after a short delay setTimeout(() => { showPhase('credentials'); }, 800); } catch (e) { detectResults.innerHTML = `
Error: ${escapeHtml(e.message)}
`; detectResults.style.display = 'block'; } } // Build credential input fields function buildCredentialInputs(data) { let html = ''; const services = data.services; const arrServices = ['radarr', 'sonarr', 'prowlarr']; for (const svc of arrServices) { const info = services[svc]; if (!info || info.status === 'not_found' && !info.url) continue; const icon = serviceIcons[svc]; const label = serviceLabels[svc]; const isConnected = info.status === 'connected'; const borderColor = isConnected ? 'var(--ok-fg)' : 'var(--border)'; html += `
${icon} ${label} ${isConnected ? '✓ Connected' : ''}
`; } // Plex status (non-editable, just shows status) const plex = services.plex; if (plex) { const plexConnected = plex.status === 'connected'; html += `
🎬 Plex ${statusBadge(plex.status)} ${escapeHtml(plex.source || '')}
`; } // Seerr status const seerr = services.seerr; if (seerr) { const seerrOk = seerr.status === 'connected'; let configuredHtml = ''; if (seerr.configuredServices) { const cs = seerr.configuredServices; configuredHtml = `
Configured: ${cs.radarr ? '✓ Radarr' : '✗ Radarr'} · ${cs.sonarr ? '✓ Sonarr' : '✗ Sonarr'} · ${cs.plex ? '✓ Plex' : '✗ Plex'}
`; } html += `
📋 Seerr ${statusBadge(seerr.status)}
${configuredHtml}
`; } credentialInputs.innerHTML = html; } // Test connection (global for onclick) window.smartTestConnection = async function(service) { const urlInput = document.getElementById(`smart-${service}-url`); const keyInput = document.getElementById(`smart-${service}-key`); const statusSpan = document.getElementById(`smart-${service}-status`); const url = urlInput?.value.trim(); const apiKey = keyInput?.value.trim(); if (!url || !apiKey) { statusSpan.innerHTML = 'Enter URL and API key'; return; } statusSpan.innerHTML = ''; try { const response = await secureFetch('/api/v1/arr/test-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service, url, apiKey }) }); const data = await response.json(); if (data.success) { statusSpan.innerHTML = `✓ ${escapeHtml(data.appName || 'Connected')} v${escapeHtml(data.version || '')}`; } else { statusSpan.innerHTML = `✗ ${escapeHtml(data.error)}`; } } catch (e) { statusSpan.innerHTML = `✗ ${escapeHtml(e.message)}`; } }; // Phase 3: Smart Connect async function smartConnect() { showPhase('progress'); progressSteps.innerHTML = '
Connecting services...
'; // Gather input const services = {}; for (const svc of ['radarr', 'sonarr', 'prowlarr']) { const url = document.getElementById(`smart-${svc}-url`)?.value.trim(); const apiKey = document.getElementById(`smart-${svc}-key`)?.value.trim(); if (apiKey && url) { services[svc] = { apiKey, url }; } else if (apiKey) { // Key provided without URL - let backend resolve services[svc] = { apiKey }; } // If no key entered but service was already connected, backend uses stored credentials } const payload = { services: Object.keys(services).length > 0 ? services : undefined, configurePlex: document.getElementById('smart-opt-plex')?.checked, configureProwlarr: document.getElementById('smart-opt-prowlarr')?.checked, configureSeerr: document.getElementById('smart-opt-seerr')?.checked, saveCredentials: document.getElementById('smart-opt-save')?.checked }; try { const response = await secureFetch('/api/v1/arr/smart-connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); // Render progress steps let stepsHtml = ''; for (const step of (data.steps || [])) { const icon = step.status === 'success' ? '' : ''; const detailColor = step.status === 'success' ? 'var(--muted)' : 'var(--bad-fg)'; stepsHtml += `
${icon} ${escapeHtml(step.step)} ${escapeHtml(step.details || '')}
`; } progressSteps.innerHTML = stepsHtml; // Show results after brief delay setTimeout(() => showResults(data), 500); } catch (e) { progressSteps.innerHTML = `
Connection error: ${escapeHtml(e.message)}
`; } } // Phase 4: Results function showResults(data) { showPhase('results'); const s = data.summary || {}; const allGood = s.failed === 0 && s.succeeded > 0; const headerColor = allGood ? 'var(--ok-fg)' : '#f39c12'; const headerIcon = allGood ? '✓' : '⚠'; const headerText = allGood ? 'All Connected!' : `${escapeHtml(String(s.succeeded))}/${escapeHtml(String(s.totalSteps))} Steps Succeeded`; let html = `
${headerIcon}
${headerText}
${escapeHtml(String(s.succeeded))} succeeded, ${escapeHtml(String(s.failed))} failed
`; // Steps detail html += '
'; for (const step of (data.steps || [])) { const icon = step.status === 'success' ? '' : ''; html += `
${icon} ${escapeHtml(step.step)} ${escapeHtml(step.details || '')}
`; } html += '
'; resultsContent.innerHTML = html; // Show retry button if any failures retryBtn.style.display = s.failed > 0 ? 'block' : 'none'; // Fetch Plex libraries if Plex was connected if (data.steps?.some(st => st.step.includes('Plex') && st.status === 'success')) { fetchPlexLibraries(); } } async function fetchPlexLibraries() { try { const res = await fetch('/api/v1/plex/libraries'); const data = await res.json(); if (data.success && data.libraries?.length > 0) { let html = `

🎬 ${escapeHtml(data.serverName)} Libraries

`; for (const lib of data.libraries) { const typeIcon = lib.type === 'movie' ? '🎬' : lib.type === 'show' ? '📺' : '🎵'; html += `
${typeIcon} ${escapeHtml(lib.title)} ${escapeHtml(String(lib.count))} items
`; } html += '
'; plexLibraries.innerHTML = html; plexLibraries.style.display = 'block'; } } catch (e) { // Ignore Plex library fetch errors } } // Event listeners openBtn?.addEventListener('click', () => { modal.classList.add('show'); plexLibraries.style.display = 'none'; smartDetect(); }); wireModal(modal, cancelBtn); connectBtn?.addEventListener('click', smartConnect); retryBtn?.addEventListener('click', smartConnect); })();