// ===== SERVICE CREDENTIALS ===== (function() { injectModal('folder-browser-modal', `

📂 Browse for Media Folders

/
Loading...
`); injectModal('service-creds-modal', `

Service Credentials

Credentials are injected automatically when accessing this service.

No credentials stored
`); const modal = document.getElementById('service-creds-modal'); let currentService = null; const arrServices = ['sonarr', 'radarr', 'prowlarr', 'overseerr']; const qualityProfileServices = ['sonarr', 'radarr']; function getServiceUrl(service) { return service.externalUrl || service.url || ''; } function showError(msg) { const el = document.getElementById('svc-creds-error'); el.textContent = msg; el.style.display = ''; } function hideError() { const el = document.getElementById('svc-creds-error'); el.textContent = ''; el.style.display = 'none'; } window.openServiceCredsModal = async function(service) { currentService = service; hideError(); const title = document.getElementById('svc-creds-title'); const desc = document.getElementById('svc-creds-desc'); const seedhostSection = document.getElementById('svc-creds-seedhost'); const apikeySection = document.getElementById('svc-creds-apikey'); const basicSection = document.getElementById('svc-creds-basic'); const qualitySection = document.getElementById('svc-creds-quality'); title.textContent = service.name + ' Credentials'; // Determine which sections to show const isExt = !!service.isExternal; const isArr = arrServices.includes(service.id) || arrServices.includes(service.appTemplate); const hasQuality = qualityProfileServices.includes(service.id) || qualityProfileServices.includes(service.appTemplate); seedhostSection.style.display = isExt ? '' : 'none'; apikeySection.style.display = isArr ? '' : 'none'; qualitySection.style.display = hasQuality ? '' : 'none'; basicSection.style.display = !isExt ? '' : 'none'; // Reset quality dropdown const qualSelect = document.getElementById('svc-quality-select'); qualSelect.innerHTML = ''; document.getElementById('svc-quality-status').textContent = ''; if (isExt) { desc.textContent = 'Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.'; // Update password placeholder with service name document.getElementById('svc-seedhost-pass').placeholder = `Password for ${service.name}`; } else if (isArr) { desc.textContent = 'API key bypasses the app login screen automatically.'; } else { desc.textContent = 'Credentials are injected automatically when accessing this service.'; } // Load existing credentials await loadServiceCreds(service); modal.classList.add('show'); }; async function loadServiceCreds(service) { const dot = document.getElementById('svc-creds-dot'); const status = document.getElementById('svc-creds-status'); const clearBtn = document.getElementById('svc-creds-clear'); let hasCreds = false; // Load seedhost creds (shared username + per-service password) if (service.isExternal) { try { const res = await fetch(`/api/v1/seedhost-creds?serviceId=${service.id}`); const data = await res.json(); if (data.success) { document.getElementById('svc-seedhost-user').value = data.username || ''; if (data.hasCredentials) hasCreds = true; } else { document.getElementById('svc-seedhost-user').value = ''; } } catch (e) { /* ignore */ } document.getElementById('svc-seedhost-pass').value = ''; } // Load per-service creds try { const res = await fetch(`/api/v1/services/${service.id}/credentials`); const data = await res.json(); if (data.success) { if (data.hasApiKey) { document.getElementById('svc-apikey-input').value = '••••••••'; hasCreds = true; } else { document.getElementById('svc-apikey-input').value = ''; } if (data.hasBasicAuth && !service.isExternal) { document.getElementById('svc-basic-user').value = data.username || ''; hasCreds = true; } else { document.getElementById('svc-basic-user').value = ''; } } } catch (e) { /* ignore */ } if (document.getElementById('svc-basic-pass')) document.getElementById('svc-basic-pass').value = ''; // Load quality profile for arr services const svcId = service.id || service.appTemplate; if (qualityProfileServices.includes(svcId)) { await loadQualityProfiles(service); } if (hasCreds) { dot.style.background = 'var(--ok-fg, #74dfc4)'; status.style.color = 'var(--ok-fg, #74dfc4)'; status.textContent = 'Credentials stored'; clearBtn.style.display = ''; // Update the card button const btn = document.getElementById(`creds-btn-${service.id}`); if (btn) btn.classList.add('has-creds'); } else { dot.style.background = 'var(--muted)'; status.style.color = 'var(--muted)'; status.textContent = 'No credentials stored'; clearBtn.style.display = 'none'; } } // Fetch and populate quality profiles dropdown async function loadQualityProfiles(service) { const qualSelect = document.getElementById('svc-quality-select'); const qualStatus = document.getElementById('svc-quality-status'); const svcId = service.id || service.appTemplate; const svcUrl = getServiceUrl(service); if (!svcUrl) { qualSelect.innerHTML = ''; return; } qualSelect.innerHTML = ''; qualStatus.textContent = ''; try { const params = new URLSearchParams({ service: svcId, url: svcUrl }); const res = await fetch(`/api/v1/arr/quality-profiles?${params}`); const data = await res.json(); if (!data.success || !data.profiles?.length) { qualSelect.innerHTML = ''; return; } qualSelect.innerHTML = ''; for (const p of data.profiles) { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = p.name; qualSelect.appendChild(opt); } // Pre-select stored profile or best match for "720" if (data.storedProfileId) { qualSelect.value = String(data.storedProfileId); } if (!qualSelect.value) { // Try to find a 720p-ish profile const match720 = data.profiles.find(p => /720/i.test(p.name)); if (match720) qualSelect.value = String(match720.id); } if (!qualSelect.value && data.profiles.length) { qualSelect.value = String(data.profiles[0].id); } qualStatus.innerHTML = `${data.profiles.length} profiles loaded`; } catch (e) { qualSelect.innerHTML = ''; qualStatus.innerHTML = `Error: ${e.message}`; } } // Fetch button for quality profiles document.getElementById('svc-quality-fetch')?.addEventListener('click', async () => { if (!currentService) return; const svcId = currentService.id || currentService.appTemplate; const svcUrl = getServiceUrl(currentService); const apiKeyInput = document.getElementById('svc-apikey-input'); const apiKey = apiKeyInput?.value.trim(); const qualSelect = document.getElementById('svc-quality-select'); const qualStatus = document.getElementById('svc-quality-status'); if (!svcUrl) { qualStatus.innerHTML = 'No service URL available'; return; } if (!apiKey || apiKey === '••••••••') { qualStatus.innerHTML = 'Enter an API key first'; return; } qualSelect.innerHTML = ''; qualStatus.textContent = ''; try { const params = new URLSearchParams({ service: svcId, url: svcUrl, apiKey }); const res = await fetch(`/api/v1/arr/quality-profiles?${params}`); const data = await res.json(); if (!data.success) { qualSelect.innerHTML = ''; qualStatus.innerHTML = `${data.error || 'Failed to fetch profiles'}`; return; } if (!data.profiles?.length) { qualSelect.innerHTML = ''; return; } qualSelect.innerHTML = ''; for (const p of data.profiles) { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = p.name; qualSelect.appendChild(opt); } // Pre-select 720p match const match720 = data.profiles.find(p => /720/i.test(p.name)); if (match720) qualSelect.value = String(match720.id); else if (data.profiles.length) qualSelect.value = String(data.profiles[0].id); qualStatus.innerHTML = `${data.profiles.length} profiles loaded`; } catch (e) { qualSelect.innerHTML = ''; qualStatus.innerHTML = `${e.message}`; } }); // Save button document.getElementById('svc-creds-save')?.addEventListener('click', async () => { if (!currentService) return; const saveBtn = document.getElementById('svc-creds-save'); saveBtn.textContent = 'Saving...'; saveBtn.disabled = true; hideError(); try { const isArr = arrServices.includes(currentService.id) || arrServices.includes(currentService.appTemplate); const svcId = currentService.id || currentService.appTemplate; // Save seedhost creds (shared username + per-service password) if (currentService.isExternal) { const user = document.getElementById('svc-seedhost-user').value.trim(); const pass = document.getElementById('svc-seedhost-pass').value; if (user) { await secureFetch('/api/v1/seedhost-creds', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user, password: pass || undefined, serviceId: currentService.id }) }); } } // Save API key — for arr services, use the arr credentials endpoint (correct namespace) const apiKeyInput = document.getElementById('svc-apikey-input'); const apiKey = apiKeyInput?.value.trim(); if (apiKey && apiKey !== '••••••••') { if (isArr) { // Use arr credentials endpoint — validates key, tests connection, stores in arr.* namespace const svcUrl = getServiceUrl(currentService); const qualSelect = document.getElementById('svc-quality-select'); const qualityProfileId = qualSelect?.value ? parseInt(qualSelect.value) : undefined; const qualityProfileName = qualSelect?.selectedOptions?.[0]?.textContent || undefined; const res = await secureFetch('/api/v1/arr/credentials', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service: svcId, apiKey, url: svcUrl || undefined, qualityProfileId: qualityProfileId || undefined, qualityProfileName: qualityProfileName || undefined }) }); const data = await res.json(); if (!data.success) { showError(data.error || 'Failed to save API key'); saveBtn.textContent = 'Save'; saveBtn.disabled = false; return; } if (data.connectionTest && !data.connectionTest.success) { showError(`API key saved but connection test failed: ${data.connectionTest.error}`); } } else { // Non-arr services use the generic endpoint await secureFetch(`/api/v1/services/${currentService.id}/credentials`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey }) }); } } else if (isArr && qualityProfileServices.includes(svcId)) { // API key unchanged but user may have changed quality profile — save profile only const qualSelect = document.getElementById('svc-quality-select'); const qualityProfileId = qualSelect?.value ? parseInt(qualSelect.value) : undefined; const qualityProfileName = qualSelect?.selectedOptions?.[0]?.textContent || undefined; if (qualityProfileId) { await secureFetch('/api/v1/arr/quality-profiles', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service: svcId, qualityProfileId, qualityProfileName }) }); } } // Save per-service basic auth if (!currentService.isExternal) { const user = document.getElementById('svc-basic-user').value.trim(); const pass = document.getElementById('svc-basic-pass').value; if (user && pass) { await secureFetch(`/api/v1/services/${currentService.id}/credentials`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user, password: pass }) }); } } await loadServiceCreds(currentService); } catch (e) { console.error('Failed to save credentials:', e); showError('Failed to save: ' + (e.message || 'Unknown error')); } saveBtn.textContent = 'Save'; saveBtn.disabled = false; }); // Clear button document.getElementById('svc-creds-clear')?.addEventListener('click', async () => { if (!currentService) return; if (!confirm(`Remove stored credentials for ${currentService.name}?`)) return; hideError(); try { const svcId = currentService.id || currentService.appTemplate; const isArr = arrServices.includes(svcId); if (currentService.isExternal) { await secureFetch(`/api/v1/seedhost-creds?serviceId=${currentService.id}`, { method: 'DELETE' }); } // Delete from both namespaces await secureFetch(`/api/v1/services/${currentService.id}/credentials`, { method: 'DELETE' }); if (isArr) { await secureFetch(`/api/v1/arr/credentials/${svcId}`, { method: 'DELETE' }); } const btn = document.getElementById(`creds-btn-${currentService.id}`); if (btn) btn.classList.remove('has-creds'); await loadServiceCreds(currentService); } catch (e) { console.error('Failed to clear credentials:', e); showError('Failed to clear: ' + (e.message || 'Unknown error')); } }); // Close button / backdrop document.getElementById('svc-creds-close')?.addEventListener('click', () => { modal.classList.remove('show'); currentService = null; }); modal?.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('show'); currentService = null; } }); // Check credential status for all services on page load (update key button highlights) window.refreshCredsButtons = async function() { try { for (const app of (window.APPS || [])) { if (!app.isExternal && !app.appTemplate && !app.url) continue; let hasCreds = false; if (app.isExternal) { try { const seedRes = await fetch(`/api/v1/seedhost-creds?serviceId=${app.id}`); const seedData = await seedRes.json(); if (seedData.success && seedData.hasCredentials) hasCreds = true; } catch (e) { /* ignore */ } } try { const r = await fetch(`/api/v1/services/${app.id}/credentials`); const d = await r.json(); if (d.success && (d.hasApiKey || d.hasBasicAuth)) hasCreds = true; } catch (e) { /* ignore */ } const btn = document.getElementById(`creds-btn-${app.id}`); if (btn) btn.classList.toggle('has-creds', hasCreds); } } catch (e) { /* ignore */ } }; })();