Pause Beatport work on tab exit

- Abort Beatport content loading when leaving the Sync/Beatport tab
- Stop Beatport slider autoplay plus discovery and sync subscriptions on exit
- Make Beatport bubble hydration cancellable so hidden tabs do not keep fetching
This commit is contained in:
Antti Kettunen 2026-04-22 08:04:50 +03:00
parent 8dbc819765
commit cc8875f30b
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7

View file

@ -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;