diff --git a/webui/static/script.js b/webui/static/script.js index d4f6178b..72a14f49 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -69,9 +69,94 @@ let deezerArlPlaylistsLoaded = false; let beatportChartStates = {}; // Key: chart_hash, Value: chart state with phases let beatportContentState = { loaded: false, - loadingPromise: null + loadingPromise: null, + abortController: null }; +function getBeatportContentSignal() { + return beatportContentState.abortController ? beatportContentState.abortController.signal : null; +} + +function throwIfBeatportLoadAborted() { + if (beatportContentState.abortController && beatportContentState.abortController.signal.aborted) { + throw new DOMException('Beatport load aborted', 'AbortError'); + } +} + +function stopBeatportDiscoveryAndSyncPolling() { + Object.entries(activeYouTubePollers).forEach(([identifier, poller]) => { + const isBeatportChart = !!youtubePlaylistStates[identifier]?.is_beatport_playlist || + !!beatportChartStates[identifier]; + if (isBeatportChart) { + clearInterval(poller); + delete activeYouTubePollers[identifier]; + } + }); + + Object.entries(_discoveryProgressCallbacks).forEach(([identifier]) => { + const isBeatportChart = !!youtubePlaylistStates[identifier]?.is_beatport_playlist || + !!beatportChartStates[identifier]; + if (isBeatportChart) { + if (socketConnected) socket.emit('discovery:unsubscribe', { ids: [identifier] }); + delete _discoveryProgressCallbacks[identifier]; + } + }); + + Object.entries(_syncProgressCallbacks).forEach(([syncPlaylistId]) => { + const beatportState = Object.values(youtubePlaylistStates).find(state => + state && state.is_beatport_playlist && state.syncPlaylistId === syncPlaylistId + ); + if (beatportState) { + if (socketConnected) socket.emit('sync:unsubscribe', { playlist_ids: [syncPlaylistId] }); + delete _syncProgressCallbacks[syncPlaylistId]; + } + }); +} + +function resetBeatportSliderInitFlags() { + const rebuildSlider = document.getElementById('beatport-rebuild-slider'); + if (rebuildSlider) rebuildSlider.dataset.initialized = 'false'; + + const releasesSlider = document.getElementById('beatport-releases-slider'); + if (releasesSlider) releasesSlider.dataset.initialized = 'false'; + beatportReleasesSliderState.isInitialized = false; + + beatportHypePicksSliderState.isInitialized = false; + + const chartsSlider = document.getElementById('beatport-charts-slider'); + if (chartsSlider) chartsSlider.dataset.initialized = 'false'; + beatportChartsSliderState.isInitialized = false; + + const djSlider = document.getElementById('beatport-dj-slider'); + if (djSlider) djSlider.dataset.initialized = 'false'; + beatportDJSliderState.isInitialized = false; +} + +function cleanupBeatportContent() { + const wasLoaded = beatportContentState.loaded || !!beatportContentState.loadingPromise; + if (!wasLoaded) return; + + console.log('๐Ÿงน Cleaning up Beatport content...'); + + if (beatportContentState.abortController) { + beatportContentState.abortController.abort(); + beatportContentState.abortController = null; + } + + stopBeatportDiscoveryAndSyncPolling(); + cleanupBeatportRebuildSlider(); + cleanupBeatportReleasesSlider(); + cleanupBeatportHypePicksSlider(); + cleanupBeatportChartsSlider(); + cleanupBeatportDJSlider(); + resetBeatportSliderInitFlags(); + + beatportContentState.loadingPromise = null; + beatportContentState.loaded = false; + + console.log('โœ… Beatport content cleaned up'); +} + // --- ListenBrainz Playlist State Management (Similar to YouTube/Tidal/Beatport) --- let listenbrainzPlaylistStates = {}; // Key: playlist_mbid, Value: playlist state with phases let listenbrainzPlaylistsLoaded = false; // Track if playlists have been loaded from backend @@ -3013,6 +3098,9 @@ async function loadPageData(pageId) { if (watchlistCountdownInterval) { clearInterval(watchlistCountdownInterval); watchlistCountdownInterval = null; } if (wishlistCountdownInterval) { clearInterval(wishlistCountdownInterval); wishlistCountdownInterval = null; } if (typeof _stopNebulaLivePolling === 'function') _stopNebulaLivePolling(); + if (pageId !== 'sync') { + cleanupBeatportContent(); + } switch (pageId) { case 'dashboard': await loadDashboardData(); @@ -10771,32 +10859,44 @@ async function ensureBeatportContentLoaded() { return beatportContentState.loadingPromise; } + beatportContentState.abortController = new AbortController(); beatportContentState.loadingPromise = (async () => { try { console.log('๐ŸŽง Lazy-loading Beatport content...'); await hydrateBeatportBubblesFromSnapshot(); + throwIfBeatportLoadAborted(); await loadBeatportChartsFromBackend(); + throwIfBeatportLoadAborted(); initializeBeatportRebuildSlider(); initializeBeatportReleasesSlider(); initializeBeatportHypePicksSlider(); initializeBeatportChartsSlider(); initializeBeatportDJSlider(); + throwIfBeatportLoadAborted(); await Promise.all([ loadBeatportTop10Lists(), loadBeatportTop10Releases() ]); + throwIfBeatportLoadAborted(); showBeatportDownloadsSection(); beatportContentState.loaded = true; console.log('โœ… Beatport content loaded'); return true; } catch (error) { + if (error && error.name === 'AbortError') { + console.log('โน Beatport content load aborted'); + return false; + } console.error('โŒ Error loading Beatport content:', error); return false; } finally { beatportContentState.loadingPromise = null; + if (beatportContentState.abortController && beatportContentState.abortController.signal.aborted) { + beatportContentState.abortController = null; + } } })(); @@ -11562,7 +11662,8 @@ async function loadBeatportChartsFromBackend() { try { console.log('๐Ÿ“‹ Loading Beatport charts from backend...'); - const response = await fetch('/api/beatport/charts'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/charts', signal ? { signal } : undefined); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch Beatport charts'); @@ -11602,7 +11703,7 @@ async function loadBeatportChartsFromBackend() { if (chartInfo.phase !== 'fresh') { try { console.log(`๐Ÿ” Fetching full state for: ${chartInfo.name}`); - const stateResponse = await fetch(`/api/beatport/charts/status/${chartHash}`); + const stateResponse = await fetch(`/api/beatport/charts/status/${chartHash}`, signal ? { signal } : undefined); if (stateResponse.ok) { const fullState = await stateResponse.json(); console.log(`๐Ÿ“‹ Retrieved full state with ${fullState.discovery_results?.length || 0} discovery results`); @@ -11652,6 +11753,7 @@ async function loadBeatportChartsFromBackend() { console.warn(`โš ๏ธ Could not fetch full state for: ${chartInfo.name}`); } } catch (error) { + if (error && error.name === 'AbortError') throw error; console.warn(`โš ๏ธ Error fetching full state for ${chartInfo.name}:`, error.message); } } @@ -11733,17 +11835,20 @@ async function loadBeatportChartsFromBackend() { } } } catch (error) { + if (error && error.name === 'AbortError') throw error; console.warn(`โš ๏ธ Error setting up download process for Beatport chart "${chartInfo.name}":`, error.message); } } } + throwIfBeatportLoadAborted(); console.log(`โœ… Successfully loaded and rehydrated ${charts.length} Beatport charts`); // Start polling for any charts that are still in discovering phase for (const chartInfo of charts) { if (chartInfo.phase === 'discovering') { console.log(`๐Ÿ”„ [Backend Loading] Auto-starting polling for discovering chart: ${chartInfo.name}`); + throwIfBeatportLoadAborted(); startBeatportDiscoveryPolling(chartInfo.hash); } } @@ -11752,6 +11857,10 @@ async function loadBeatportChartsFromBackend() { updateBeatportClearButtonState(); } catch (error) { + if (error && error.name === 'AbortError') { + console.log('โน Beatport chart hydration aborted'); + return; + } console.error('โŒ Error loading Beatport charts from backend:', error); showToast(`Error loading Beatport charts: ${error.message}`, 'error'); } @@ -11925,7 +12034,8 @@ async function rehydrateBeatportChart(chartInfo, userRequested = false) { // Get full state from backend including discovery results let fullState; try { - const stateResponse = await fetch(`/api/beatport/charts/status/${chartHash}`); + const signal = getBeatportContentSignal(); + const stateResponse = await fetch(`/api/beatport/charts/status/${chartHash}`, signal ? { signal } : undefined); if (stateResponse.ok) { fullState = await stateResponse.json(); console.log(`๐Ÿ“‹ [Rehydration] Retrieved full backend state with ${fullState.discovery_results?.length || 0} discovery results`); @@ -11933,6 +12043,7 @@ async function rehydrateBeatportChart(chartInfo, userRequested = false) { console.warn(`โš ๏ธ [Rehydration] Could not fetch full state, using basic info`); } } catch (error) { + if (error && error.name === 'AbortError') return; console.warn(`โš ๏ธ [Rehydration] Error fetching full state:`, error.message); } @@ -29633,6 +29744,8 @@ function initializeSyncPage() { tabButtons.forEach(button => { button.addEventListener('click', () => { const tabId = button.dataset.tab; + const previousActiveTab = document.querySelector('.sync-tab-button.active'); + const previousTabId = previousActiveTab ? previousActiveTab.dataset.tab : null; // Update button active state tabButtons.forEach(btn => btn.classList.remove('active')); @@ -29676,6 +29789,10 @@ function initializeSyncPage() { loadServerPlaylists(); } + if (previousTabId === 'beatport' && tabId !== 'beatport') { + cleanupBeatportContent(); + } + // Lazily load Beatport content the first time the Beatport tab is opened if (tabId === 'beatport') { ensureBeatportContentLoaded(); @@ -39985,7 +40102,8 @@ async function hydrateBeatportBubblesFromSnapshot() { try { console.log('๐Ÿ”„ Loading Beatport bubble snapshot from backend...'); - const response = await fetch('/api/beatport_bubbles/hydrate'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport_bubbles/hydrate', signal ? { signal } : undefined); const data = await response.json(); if (!data.success) { @@ -40021,6 +40139,10 @@ async function hydrateBeatportBubblesFromSnapshot() { updateBeatportDownloadsSection(); console.log(`โœ… Hydrated ${Object.keys(beatportDownloadBubbles).length} Beatport download bubbles`); } catch (error) { + if (error && error.name === 'AbortError') { + console.log('โน Beatport bubble hydration aborted'); + return; + } console.error('โŒ Error hydrating Beatport bubbles:', error); } } @@ -51055,7 +51177,8 @@ async function loadBeatportHeroTracks() { console.log('๐ŸŽฏ Loading real Beatport hero tracks...'); try { - const response = await fetch('/api/beatport/hero-tracks'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/hero-tracks', signal ? { signal } : undefined); const data = await response.json(); if (data.success && data.tracks && data.tracks.length > 0) { @@ -51066,6 +51189,7 @@ async function loadBeatportHeroTracks() { setupBeatportSliderWithPlaceholders(); } } catch (error) { + if (error && error.name === 'AbortError') return; console.error('โŒ Error loading Beatport tracks:', error); setupBeatportSliderWithPlaceholders(); } @@ -51390,7 +51514,8 @@ async function loadBeatportNewReleases() { try { console.log('๐Ÿ“ก Fetching new releases data...'); - const response = await fetch('/api/beatport/new-releases'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/new-releases', signal ? { signal } : undefined); const data = await response.json(); if (data.success && data.releases && data.releases.length > 0) { @@ -51403,6 +51528,7 @@ async function loadBeatportNewReleases() { return false; } } catch (error) { + if (error && error.name === 'AbortError') return false; console.error('Error loading new releases:', error); showBeatportReleasesError('Failed to load releases'); return false; @@ -51726,7 +51852,8 @@ async function loadBeatportHypePicks() { try { console.log('๐Ÿ”ฅ Fetching hype picks data...'); - const response = await fetch('/api/beatport/hype-picks'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/hype-picks', signal ? { signal } : undefined); const data = await response.json(); if (data.success && data.releases && data.releases.length > 0) { @@ -51739,6 +51866,7 @@ async function loadBeatportHypePicks() { return false; } } catch (error) { + if (error && error.name === 'AbortError') return false; console.error('Error loading hype picks:', error); showBeatportHypePicksError('Failed to load hype picks'); return false; @@ -52064,7 +52192,8 @@ function initializeBeatportChartsSlider() { async function loadBeatportFeaturedCharts() { try { console.log('๐Ÿ“Š Loading featured charts data...'); - const response = await fetch('/api/beatport/featured-charts'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/featured-charts', signal ? { signal } : undefined); const data = await response.json(); if (data.success && data.charts && data.charts.length > 0) { @@ -52076,6 +52205,7 @@ async function loadBeatportFeaturedCharts() { return false; } } catch (error) { + if (error && error.name === 'AbortError') return false; console.error('โŒ Error loading featured charts:', error); return false; } @@ -52358,7 +52488,8 @@ function initializeBeatportDJSlider() { async function loadBeatportDJCharts() { try { console.log('๐ŸŽง Loading DJ charts data...'); - const response = await fetch('/api/beatport/dj-charts'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/dj-charts', signal ? { signal } : undefined); const data = await response.json(); if (data.success && data.charts && data.charts.length > 0) { @@ -52370,6 +52501,7 @@ async function loadBeatportDJCharts() { return false; } } catch (error) { + if (error && error.name === 'AbortError') return false; console.error('โŒ Error loading DJ charts:', error); return false; } @@ -52599,7 +52731,8 @@ function cleanupBeatportDJSlider() { async function loadBeatportTop10Lists() { try { console.log('๐Ÿ† Loading top 10 lists data...'); - const response = await fetch('/api/beatport/homepage/top-10-lists'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/homepage/top-10-lists', signal ? { signal } : undefined); const data = await response.json(); if (data.success) { @@ -52615,6 +52748,7 @@ async function loadBeatportTop10Lists() { return false; } } catch (error) { + if (error && error.name === 'AbortError') return false; console.error('Error loading top 10 lists:', error); showTop10ListsError('Failed to load top 10 lists'); return false; @@ -52749,7 +52883,8 @@ function showTop10ListsError(errorMessage) { async function loadBeatportTop10Releases() { try { console.log('๐Ÿ’ฟ Loading top 10 releases data...'); - const response = await fetch('/api/beatport/homepage/top-10-releases-cards'); + const signal = getBeatportContentSignal(); + const response = await fetch('/api/beatport/homepage/top-10-releases-cards', signal ? { signal } : undefined); const data = await response.json(); if (data.success) { @@ -52762,6 +52897,7 @@ async function loadBeatportTop10Releases() { return false; } } catch (error) { + if (error && error.name === 'AbortError') return false; console.error('Error loading top 10 releases:', error); showTop10ReleasesError('Failed to load top 10 releases'); return false;