diff --git a/webui/static/core.js b/webui/static/core.js index 9257e233..3a23cb53 100644 --- a/webui/static/core.js +++ b/webui/static/core.js @@ -476,6 +476,12 @@ function handleServiceStatusUpdate(data) { if (typeof syncSpotifySettingsAuthState === 'function') { syncSpotifySettingsAuthState(data?.spotify || null); } + if (typeof syncPrimaryMetadataSourceAvailability === 'function') { + syncPrimaryMetadataSourceAvailability(data?.spotify || null); + } + if (typeof sanitizeMetadataSourceSelection === 'function') { + sanitizeMetadataSourceSelection({ quiet: true }); + } // Same logic as fetchAndUpdateServiceStatus response handler updateServiceStatus('spotify', data.spotify); diff --git a/webui/static/settings.js b/webui/static/settings.js index c034298f..dfa092bb 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -58,8 +58,25 @@ function syncMetadataSourceSelection(source) { select.dataset.lastValidSource = source; } -function focusSpotifySettingsSection() { - const card = document.querySelector('#settings-page .stg-service[data-service="spotify"]'); +function _isMetadataSourceSelectable(source) { + if (source === 'spotify') { + return _lastServiceStatus?.spotify?.authenticated === true; + } + if (source === 'discogs') { + const token = document.getElementById('discogs-token'); + return !!token?.value?.trim(); + } + return true; +} + +function _metadataSourceFallback(source) { + if (source === 'spotify') return 'deezer'; + if (source === 'discogs') return 'itunes'; + return 'itunes'; +} + +function focusServiceSettingsSection(service, message) { + const card = document.querySelector(`#settings-page .stg-service[data-service="${service}"]`); if (!card) return; const header = card.querySelector('.stg-service-header'); @@ -74,7 +91,39 @@ function focusSpotifySettingsSection() { firstControl.focus({ preventScroll: true }); } - showToast('Spotify must be authenticated before it can be selected as the primary metadata source.', 'warning'); + if (message) { + showToast(message, 'warning'); + } +} + +function sanitizeMetadataSourceSelection({ quiet = true } = {}) { + const select = document.getElementById('metadata-fallback-source'); + if (!select) return false; + + const selectedSource = select.value || 'itunes'; + if (_isMetadataSourceSelectable(selectedSource)) { + select.dataset.lastValidSource = selectedSource; + return false; + } + + const lastValid = select.dataset.lastValidSource; + const fallbackSource = lastValid && lastValid !== selectedSource && _isMetadataSourceSelectable(lastValid) + ? lastValid + : _metadataSourceFallback(selectedSource); + + if (fallbackSource && fallbackSource !== selectedSource) { + select.value = fallbackSource; + } + select.dataset.lastValidSource = fallbackSource; + + if (!quiet) { + const message = selectedSource === 'discogs' + ? 'Discogs requires a personal access token before it can be selected as the primary metadata source.' + : 'Spotify must be authenticated before it can be selected as the primary metadata source.'; + focusServiceSettingsSection(selectedSource, message); + } + + return true; } function handleMetadataSourceChange(event) { @@ -82,22 +131,12 @@ function handleMetadataSourceChange(event) { if (!select || select.id !== 'metadata-fallback-source') return; const selectedSource = select.value; - if (selectedSource !== 'spotify') { + if (_isMetadataSourceSelectable(selectedSource)) { select.dataset.lastValidSource = selectedSource; return; } - const spotifySessionActive = _lastServiceStatus?.spotify?.authenticated === true; - if (spotifySessionActive) { - select.dataset.lastValidSource = selectedSource; - return; - } - - const fallbackSource = select.dataset.lastValidSource || _lastServiceStatus?.spotify?.source || 'deezer'; - if (fallbackSource && fallbackSource !== 'spotify') { - select.value = fallbackSource; - } - focusSpotifySettingsSection(); + sanitizeMetadataSourceSelection({ quiet: false }); } function initializeSettings() { @@ -130,6 +169,15 @@ function initializeSettings() { if (metadataSourceSelect) { metadataSourceSelect.addEventListener('change', handleMetadataSourceChange); } + const discogsTokenInput = document.getElementById('discogs-token'); + if (discogsTokenInput) { + discogsTokenInput.addEventListener('input', () => { + if (typeof syncPrimaryMetadataSourceAvailability === 'function') { + syncPrimaryMetadataSourceAvailability(_lastServiceStatus?.spotify || null); + } + sanitizeMetadataSourceSelection({ quiet: true }); + }); + } // Server toggle buttons const plexToggle = document.getElementById('plex-toggle'); @@ -158,11 +206,12 @@ function initializeSettings() { // Test connection buttons // Test button event listeners removed - they use onclick attributes in HTML to avoid double firing - if (typeof syncSpotifyMetadataSourceAvailability === 'function') { - syncSpotifyMetadataSourceAvailability(_lastServiceStatus?.spotify || null); + if (typeof syncPrimaryMetadataSourceAvailability === 'function') { + syncPrimaryMetadataSourceAvailability(_lastServiceStatus?.spotify || null); } syncSpotifySettingsAuthState(_lastServiceStatus?.spotify || null); syncMetadataSourceSelection(_lastServiceStatus?.spotify?.source); + sanitizeMetadataSourceSelection({ quiet: true }); if (metadataSourceSelect) { metadataSourceSelect.dataset.lastValidSource = metadataSourceSelect.value; } @@ -2491,6 +2540,8 @@ async function saveSettings(quiet = false) { } const metadataSourceSelect = document.getElementById('metadata-fallback-source'); + const discogsTokenInput = document.getElementById('discogs-token'); + const discogsTokenPresent = !!discogsTokenInput?.value?.trim(); let metadataSource = metadataSourceSelect?.value || 'itunes'; const spotifySessionActive = _lastServiceStatus?.spotify?.authenticated === true; if (metadataSource === 'spotify' && !spotifySessionActive) { @@ -2499,6 +2550,12 @@ async function saveSettings(quiet = false) { if (!quiet) { showToast('Spotify is disconnected, so Deezer is used as the primary metadata source.', 'warning'); } + } else if (metadataSource === 'discogs' && !discogsTokenPresent) { + metadataSource = 'itunes'; + if (metadataSourceSelect) metadataSourceSelect.value = metadataSource; + if (!quiet) { + showToast('Discogs requires a personal access token before it can be selected as the primary metadata source.', 'warning'); + } } const settings = { diff --git a/webui/static/shared-helpers.js b/webui/static/shared-helpers.js index 2262b40f..6fed9d88 100644 --- a/webui/static/shared-helpers.js +++ b/webui/static/shared-helpers.js @@ -3148,6 +3148,12 @@ async function fetchAndUpdateServiceStatus() { if (typeof syncSpotifySettingsAuthState === 'function') { syncSpotifySettingsAuthState(data?.spotify || null); } + if (typeof syncPrimaryMetadataSourceAvailability === 'function') { + syncPrimaryMetadataSourceAvailability(data?.spotify || null); + } + if (typeof sanitizeMetadataSourceSelection === 'function') { + sanitizeMetadataSourceSelection({ quiet: true }); + } // Update service status indicators and text (dashboard) updateServiceStatus('spotify', data.spotify); @@ -3190,20 +3196,32 @@ async function fetchAndUpdateServiceStatus() { } } -function syncSpotifyMetadataSourceAvailability(statusData) { +function syncPrimaryMetadataSourceAvailability(statusData) { const select = document.getElementById('metadata-fallback-source'); if (!select) return; if (!statusData) return; const spotifyOption = select.querySelector('option[value="spotify"]'); - if (!spotifyOption) return; + const discogsOption = select.querySelector('option[value="discogs"]'); const spotifyAvailable = statusData?.authenticated === true; - spotifyOption.dataset.unavailable = spotifyAvailable ? 'false' : 'true'; - spotifyOption.textContent = spotifyAvailable ? 'Spotify' : '🔒 Spotify'; - spotifyOption.title = spotifyAvailable - ? 'Spotify' - : 'Spotify authentication is required before this source can be selected.'; + if (spotifyOption) { + spotifyOption.dataset.unavailable = spotifyAvailable ? 'false' : 'true'; + spotifyOption.textContent = spotifyAvailable ? 'Spotify' : '🔒 Spotify'; + spotifyOption.title = spotifyAvailable + ? 'Spotify' + : 'Spotify authentication is required before this source can be selected.'; + } + + if (discogsOption) { + const discogsToken = document.getElementById('discogs-token'); + const discogsAvailable = !!discogsToken?.value?.trim(); + discogsOption.dataset.unavailable = discogsAvailable ? 'false' : 'true'; + discogsOption.textContent = discogsAvailable ? 'Discogs' : '🔒 Discogs'; + discogsOption.title = discogsAvailable + ? 'Discogs' + : 'Discogs personal access token is required before this source can be selected.'; + } } function getMetadataSourceLabel(source) { @@ -3304,7 +3322,7 @@ function updateServiceStatus(service, statusData) { disconnectBtn.style.display = spotifySessionActive ? '' : 'none'; } - syncSpotifyMetadataSourceAvailability(statusData); + syncPrimaryMetadataSourceAvailability(statusData); } // Update download source title on dashboard card