// ========== SMART ARR CONNECT ==========
(function() {
injectModal('arr-setup-modal', `
🎬 Smart Arr Connect
Auto-discover and connect your entire media stack.
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 = '';
// 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);
})();