From bf48569c8b63ecedbd32df411f002aaaca895f97 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Thu, 28 Aug 2025 15:23:20 -0700 Subject: [PATCH] sequential sync --- webui/static/script.js | 203 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 2 deletions(-) diff --git a/webui/static/script.js b/webui/static/script.js index 58d58eb2..1367ebfe 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -32,6 +32,145 @@ let activeSyncPollers = {}; // Key: playlist_id, Value: intervalId let playlistTrackCache = {}; // Key: playlist_id, Value: tracks array let spotifyPlaylistsLoaded = false; let activeDownloadProcesses = {}; +let sequentialSyncManager = null; + +// Sequential Sync Manager Class +class SequentialSyncManager { + constructor() { + this.queue = []; + this.currentIndex = 0; + this.isRunning = false; + this.startTime = null; + } + + start(playlistIds) { + if (this.isRunning) { + console.warn('Sequential sync already running'); + return; + } + + // Convert playlist IDs to ordered array (maintain display order) + this.queue = Array.from(playlistIds); + this.currentIndex = 0; + this.isRunning = true; + this.startTime = Date.now(); + + console.log(`🚀 Starting sequential sync for ${this.queue.length} playlists:`, this.queue); + this.updateUI(); + this.syncNext(); + } + + async syncNext() { + if (this.currentIndex >= this.queue.length) { + this.complete(); + return; + } + + const playlistId = this.queue[this.currentIndex]; + const playlist = spotifyPlaylists.find(p => p.id === playlistId); + console.log(`🔄 Sequential sync: Processing playlist ${this.currentIndex + 1}/${this.queue.length}: ${playlist?.name || playlistId}`); + + this.updateUI(); + + try { + // Use existing single sync function + await startPlaylistSync(playlistId); + + // Wait for sync to complete by monitoring the poller + await this.waitForSyncCompletion(playlistId); + + } catch (error) { + console.error(`❌ Sequential sync: Failed to sync playlist ${playlistId}:`, error); + showToast(`Failed to sync "${playlist?.name || playlistId}": ${error.message}`, 'error'); + } + + // Move to next playlist + this.currentIndex++; + setTimeout(() => this.syncNext(), 1000); // Small delay between syncs + } + + async waitForSyncCompletion(playlistId) { + return new Promise((resolve) => { + // Monitor the existing sync poller for completion + const checkCompletion = () => { + if (!activeSyncPollers[playlistId]) { + // Poller stopped = sync completed + resolve(); + return; + } + // Check again in 1 second + setTimeout(checkCompletion, 1000); + }; + checkCompletion(); + }); + } + + complete() { + const duration = ((Date.now() - this.startTime) / 1000).toFixed(1); + const completedCount = this.queue.length; + console.log(`🏁 Sequential sync completed in ${duration}s`); + + this.isRunning = false; + this.queue = []; + this.currentIndex = 0; + this.startTime = null; + + // Re-enable playlist selection + disablePlaylistSelection(false); + + this.updateUI(); + updateRefreshButtonState(); // Refresh button state after completion + showToast(`Sequential sync completed for ${completedCount} playlists in ${duration}s`, 'success'); + } + + cancel() { + if (!this.isRunning) return; + + console.log('🛑 Cancelling sequential sync'); + this.isRunning = false; + this.queue = []; + this.currentIndex = 0; + this.startTime = null; + + // Re-enable playlist selection + disablePlaylistSelection(false); + + this.updateUI(); + updateRefreshButtonState(); // Refresh button state after cancellation + showToast('Sequential sync cancelled', 'info'); + } + + updateUI() { + const startSyncBtn = document.getElementById('start-sync-btn'); + const selectionInfo = document.getElementById('selection-info'); + + if (!this.isRunning) { + // Reset to normal state + if (startSyncBtn) { + startSyncBtn.textContent = 'Start Sync'; + startSyncBtn.disabled = selectedPlaylists.size === 0; + } + if (selectionInfo) { + const count = selectedPlaylists.size; + selectionInfo.textContent = count === 0 + ? 'Select playlists to sync' + : `${count} playlist${count > 1 ? 's' : ''} selected`; + } + } else { + // Show sequential sync status + if (startSyncBtn) { + startSyncBtn.textContent = 'Cancel Sequential Sync'; + startSyncBtn.disabled = false; + } + if (selectionInfo) { + const current = this.currentIndex + 1; + const total = this.queue.length; + const currentPlaylist = spotifyPlaylists.find(p => p.id === this.queue[this.currentIndex]); + selectionInfo.textContent = `Syncing ${current}/${total}: ${currentPlaylist?.name || 'Unknown'}`; + } + } + } +} // API endpoints const API = { @@ -1595,6 +1734,12 @@ function togglePlaylistSelection(event) { } function updateSyncActionsUI() { + // If sequential sync is running, let the manager handle UI updates + if (sequentialSyncManager && sequentialSyncManager.isRunning) { + sequentialSyncManager.updateUI(); + return; + } + const selectionInfo = document.getElementById('selection-info'); const startSyncBtn = document.getElementById('start-sync-btn'); const count = selectedPlaylists.size; @@ -2401,10 +2546,57 @@ function stopSyncPolling(playlistId) { updateRefreshButtonState(); } +// Sequential Sync Functions +function startSequentialSync() { + // Initialize manager if needed + if (!sequentialSyncManager) { + sequentialSyncManager = new SequentialSyncManager(); + } + + // Check if already running - if so, cancel + if (sequentialSyncManager.isRunning) { + sequentialSyncManager.cancel(); + return; + } + + // Validate selection + if (selectedPlaylists.size === 0) { + showToast('No playlists selected for sync', 'error'); + return; + } + + // Get playlist order from DOM to maintain display order + const playlistCards = document.querySelectorAll('.playlist-card'); + const orderedPlaylistIds = []; + + playlistCards.forEach(card => { + const playlistId = card.dataset.playlistId; + if (selectedPlaylists.has(playlistId)) { + orderedPlaylistIds.push(playlistId); + } + }); + + console.log(`🚀 Starting sequential sync for ${orderedPlaylistIds.length} playlists`); + + // Start sequential sync + sequentialSyncManager.start(orderedPlaylistIds); + + // Disable playlist selection during sync + disablePlaylistSelection(true); +} + +function disablePlaylistSelection(disabled) { + const checkboxes = document.querySelectorAll('.playlist-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.disabled = disabled; + }); +} + function hasActiveOperations() { const hasActiveSyncs = Object.keys(activeSyncPollers).length > 0; const hasActiveDownloads = Object.values(activeDownloadProcesses).some(p => p.status === 'running'); - return hasActiveSyncs || hasActiveDownloads; + const hasSequentialSync = sequentialSyncManager && sequentialSyncManager.isRunning; + return hasActiveSyncs || hasActiveDownloads || hasSequentialSync; } @@ -2416,7 +2608,8 @@ function updateRefreshButtonState() { refreshBtn.disabled = true; // Provide context-specific text const hasActiveSyncs = Object.keys(activeSyncPollers).length > 0; - if (hasActiveSyncs) { + const hasSequentialSync = sequentialSyncManager && sequentialSyncManager.isRunning; + if (hasActiveSyncs || hasSequentialSync) { refreshBtn.textContent = '🔄 Syncing...'; } else { refreshBtn.textContent = '📥 Downloading...'; @@ -4703,6 +4896,12 @@ function initializeSyncPage() { refreshBtn.removeEventListener('click', loadSpotifyPlaylists); refreshBtn.addEventListener('click', loadSpotifyPlaylists); } + + // Logic for the Start Sync button + const startSyncBtn = document.getElementById('start-sync-btn'); + if (startSyncBtn) { + startSyncBtn.addEventListener('click', startSequentialSync); + } }