From abbb93f05320d543c12535f025e0bacee628b2f6 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Wed, 12 Nov 2025 18:35:07 -0800 Subject: [PATCH] scan tool --- webui/index.html | 31 +++++++ webui/static/script.js | 200 +++++++++++++++++++++++++++++++++++++++++ webui/static/style.css | 38 ++++++++ 3 files changed, 269 insertions(+) diff --git a/webui/index.html b/webui/index.html index 1adc10bf..574fcb9d 100644 --- a/webui/index.html +++ b/webui/index.html @@ -384,6 +384,37 @@

0 files scanned (0.0%)

+ + diff --git a/webui/static/script.js b/webui/static/script.js index 5352e8be..1360a396 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -9467,6 +9467,44 @@ const TOOL_HELP_CONTENT = {
  • Space Freed: Total disk space reclaimed
  • ` + }, + 'media-scan': { + title: 'Media Server Scan', + content: ` +

    What does this tool do?

    +

    The Media Server Scan tool manually triggers a Plex media library scan to detect newly downloaded music files.

    + +

    When to use it?

    + + +

    What happens when you scan?

    +
      +
    1. Plex library scan: Plex scans your music folder for new/changed files
    2. +
    3. Automatic database update: After the scan completes, SoulSync automatically updates its internal database with new tracks
    4. +
    5. Library refreshed: New music appears in Plex and SoulSync within moments
    6. +
    + +

    Plex only?

    +

    Yes! This tool only appears when Plex is your active media server because:

    + + +

    Stats Explained

    + + +

    Scan workflow

    +

    This tool replicates the same scan process that runs automatically after completing a download modal - ensuring your new tracks are immediately available in your library!

    + ` } }; @@ -9637,6 +9675,15 @@ async function loadDashboardData() { duplicateCleanButton.addEventListener('click', handleDuplicateCleanButtonClick); } + // Attach event listener for the media scan tool + const mediaScanButton = document.getElementById('media-scan-button'); + if (mediaScanButton) { + mediaScanButton.addEventListener('click', handleMediaScanButtonClick); + } + + // Check active media server and show media scan tool only for Plex + await checkAndShowMediaScanForPlex(); + // Attach event listeners for tool help buttons initializeToolHelpButtons(); @@ -19456,6 +19503,159 @@ async function checkAndHideMetadataUpdaterForNonPlex() { } } +async function checkAndShowMediaScanForPlex() { + /** + * Show media scan tool only for Plex (Jellyfin/Navidrome auto-scan) + */ + try { + const response = await fetch('/api/active-media-server'); + const data = await response.json(); + + if (data.success) { + const mediaScanCard = document.getElementById('media-scan-card'); + if (mediaScanCard) { + // Show media scan tool only for Plex + if (data.active_server === 'plex') { + mediaScanCard.style.display = 'flex'; + console.log('Media scan tool shown: Plex is active server'); + } else { + // Hide for Jellyfin/Navidrome (they auto-scan) + mediaScanCard.style.display = 'none'; + console.log(`Media scan tool hidden: ${data.active_server} auto-scans`); + } + } + } + } catch (error) { + console.warn('Could not check active media server for media scan visibility:', error); + } +} + +async function handleMediaScanButtonClick() { + /** + * Trigger a manual Plex media library scan + */ + const button = document.getElementById('media-scan-button'); + const phaseLabel = document.getElementById('media-scan-phase-label'); + const progressBar = document.getElementById('media-scan-progress-bar'); + const progressLabel = document.getElementById('media-scan-progress-label'); + const statusValue = document.getElementById('media-scan-status'); + + if (!button) return; + + try { + // Disable button and update UI + button.disabled = true; + phaseLabel.textContent = 'Requesting scan...'; + progressBar.style.width = '30%'; + progressLabel.textContent = 'Sending scan request to Plex'; + statusValue.textContent = 'Scanning...'; + statusValue.style.color = '#1db954'; + + // Request scan + const response = await fetch('/api/scan/request', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + reason: 'Manual scan triggered from dashboard', + auto_database_update: true + }) + }); + + const result = await response.json(); + + if (result.success) { + // Update UI to show scan in progress + phaseLabel.textContent = 'Scan in progress...'; + progressBar.style.width = '100%'; + + if (result.scan_info && result.scan_info.delay_seconds) { + progressLabel.textContent = `Scan scheduled (${result.scan_info.delay_seconds}s delay)`; + showToast(`📡 Media scan scheduled (${result.scan_info.delay_seconds}s delay)`, 'success', 5000); + } else { + progressLabel.textContent = 'Scan initiated successfully'; + showToast('📡 Media scan initiated successfully', 'success', 3000); + } + + // Show auto database update message + if (result.auto_database_update) { + showToast('🔄 Database will update automatically after scan', 'info', 3000); + } + + // Update last scan time + const lastTimeEl = document.getElementById('media-scan-last-time'); + if (lastTimeEl) { + const now = new Date(); + lastTimeEl.textContent = now.toLocaleTimeString(); + } + + // Poll scan status for ~30 seconds + let pollCount = 0; + const pollInterval = setInterval(async () => { + pollCount++; + + if (pollCount > 15) { // Stop after 30 seconds (15 * 2s) + clearInterval(pollInterval); + // Reset UI + button.disabled = false; + phaseLabel.textContent = 'Scan completed'; + progressBar.style.width = '0%'; + progressLabel.textContent = 'Ready for next scan'; + statusValue.textContent = 'Idle'; + statusValue.style.color = '#b3b3b3'; + return; + } + + try { + const statusResponse = await fetch('/api/scan/status'); + const statusData = await statusResponse.json(); + + if (statusData.success && statusData.status) { + const status = statusData.status; + + // Update status display + if (status.is_scanning) { + phaseLabel.textContent = 'Plex is scanning library...'; + progressLabel.textContent = status.progress_message || 'Scan in progress'; + } else { + // Scan complete + clearInterval(pollInterval); + button.disabled = false; + phaseLabel.textContent = 'Scan completed successfully'; + progressBar.style.width = '0%'; + progressLabel.textContent = 'Ready for next scan'; + statusValue.textContent = 'Idle'; + statusValue.style.color = '#b3b3b3'; + showToast('✅ Media scan completed', 'success', 3000); + } + } + } catch (pollError) { + console.debug('Scan status poll error:', pollError); + } + }, 2000); // Poll every 2 seconds + + } else { + // Error occurred + showToast(`❌ Scan request failed: ${result.error}`, 'error', 5000); + button.disabled = false; + phaseLabel.textContent = 'Scan failed'; + progressBar.style.width = '0%'; + progressLabel.textContent = result.error || 'Unknown error'; + statusValue.textContent = 'Error'; + statusValue.style.color = '#f44336'; + } + + } catch (error) { + console.error('Error requesting media scan:', error); + showToast('❌ Failed to request media scan', 'error', 3000); + button.disabled = false; + phaseLabel.textContent = 'Error'; + progressBar.style.width = '0%'; + progressLabel.textContent = error.message; + statusValue.textContent = 'Error'; + statusValue.style.color = '#f44336'; + } +} + /** * Check for ongoing metadata update and restore state on page load */ diff --git a/webui/static/style.css b/webui/static/style.css index 9fb3e5db..b6206e62 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -4136,6 +4136,44 @@ body { transform: none; } +/* Media Scan Button Styling */ +.media-scan-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + background: linear-gradient(135deg, #667eea, #764ba2) !important; + padding: 14px 24px !important; + font-size: 15px !important; + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3); + width: 100%; +} + +.media-scan-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #7c8ff0, #8a5ab8) !important; + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); + transform: translateY(-2px); +} + +.media-scan-btn .scan-icon { + font-size: 18px; + animation: pulse 2s ease-in-out infinite; +} + +.media-scan-btn:disabled .scan-icon { + animation: spin 1s linear infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(1.1); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + .tool-card-progress-section { padding-top: 10px; border-top: 1px solid #404040;