// ============================================================ // Scripts: CRUD, quick access, execution dialog // ============================================================ let scriptFormDirty = false; async function loadScripts() { const token = localStorage.getItem('media_server_token'); try { const response = await fetch('/api/scripts/list', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { scripts = await response.json(); displayQuickAccess(); } } catch (error) { console.error('Error loading scripts:', error); } } let _quickAccessGen = 0; async function displayQuickAccess() { const gen = ++_quickAccessGen; const grid = document.getElementById('scripts-grid'); const fragment = document.createDocumentFragment(); const hasScripts = scripts.length > 0; let hasLinks = false; scripts.forEach(script => { const button = document.createElement('button'); button.className = 'script-btn'; button.onclick = () => executeScript(script.name, button); if (script.icon) { const iconEl = document.createElement('div'); iconEl.className = 'script-icon'; iconEl.setAttribute('data-mdi-icon', script.icon); button.appendChild(iconEl); } const label = document.createElement('div'); label.className = 'script-label'; label.textContent = script.label || script.name; button.appendChild(label); if (script.description) { const description = document.createElement('div'); description.className = 'script-description'; description.textContent = script.description; button.appendChild(description); } fragment.appendChild(button); }); try { const token = localStorage.getItem('media_server_token'); if (token) { const response = await fetch('/api/links/list', { headers: { 'Authorization': `Bearer ${token}` } }); if (gen !== _quickAccessGen) return; if (response.ok) { const links = await response.json(); hasLinks = links.length > 0; links.forEach(link => { const card = document.createElement('a'); card.className = 'script-btn link-card'; card.href = link.url; card.target = '_blank'; card.rel = 'noopener noreferrer'; if (link.icon) { const iconEl = document.createElement('div'); iconEl.className = 'script-icon'; iconEl.setAttribute('data-mdi-icon', link.icon); card.appendChild(iconEl); } const label = document.createElement('div'); label.className = 'script-label'; label.textContent = link.label || link.name; card.appendChild(label); if (link.description) { const desc = document.createElement('div'); desc.className = 'script-description'; desc.textContent = link.description; card.appendChild(desc); } fragment.appendChild(card); }); } } } catch (e) { if (gen !== _quickAccessGen) return; console.warn('Failed to load links for quick access:', e); } if (!hasScripts && !hasLinks) { const empty = document.createElement('div'); empty.className = 'scripts-empty empty-state-illustration'; empty.innerHTML = `

${t('quick_access.no_items')}

`; fragment.prepend(empty); } grid.innerHTML = ''; grid.appendChild(fragment); resolveMdiIcons(grid); } async function executeScript(scriptName, buttonElement) { const token = localStorage.getItem('media_server_token'); buttonElement.classList.add('executing'); try { const response = await fetch(`/api/scripts/execute/${scriptName}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ args: [] }) }); const result = await response.json(); if (response.ok && result.success) { showToast(`${scriptName} executed successfully`, 'success'); } else { showToast(`Failed to execute ${scriptName}`, 'error'); } } catch (error) { console.error(`Error executing script ${scriptName}:`, error); showToast(`Error executing ${scriptName}`, 'error'); } finally { buttonElement.classList.remove('executing'); } } // ============================================================ // Script Management CRUD // ============================================================ let _loadScriptsPromise = null; async function loadScriptsTable() { if (_loadScriptsPromise) return _loadScriptsPromise; _loadScriptsPromise = _loadScriptsTableImpl(); _loadScriptsPromise.finally(() => { _loadScriptsPromise = null; }); return _loadScriptsPromise; } async function _loadScriptsTableImpl() { const token = localStorage.getItem('media_server_token'); const tbody = document.getElementById('scriptsTableBody'); try { const response = await fetch('/api/scripts/list', { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('Failed to fetch scripts'); } const scriptsList = await response.json(); if (scriptsList.length === 0) { tbody.innerHTML = '

' + t('scripts.empty') + '

'; return; } tbody.innerHTML = scriptsList.map(script => ` ${script.icon ? `` : ''}${escapeHtml(script.name)} ${escapeHtml(script.label || script.name)} ${escapeHtml(script.command || 'N/A')} ${script.timeout}s
`).join(''); resolveMdiIcons(tbody); } catch (error) { console.error('Error loading scripts:', error); tbody.innerHTML = 'Failed to load scripts'; } } function showAddScriptDialog() { const dialog = document.getElementById('scriptDialog'); const form = document.getElementById('scriptForm'); const title = document.getElementById('dialogTitle'); form.reset(); document.getElementById('scriptOriginalName').value = ''; document.getElementById('scriptIsEdit').value = 'false'; document.getElementById('scriptName').disabled = false; document.getElementById('scriptIconPreview').innerHTML = ''; title.textContent = t('scripts.dialog.add'); scriptFormDirty = false; document.body.classList.add('dialog-open'); dialog.showModal(); } async function showEditScriptDialog(scriptName) { const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('scriptDialog'); const title = document.getElementById('dialogTitle'); try { const response = await fetch('/api/scripts/list', { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('Failed to fetch script details'); } const scriptsList = await response.json(); const script = scriptsList.find(s => s.name === scriptName); if (!script) { showToast('Script not found', 'error'); return; } document.getElementById('scriptOriginalName').value = scriptName; document.getElementById('scriptIsEdit').value = 'true'; document.getElementById('scriptName').value = scriptName; document.getElementById('scriptName').disabled = true; document.getElementById('scriptLabel').value = script.label || ''; document.getElementById('scriptCommand').value = script.command || ''; document.getElementById('scriptDescription').value = script.description || ''; document.getElementById('scriptIcon').value = script.icon || ''; document.getElementById('scriptTimeout').value = script.timeout || 30; const preview = document.getElementById('scriptIconPreview'); if (script.icon) { fetchMdiIcon(script.icon).then(svg => { preview.innerHTML = svg; }); } else { preview.innerHTML = ''; } title.textContent = t('scripts.dialog.edit'); scriptFormDirty = false; document.body.classList.add('dialog-open'); dialog.showModal(); } catch (error) { console.error('Error loading script for edit:', error); showToast('Failed to load script details', 'error'); } } async function closeScriptDialog() { if (scriptFormDirty) { if (!await showConfirm(t('scripts.confirm.unsaved'))) { return; } } const dialog = document.getElementById('scriptDialog'); scriptFormDirty = false; dialog.close(); document.body.classList.remove('dialog-open'); } async function saveScript(event) { event.preventDefault(); const submitBtn = event.target.querySelector('button[type="submit"]'); if (submitBtn) submitBtn.disabled = true; const token = localStorage.getItem('media_server_token'); const isEdit = document.getElementById('scriptIsEdit').value === 'true'; const scriptName = isEdit ? document.getElementById('scriptOriginalName').value : document.getElementById('scriptName').value; const data = { command: document.getElementById('scriptCommand').value, label: document.getElementById('scriptLabel').value || null, description: document.getElementById('scriptDescription').value || '', icon: document.getElementById('scriptIcon').value || null, timeout: parseInt(document.getElementById('scriptTimeout').value) || 30, shell: true }; const endpoint = isEdit ? `/api/scripts/update/${scriptName}` : `/api/scripts/create/${scriptName}`; const method = isEdit ? 'PUT' : 'POST'; try { const response = await fetch(endpoint, { method, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (response.ok && result.success) { showToast(`Script ${isEdit ? 'updated' : 'created'} successfully`, 'success'); scriptFormDirty = false; closeScriptDialog(); } else { showToast(result.detail || `Failed to ${isEdit ? 'update' : 'create'} script`, 'error'); } } catch (error) { console.error('Error saving script:', error); showToast(`Error ${isEdit ? 'updating' : 'creating'} script`, 'error'); } finally { if (submitBtn) submitBtn.disabled = false; } } async function deleteScriptConfirm(scriptName) { if (!await showConfirm(t('scripts.confirm.delete').replace('{name}', scriptName))) { return; } const token = localStorage.getItem('media_server_token'); try { const response = await fetch(`/api/scripts/delete/${scriptName}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); const result = await response.json(); if (response.ok && result.success) { showToast('Script deleted successfully', 'success'); } else { showToast(result.detail || 'Failed to delete script', 'error'); } } catch (error) { console.error('Error deleting script:', error); showToast('Error deleting script', 'error'); } } // ============================================================ // Execution Result Dialog (shared by scripts and callbacks) // ============================================================ function closeExecutionDialog() { const dialog = document.getElementById('executionDialog'); dialog.close(); document.body.classList.remove('dialog-open'); } function showExecutionResult(name, result, type = 'script') { const dialog = document.getElementById('executionDialog'); const title = document.getElementById('executionDialogTitle'); const statusDiv = document.getElementById('executionStatus'); const outputSection = document.getElementById('outputSection'); const errorSection = document.getElementById('errorSection'); const outputPre = document.getElementById('executionOutput'); const errorPre = document.getElementById('executionError'); title.textContent = `Execution Result: ${name}`; const success = result.success && result.exit_code === 0; const statusClass = success ? 'success' : 'error'; const statusText = success ? 'Success' : 'Failed'; statusDiv.innerHTML = `
${statusText}
${result.exit_code !== undefined ? result.exit_code : 'N/A'}
${result.execution_time !== undefined && result.execution_time !== null ? result.execution_time.toFixed(3) + 's' : 'N/A'}
`; outputSection.style.display = 'block'; if (result.stdout && result.stdout.trim()) { outputPre.textContent = result.stdout; } else { outputPre.textContent = '(no output)'; outputPre.style.fontStyle = 'italic'; outputPre.style.color = 'var(--text-secondary)'; } if (result.stderr && result.stderr.trim()) { errorSection.style.display = 'block'; errorPre.textContent = result.stderr; errorPre.style.fontStyle = 'normal'; errorPre.style.color = 'var(--error)'; } else if (!success && result.error) { errorSection.style.display = 'block'; errorPre.textContent = result.error; errorPre.style.fontStyle = 'normal'; errorPre.style.color = 'var(--error)'; } else { errorSection.style.display = 'none'; } dialog.showModal(); } async function executeScriptDebug(scriptName) { const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('executionDialog'); const title = document.getElementById('executionDialogTitle'); const statusDiv = document.getElementById('executionStatus'); title.textContent = `Executing: ${scriptName}`; statusDiv.innerHTML = `
Running...
`; document.getElementById('outputSection').style.display = 'none'; document.getElementById('errorSection').style.display = 'none'; document.body.classList.add('dialog-open'); dialog.showModal(); try { const response = await fetch(`/api/scripts/execute/${scriptName}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ args: [] }) }); const result = await response.json(); if (response.ok) { showExecutionResult(scriptName, result, 'script'); } else { showExecutionResult(scriptName, { success: false, exit_code: -1, error: result.detail || 'Execution failed', stderr: result.detail || 'Unknown error' }, 'script'); } } catch (error) { console.error(`Error executing script ${scriptName}:`, error); showExecutionResult(scriptName, { success: false, exit_code: -1, error: error.message, stderr: `Network error: ${error.message}` }, 'script'); } } async function executeCallbackDebug(callbackName) { const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('executionDialog'); const title = document.getElementById('executionDialogTitle'); const statusDiv = document.getElementById('executionStatus'); title.textContent = `Executing: ${callbackName}`; statusDiv.innerHTML = `
Running...
`; document.getElementById('outputSection').style.display = 'none'; document.getElementById('errorSection').style.display = 'none'; document.body.classList.add('dialog-open'); dialog.showModal(); try { const response = await fetch(`/api/callbacks/execute/${callbackName}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); const result = await response.json(); if (response.ok) { showExecutionResult(callbackName, result, 'callback'); } else { showExecutionResult(callbackName, { success: false, exit_code: -1, error: result.detail || 'Execution failed', stderr: result.detail || 'Unknown error' }, 'callback'); } } catch (error) { console.error(`Error executing callback ${callbackName}:`, error); showExecutionResult(callbackName, { success: false, exit_code: -1, error: error.message, stderr: `Network error: ${error.message}` }, 'callback'); } }