@@ -11403,14 +11425,14 @@ function createAlbumCard(album, confidence) {
${confidencePercent}% match
`;
-
+
// Set background image if available
if (imageUrl) {
card.style.backgroundImage = `url(${imageUrl})`;
card.style.backgroundSize = 'cover';
card.style.backgroundPosition = 'center';
}
-
+
return card;
}
@@ -11422,15 +11444,15 @@ function selectArtist(artist) {
document.querySelectorAll('#artist-manual-results .suggestion-card').forEach(card => {
card.classList.remove('selected');
});
-
+
// Mark new selection
event.currentTarget.classList.add('selected');
-
+
// Store selection
currentMatchingData.selectedArtist = artist;
-
+
console.log('๐ฏ Selected artist:', artist.name);
-
+
if (currentMatchingData.isAlbumDownload) {
// Transition to album selection stage
transitionToAlbumStage();
@@ -11448,15 +11470,15 @@ function selectAlbum(album) {
document.querySelectorAll('#album-manual-results .suggestion-card').forEach(card => {
card.classList.remove('selected');
});
-
+
// Mark new selection
event.currentTarget.classList.add('selected');
-
+
// Store selection
currentMatchingData.selectedAlbum = album;
-
+
console.log('๐ฏ Selected album:', album.name);
-
+
// Enable confirm button
document.getElementById('confirm-match-btn').disabled = false;
}
@@ -11464,34 +11486,34 @@ function selectAlbum(album) {
function transitionToAlbumStage() {
// Hide artist stage
document.getElementById('artist-selection-stage').classList.add('hidden');
-
+
// Show album stage
const albumStage = document.getElementById('album-selection-stage');
albumStage.classList.remove('hidden');
-
+
// Update selected artist name
document.getElementById('selected-artist-name').textContent = currentMatchingData.selectedArtist.name;
-
+
// Update current stage
currentMatchingData.currentStage = 'album';
-
+
// Fetch album suggestions
fetchAlbumSuggestions();
}
function handleArtistSearch(event) {
const query = event.target.value.trim();
-
+
// Clear previous timer
if (searchTimers.artist) {
clearTimeout(searchTimers.artist);
}
-
+
if (query.length < 2) {
document.getElementById('artist-manual-results').innerHTML = '';
return;
}
-
+
// Debounce search
searchTimers.artist = setTimeout(() => {
performArtistSearch(query);
@@ -11500,17 +11522,17 @@ function handleArtistSearch(event) {
function handleAlbumSearch(event) {
const query = event.target.value.trim();
-
+
// Clear previous timer
if (searchTimers.album) {
clearTimeout(searchTimers.album);
}
-
+
if (query.length < 2) {
document.getElementById('album-manual-results').innerHTML = '';
return;
}
-
+
// Debounce search
searchTimers.album = setTimeout(() => {
performAlbumSearch(query);
@@ -11549,10 +11571,10 @@ async function performArtistSearch(query) {
async function performAlbumSearch(query) {
if (!currentMatchingData.selectedArtist) return;
-
+
try {
showLoadingCards('album-manual-results', 'Searching albums...');
-
+
const response = await fetch('/api/match/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -11562,7 +11584,7 @@ async function performAlbumSearch(query) {
artist_id: currentMatchingData.selectedArtist.id
})
});
-
+
const data = await response.json();
if (data.results) {
renderAlbumSearchResults(data.results);
@@ -11578,7 +11600,7 @@ async function performAlbumSearch(query) {
function renderArtistSearchResults(results) {
const container = document.getElementById('artist-manual-results');
container.innerHTML = '';
-
+
results.forEach((result, index) => {
console.log(`Manual search result ${index}:`, result);
console.log(` result.artist:`, result.artist);
@@ -11600,7 +11622,7 @@ function renderArtistSearchResults(results) {
function renderAlbumSearchResults(results) {
const container = document.getElementById('album-manual-results');
container.innerHTML = '';
-
+
results.forEach(result => {
const card = createAlbumCard(result.album, result.confidence);
container.appendChild(card);
@@ -11619,10 +11641,10 @@ function showNoResultsMessage(containerId, message) {
function skipMatching() {
console.log('๐ฏ Skipping matching, proceeding with normal download');
-
+
// Close modal
closeMatchingModal();
-
+
// Start normal download
if (currentMatchingData.isAlbumDownload) {
// For albums, we need to download each track
@@ -12723,10 +12745,10 @@ function resetWishlistModalToIdleState() {
// Reset wishlist modal to idle state after background processing completes
const playlistId = 'wishlist';
const process = activeDownloadProcesses[playlistId];
-
+
if (process) {
console.log('๐ Resetting wishlist modal to idle state...');
-
+
// Reset button states
const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`);
const cancelBtn = document.getElementById(`cancel-all-btn-${playlistId}`);
@@ -12744,30 +12766,30 @@ function resetWishlistModalToIdleState() {
if (forceToggleContainer) {
forceToggleContainer.style.display = 'flex';
}
-
+
// Reset progress displays
const analysisText = document.getElementById(`analysis-progress-text-${playlistId}`);
const analysisBar = document.getElementById(`analysis-progress-fill-${playlistId}`);
const downloadText = document.getElementById(`download-progress-text-${playlistId}`);
const downloadBar = document.getElementById(`download-progress-fill-${playlistId}`);
-
+
if (analysisText) analysisText.textContent = 'Ready to start';
if (analysisBar) analysisBar.style.width = '0%';
if (downloadText) downloadText.textContent = 'Waiting for analysis';
if (downloadBar) downloadBar.style.width = '0%';
-
+
// Reset all track rows to pending state
const trackRows = document.querySelectorAll(`#download-missing-modal-${CSS.escape(playlistId)} tr[data-track-index]`);
trackRows.forEach((row, index) => {
const matchCell = row.querySelector(`#match-${playlistId}-${index}`);
const downloadCell = row.querySelector(`#download-${playlistId}-${index}`);
const actionsCell = row.querySelector(`#actions-${playlistId}-${index}`);
-
+
if (matchCell) matchCell.textContent = '๐ Pending';
if (downloadCell) downloadCell.textContent = '-';
if (actionsCell) actionsCell.innerHTML = '-';
});
-
+
// Reset stats
const foundElement = document.getElementById(`stat-found-${playlistId}`);
const missingElement = document.getElementById(`stat-missing-${playlistId}`);
@@ -12775,7 +12797,7 @@ function resetWishlistModalToIdleState() {
if (foundElement) foundElement.textContent = '-';
if (missingElement) missingElement.textContent = '-';
if (downloadedElement) downloadedElement.textContent = '0';
-
+
// Reset process status
process.status = 'idle';
process.batchId = null;
@@ -12783,7 +12805,7 @@ function resetWishlistModalToIdleState() {
clearInterval(process.poller);
process.poller = null;
}
-
+
console.log('โ Wishlist modal fully reset to idle state');
} else {
console.log('โ ๏ธ No wishlist process found to reset');
@@ -12802,10 +12824,10 @@ async function loadDashboardData() {
if (metadataButton) {
metadataButton.addEventListener('click', handleMetadataUpdateButtonClick);
}
-
+
// Check active media server and hide metadata updater if not Plex
await checkAndHideMetadataUpdaterForNonPlex();
-
+
// Check for ongoing metadata update and restore state
await checkAndRestoreMetadataUpdateState();
@@ -12841,32 +12863,32 @@ async function loadDashboardData() {
// Initial load of stats
await fetchAndUpdateDbStats();
-
+
// Start periodic refresh of stats (every 30 seconds)
stopDbStatsPolling(); // Ensure no duplicates
dbStatsInterval = setInterval(fetchAndUpdateDbStats, 30000);
// Initial load of wishlist count
await updateWishlistCount();
-
+
// Start periodic refresh of wishlist count (every 30 seconds, matching GUI behavior)
stopWishlistCountPolling(); // Ensure no duplicates
wishlistCountInterval = setInterval(updateWishlistCount, 30000);
-
+
// Initial load of service status and system statistics
await fetchAndUpdateServiceStatus();
await fetchAndUpdateSystemStats();
-
+
// Service status is already polled globally (line 311)
// System stats polling kept here (dashboard-specific)
setInterval(fetchAndUpdateSystemStats, 10000);
-
+
// Initial load of activity feed
await fetchAndUpdateActivityFeed();
-
+
// Start periodic refresh of activity feed (every 5 seconds for responsiveness)
setInterval(fetchAndUpdateActivityFeed, 5000);
-
+
// Start periodic toast checking (every 3 seconds)
setInterval(checkForActivityToasts, 3000);
@@ -12881,7 +12903,7 @@ async function loadDashboardData() {
// Check for any active download processes that need rehydration
await checkForActiveProcesses();
-
+
// Automatic wishlist processing now runs server-side
}
@@ -12891,7 +12913,7 @@ async function fetchAndUpdateDbStats() {
try {
const response = await fetch('/api/database/stats');
if (!response.ok) return;
-
+
const stats = await response.json();
// This function updates the stat cards in the top grid
@@ -12933,7 +12955,7 @@ function updateDbUpdaterCardInfo(stats) {
if (albumsStatEl) albumsStatEl.textContent = stats.albums.toLocaleString() || '0';
if (tracksStatEl) tracksStatEl.textContent = stats.tracks.toLocaleString() || '0';
if (sizeStatEl) sizeStatEl.textContent = `${stats.database_size_mb.toFixed(2)} MB`;
-
+
// Update the title of the tool card to show which server is active
const toolCardTitle = document.querySelector('#db-updater-card .tool-card-title');
if (toolCardTitle && stats.server_source) {
@@ -12948,14 +12970,14 @@ async function updateWishlistCount() {
try {
const response = await fetch('/api/wishlist/count');
if (!response.ok) return;
-
+
const data = await response.json();
const count = data.count || 0;
-
+
const wishlistButton = document.getElementById('wishlist-button');
if (wishlistButton) {
wishlistButton.textContent = `๐ต Wishlist (${count})`;
-
+
// Update button styling based on count (matching GUI behavior)
if (count === 0) {
wishlistButton.classList.remove('wishlist-active');
@@ -12965,10 +12987,10 @@ async function updateWishlistCount() {
wishlistButton.classList.add('wishlist-active');
}
}
-
+
// Check for auto-initiated wishlist processes that user should see immediately
await checkForAutoInitiatedWishlistProcess();
-
+
} catch (error) {
console.warn('Could not fetch wishlist count:', error);
}
@@ -12977,44 +12999,44 @@ async function updateWishlistCount() {
async function checkForAutoInitiatedWishlistProcess() {
try {
const playlistId = 'wishlist';
-
+
// Only check if we're on the dashboard and no modal is currently visible
if (currentPage !== 'dashboard') {
return;
}
-
+
// Don't override if user has manually closed the modal during auto-processing
if (WishlistModalState.wasUserClosed()) {
return;
}
-
+
// Check for active wishlist processes
const response = await fetch('/api/active-processes');
if (!response.ok) return;
-
+
const data = await response.json();
const processes = data.active_processes || [];
const serverWishlistProcess = processes.find(p => p.playlist_id === playlistId);
const clientWishlistProcess = activeDownloadProcesses[playlistId];
-
+
if (serverWishlistProcess && serverWishlistProcess.auto_initiated) {
console.log('๐ค [Auto-Processing] Detected auto-initiated wishlist process during polling');
-
+
// Only sync frontend state if needed, but don't auto-show modal
- const needsSync = !clientWishlistProcess ||
+ const needsSync = !clientWishlistProcess ||
clientWishlistProcess.batchId !== serverWishlistProcess.batch_id ||
!clientWishlistProcess.modalElement ||
!document.body.contains(clientWishlistProcess.modalElement);
-
+
if (needsSync) {
console.log('๐ [Auto-Processing] Syncing frontend state for auto-processing (background mode)');
await rehydrateModal(serverWishlistProcess, false); // Background sync only
}
-
+
// Note: Modal visibility is controlled by user interaction only
// User must click wishlist button to see auto-processing progress
}
-
+
} catch (error) {
console.warn('Error checking for auto-initiated wishlist process:', error);
}
@@ -13073,9 +13095,9 @@ function updateDbProgressUI(state) {
phaseLabel.textContent = state.phase || 'Idle';
progressBar.style.backgroundColor = '#1db954'; // Green for normal
}
-
+
if (state.status === 'finished' || state.status === 'error') {
- // Final stats refresh after completion/error
+ // Final stats refresh after completion/error
setTimeout(fetchAndUpdateDbStats, 500);
}
}
@@ -13088,7 +13110,7 @@ function updateDbProgressUI(state) {
async function loadTidalPlaylists() {
const container = document.getElementById('tidal-playlist-container');
const refreshBtn = document.getElementById('tidal-refresh-btn');
-
+
container.innerHTML = `
๐ Loading Tidal playlists...
`;
refreshBtn.disabled = true;
refreshBtn.textContent = '๐ Loading...';
@@ -13099,13 +13121,13 @@ async function loadTidalPlaylists() {
const error = await response.json();
throw new Error(error.error || 'Failed to fetch Tidal playlists');
}
-
+
tidalPlaylists = await response.json();
renderTidalPlaylists();
tidalPlaylistsLoaded = true;
console.log(`๐ต Loaded ${tidalPlaylists.length} Tidal playlists`);
-
+
// Load and apply saved discovery states from backend (like YouTube)
await loadTidalPlaylistStatesFromBackend();
@@ -13133,10 +13155,10 @@ function renderTidalPlaylists() {
playlist: p
};
}
-
+
return createTidalCard(p);
}).join('');
-
+
// Add click handlers to cards
tidalPlaylists.forEach(p => {
const card = document.getElementById(`tidal-card-${p.id}`);
@@ -13149,12 +13171,12 @@ function renderTidalPlaylists() {
function createTidalCard(playlist) {
const state = tidalPlaylistStates[playlist.id];
const phase = state.phase;
-
+
// Get phase-specific button text (like YouTube cards)
let buttonText = getActionButtonText(phase);
let phaseText = getPhaseText(phase);
let phaseColor = getPhaseColor(phase);
-
+
return `
๐ต
@@ -13181,38 +13203,38 @@ async function handleTidalCardClick(playlistId) {
showToast('Playlist state not found - try refreshing the page', 'error');
return;
}
-
+
// Validate required state data
if (!state.playlist) {
console.error(`โ [Card Click] No playlist data found for Tidal playlist: ${playlistId}`);
showToast('Playlist data missing - try refreshing the page', 'error');
return;
}
-
+
// Validate phase
if (!state.phase) {
console.warn(`โ ๏ธ [Card Click] No phase set for Tidal playlist ${playlistId} - defaulting to 'fresh'`);
state.phase = 'fresh';
}
-
+
console.log(`๐ต [Card Click] Tidal card clicked: ${playlistId}, Phase: ${state.phase}`);
-
+
if (state.phase === 'fresh') {
// No need to fetch data - we already have all tracks from initial load (like sync.py)
console.log(`๐ต Using pre-loaded Tidal playlist data for: ${state.playlist.name}`);
console.log(`๐ต Ready with ${state.playlist.tracks.length} Tidal tracks for discovery`);
-
+
// Open discovery modal - phase will be updated when discovery actually starts
openTidalDiscoveryModal(playlistId, state.playlist);
-
+
} else if (state.phase === 'discovering' || state.phase === 'discovered' || state.phase === 'syncing' || state.phase === 'sync_complete') {
// Reopen existing modal with preserved discovery results (like GUI sync.py)
console.log(`๐ต [Card Click] Opening Tidal discovery modal for ${state.phase} phase`);
-
+
// Validate that we have discovery results to show
if (state.phase === 'discovered' && (!state.discovery_results || state.discovery_results.length === 0)) {
console.warn(`โ ๏ธ [Card Click] Discovered phase but no discovery results found - attempting to reload from backend`);
-
+
// Try to fetch from backend as fallback
try {
const stateResponse = await fetch(`/api/tidal/state/${playlistId}`);
@@ -13223,7 +13245,7 @@ async function handleTidalCardClick(playlistId) {
state.discovery_results = fullState.discovery_results;
state.spotify_matches = fullState.spotify_matches || state.spotify_matches;
state.discovery_progress = fullState.discovery_progress || state.discovery_progress;
- tidalPlaylistStates[playlistId] = {...tidalPlaylistStates[playlistId], ...state};
+ tidalPlaylistStates[playlistId] = { ...tidalPlaylistStates[playlistId], ...state };
console.log(`โ [Card Click] Restored ${fullState.discovery_results.length} discovery results from backend`);
}
}
@@ -13231,7 +13253,7 @@ async function handleTidalCardClick(playlistId) {
console.error(`โ [Card Click] Failed to fetch discovery results from backend: ${error}`);
}
}
-
+
openTidalDiscoveryModal(playlistId, state.playlist);
} else if (state.phase === 'downloading' || state.phase === 'download_complete') {
// Open download modal if we have the converted playlist ID
@@ -13255,7 +13277,7 @@ async function handleTidalCardClick(playlistId) {
} else {
console.error('โ [Card Click] No converted Spotify playlist ID found for Tidal download modal');
console.log('๐ [Card Click] Available state data:', Object.keys(state));
-
+
// Fallback: try to open discovery modal if we have discovery results
if (state.discovery_results && state.discovery_results.length > 0) {
console.log(`๐ [Card Click] Fallback: Opening discovery modal with ${state.discovery_results.length} results`);
@@ -13275,9 +13297,9 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
showToast('Cannot open download modal - invalid playlist data', 'error');
return;
}
-
+
console.log(`๐ง [Rehydration] Rehydrating Tidal download modal for: ${state.playlist.name}`);
-
+
// Get discovery results from backend if not already loaded
if (!state.discovery_results) {
console.log(`๐ Fetching discovery results from backend for Tidal playlist: ${playlistId}`);
@@ -13294,7 +13316,7 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
return;
}
}
-
+
// Extract Spotify tracks from discovery results
const spotifyTracks = [];
for (const result of state.discovery_results) {
@@ -13302,34 +13324,34 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
spotifyTracks.push(result.spotify_data);
}
}
-
+
if (spotifyTracks.length === 0) {
console.error('โ No Spotify tracks found for download modal');
showToast('No Spotify matches found for download', 'error');
return;
}
-
+
const virtualPlaylistId = state.convertedSpotifyPlaylistId;
const playlistName = state.playlist.name;
// Create the download modal
await openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, spotifyTracks);
-
+
// If we have a download process ID, set up the modal for the running state
if (state.download_process_id) {
const process = activeDownloadProcesses[virtualPlaylistId];
if (process) {
process.status = state.phase === 'download_complete' ? 'complete' : 'running';
process.batchId = state.download_process_id;
-
+
// Update UI based on phase
const beginBtn = document.getElementById(`begin-analysis-btn-${virtualPlaylistId}`);
const cancelBtn = document.getElementById(`cancel-all-btn-${virtualPlaylistId}`);
-
+
if (state.phase === 'downloading') {
if (beginBtn) beginBtn.style.display = 'none';
if (cancelBtn) cancelBtn.style.display = 'inline-block';
-
+
// Start polling for live updates
startModalDownloadPolling(virtualPlaylistId);
console.log(`๐ Started polling for active Tidal download: ${state.download_process_id}`);
@@ -13337,7 +13359,7 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
if (beginBtn) beginBtn.style.display = 'none';
if (cancelBtn) cancelBtn.style.display = 'none';
console.log(`โ Showing completed Tidal download results: ${state.download_process_id}`);
-
+
// For completed downloads, fetch the final results once to populate the modal
try {
const response = await fetch(`/api/playlists/${state.download_process_id}/download_status`);
@@ -13361,9 +13383,9 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
}
}
}
-
+
console.log(`โ Successfully rehydrated Tidal download modal for: ${state.playlist.name}`);
-
+
} catch (error) {
console.error(`โ Error rehydrating Tidal download modal:`, error);
showToast('Error opening download modal', 'error');
@@ -13376,32 +13398,32 @@ function updateCompletedModalResults(playlistId, downloadData) {
* This reuses the existing status polling logic but applies it once for completed state
*/
console.log(`๐ [Completed Results] Updating modal ${playlistId} with final download results`);
-
+
// Validate input data
if (!downloadData || !downloadData.tasks) {
console.error(`โ [Completed Results] Invalid download data for playlist ${playlistId}:`, downloadData);
return;
}
-
+
try {
// Update analysis progress to 100%
const analysisProgressFill = document.getElementById(`analysis-progress-fill-${playlistId}`);
const analysisProgressText = document.getElementById(`analysis-progress-text-${playlistId}`);
if (analysisProgressFill) analysisProgressFill.style.width = '100%';
if (analysisProgressText) analysisProgressText.textContent = 'Analysis complete!';
-
+
// Update analysis results and stats
if (downloadData.analysis_results) {
updateTrackAnalysisResults(playlistId, downloadData.analysis_results);
const foundCount = downloadData.analysis_results.filter(r => r.found).length;
const missingCount = downloadData.analysis_results.filter(r => !r.found).length;
-
+
const statFound = document.getElementById(`stat-found-${playlistId}`);
const statMissing = document.getElementById(`stat-missing-${playlistId}`);
if (statFound) statFound.textContent = foundCount;
if (statMissing) statMissing.textContent = missingCount;
}
-
+
// Process completed tasks to update individual track statuses
const missingTracks = (downloadData.analysis_results || []).filter(r => !r.found);
let completedCount = 0;
@@ -13410,11 +13432,11 @@ function updateCompletedModalResults(playlistId, downloadData) {
(downloadData.tasks || []).forEach(task => {
const row = document.querySelector(`#download-missing-modal-${CSS.escape(playlistId)} tr[data-track-index="${task.track_index}"]`);
if (!row) return;
-
+
row.dataset.taskId = task.task_id;
const statusEl = document.getElementById(`download-${playlistId}-${task.track_index}`);
const actionsEl = document.getElementById(`actions-${playlistId}-${task.track_index}`);
-
+
let statusText = '';
switch (task.status) {
case 'pending': statusText = 'โธ๏ธ Pending'; break;
@@ -13426,7 +13448,7 @@ function updateCompletedModalResults(playlistId, downloadData) {
case 'cancelled': statusText = '๐ซ Cancelled'; failedOrCancelledCount++; break;
default: statusText = `โช ${task.status}`; break;
}
-
+
if (statusEl) statusEl.textContent = statusText;
if (actionsEl) actionsEl.innerHTML = '-'; // Remove action buttons for completed tasks
});
@@ -13435,17 +13457,17 @@ function updateCompletedModalResults(playlistId, downloadData) {
const totalFinished = completedCount + failedOrCancelledCount;
const missingCount = missingTracks.length;
const progressPercent = missingCount > 0 ? (totalFinished / missingCount) * 100 : 100;
-
+
const downloadProgressFill = document.getElementById(`download-progress-fill-${playlistId}`);
const downloadProgressText = document.getElementById(`download-progress-text-${playlistId}`);
const statDownloaded = document.getElementById(`stat-downloaded-${playlistId}`);
-
+
if (downloadProgressFill) downloadProgressFill.style.width = `${progressPercent}%`;
if (downloadProgressText) downloadProgressText.textContent = `${completedCount}/${missingCount} completed (${progressPercent.toFixed(0)}%)`;
if (statDownloaded) statDownloaded.textContent = completedCount;
-
+
console.log(`โ [Completed Results] Updated modal with ${completedCount} completed, ${failedOrCancelledCount} failed tasks`);
-
+
} catch (error) {
console.error(`โ [Completed Results] Error updating completed modal results:`, error);
}
@@ -13454,29 +13476,29 @@ function updateCompletedModalResults(playlistId, downloadData) {
function updateTidalCardPhase(playlistId, phase) {
const state = tidalPlaylistStates[playlistId];
if (!state) return;
-
+
state.phase = phase;
-
+
// Re-render the card with new phase
const card = document.getElementById(`tidal-card-${playlistId}`);
if (card) {
const oldButtonText = card.querySelector('.playlist-card-action-btn')?.textContent || 'unknown';
const newCardHtml = createTidalCard(state.playlist);
card.outerHTML = newCardHtml;
-
+
// Verify the card was actually updated
const updatedCard = document.getElementById(`tidal-card-${playlistId}`);
const newButtonText = updatedCard?.querySelector('.playlist-card-action-btn')?.textContent || 'unknown';
-
+
console.log(`๐ [Card Update] Re-rendered Tidal card ${playlistId}:`);
console.log(` ๐ Phase: ${phase}`);
console.log(` ๐ Button text: "${oldButtonText}" โ "${newButtonText}"`);
console.log(` โ Expected: "${getActionButtonText(phase)}"`);
-
+
if (newButtonText !== getActionButtonText(phase)) {
console.error(`โ [Card Update] Button text mismatch! Expected "${getActionButtonText(phase)}", got "${newButtonText}"`);
}
-
+
// Re-attach click handler
const newCard = document.getElementById(`tidal-card-${playlistId}`);
if (newCard) {
@@ -13485,7 +13507,7 @@ function updateTidalCardPhase(playlistId, phase) {
} else {
console.error(`โ [Card Update] Failed to find new card after rendering: tidal-card-${playlistId}`);
}
-
+
// If we have sync progress and we're in sync/sync_complete phase, restore it
if ((phase === 'syncing' || phase === 'sync_complete') && state.lastSyncProgress) {
setTimeout(() => {
@@ -13493,21 +13515,21 @@ function updateTidalCardPhase(playlistId, phase) {
}, 0);
}
}
-
+
console.log(`๐ต Updated Tidal card phase: ${playlistId} -> ${phase}`);
}
async function openTidalDiscoveryModal(playlistId, playlistData) {
console.log(`๐ต Opening Tidal discovery modal (reusing YouTube modal): ${playlistData.name}`);
-
+
// Create a fake YouTube-style urlHash for the modal system
const fakeUrlHash = `tidal_${playlistId}`;
-
+
// Get current Tidal card state to check if discovery is already done or in progress
const tidalCardState = tidalPlaylistStates[playlistId];
const isAlreadyDiscovered = tidalCardState && (tidalCardState.phase === 'discovered' || tidalCardState.phase === 'syncing' || tidalCardState.phase === 'sync_complete');
const isCurrentlyDiscovering = tidalCardState && tidalCardState.phase === 'discovering';
-
+
// Prepare discovery results in the correct format for modal
let transformedResults = [];
let actualMatches = 0;
@@ -13515,10 +13537,10 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
transformedResults = tidalCardState.discovery_results.map((result, index) => {
// Check multiple status formats
const isFound = result.status === 'found' ||
- result.status === 'โ Found' ||
- result.status_class === 'found' ||
- result.spotify_data ||
- result.spotify_track;
+ result.status === 'โ Found' ||
+ result.status_class === 'found' ||
+ result.spotify_data ||
+ result.spotify_track;
if (isFound) actualMatches++;
return {
@@ -13538,7 +13560,7 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
});
console.log(`๐ต Tidal modal: Calculated ${actualMatches} matches from ${transformedResults.length} results`);
}
-
+
// Create YouTube-compatible state structure
const modalPhase = tidalCardState ? tidalCardState.phase : 'fresh';
youtubePlaylistStates[fakeUrlHash] = {
@@ -13557,37 +13579,37 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
discoveryResults: transformedResults, // Both formats for compatibility
discoveryProgress: isAlreadyDiscovered ? 100 : 0 // Frontend format for modal progress display
};
-
+
// Only start discovery if not already discovered AND not currently discovering
if (!isAlreadyDiscovered && !isCurrentlyDiscovering) {
// Start Tidal discovery process automatically (like sync.py)
try {
console.log(`๐ Starting Tidal discovery for: ${playlistData.name}`);
-
+
const response = await fetch(`/api/tidal/discovery/start/${playlistId}`, {
method: 'POST'
});
-
+
const result = await response.json();
-
+
if (result.error) {
console.error('โ Error starting Tidal discovery:', result.error);
showToast(`Error starting discovery: ${result.error}`, 'error');
return;
}
-
+
console.log('โ Tidal discovery started, beginning polling...');
-
+
// Update phase to discovering now that backend discovery is actually started
tidalPlaylistStates[playlistId].phase = 'discovering';
updateTidalCardPhase(playlistId, 'discovering');
-
+
// Update modal phase to match
youtubePlaylistStates[fakeUrlHash].phase = 'discovering';
-
+
// Start polling for progress
startTidalDiscoveryPolling(fakeUrlHash, playlistId);
-
+
} catch (error) {
console.error('โ Error starting Tidal discovery:', error);
showToast(`Error starting discovery: ${error.message}`, 'error');
@@ -13603,31 +13625,31 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
} else {
console.log('โ Using existing results - no need to re-discover');
}
-
+
// Reuse YouTube discovery modal (exact sync.py pattern)
openYouTubeDiscoveryModal(fakeUrlHash);
}
function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
console.log(`๐ Starting Tidal discovery polling for: ${playlistId}`);
-
+
// Stop any existing polling
if (activeYouTubePollers[fakeUrlHash]) {
clearInterval(activeYouTubePollers[fakeUrlHash]);
}
-
+
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/tidal/discovery/status/${playlistId}`);
const status = await response.json();
-
+
if (status.error) {
console.error('โ Error polling Tidal discovery status:', status.error);
clearInterval(pollInterval);
delete activeYouTubePollers[fakeUrlHash];
return;
}
-
+
// Transform Tidal results to YouTube modal format first
const transformedStatus = {
progress: status.progress,
@@ -13635,10 +13657,10 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
spotify_total: status.spotify_total,
results: status.results.map((result, index) => {
const isFound = result.status === 'found' ||
- result.status === 'โ Found' ||
- result.status_class === 'found' ||
- result.spotify_data ||
- result.spotify_track;
+ result.status === 'โ Found' ||
+ result.status_class === 'found' ||
+ result.spotify_data ||
+ result.spotify_track;
return {
index: index,
@@ -13656,7 +13678,7 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
};
})
};
-
+
// Update fake YouTube state with Tidal discovery results
const state = youtubePlaylistStates[fakeUrlHash];
if (state) {
@@ -13667,10 +13689,10 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
state.discovery_results = status.results; // Backend format
state.discoveryResults = transformedStatus.results; // Frontend format - for button logic
state.phase = status.phase;
-
+
// Update modal with transformed data (reuse YouTube modal update logic)
updateYouTubeDiscoveryModal(fakeUrlHash, transformedStatus);
-
+
// Update Tidal card phase and save discovery results FIRST
if (tidalPlaylistStates[playlistId]) {
tidalPlaylistStates[playlistId].phase = status.phase;
@@ -13679,27 +13701,27 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
tidalPlaylistStates[playlistId].discovery_progress = status.progress;
updateTidalCardPhase(playlistId, status.phase);
}
-
+
// Update Tidal card progress AFTER phase update to avoid being overwritten
updateTidalCardProgress(playlistId, status);
-
+
console.log(`๐ Tidal discovery progress: ${status.progress}% (${status.spotify_matches}/${status.spotify_total} found)`);
}
-
+
// Stop polling when complete
if (status.complete) {
console.log(`โ Tidal discovery complete: ${status.spotify_matches}/${status.spotify_total} tracks found`);
clearInterval(pollInterval);
delete activeYouTubePollers[fakeUrlHash];
}
-
+
} catch (error) {
console.error('โ Error polling Tidal discovery:', error);
clearInterval(pollInterval);
delete activeYouTubePollers[fakeUrlHash];
}
}, 1000); // Poll every second like YouTube
-
+
// Store poller reference (reuse YouTube poller storage)
activeYouTubePollers[fakeUrlHash] = pollInterval;
}
@@ -13708,35 +13730,35 @@ async function loadTidalPlaylistStatesFromBackend() {
// Load all stored Tidal playlist discovery states from backend (similar to YouTube hydration)
try {
console.log('๐ต Loading Tidal playlist states from backend...');
-
+
const response = await fetch('/api/tidal/playlists/states');
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to fetch Tidal playlist states');
}
-
+
const data = await response.json();
const states = data.states || [];
-
+
console.log(`๐ต Found ${states.length} stored Tidal playlist states in backend`);
-
+
if (states.length === 0) {
console.log('๐ต No Tidal playlist states to hydrate');
return;
}
-
+
// Apply states to existing playlist cards
for (const stateInfo of states) {
await applyTidalPlaylistState(stateInfo);
}
-
+
// Rehydrate download modals for Tidal playlists in downloading/download_complete phases
for (const stateInfo of states) {
- if ((stateInfo.phase === 'downloading' || stateInfo.phase === 'download_complete') &&
+ if ((stateInfo.phase === 'downloading' || stateInfo.phase === 'download_complete') &&
stateInfo.converted_spotify_playlist_id && stateInfo.download_process_id) {
-
+
const convertedPlaylistId = stateInfo.converted_spotify_playlist_id;
-
+
if (!activeDownloadProcesses[convertedPlaylistId]) {
console.log(`๐ง Rehydrating download modal for Tidal playlist: ${stateInfo.playlist_id}`);
try {
@@ -13746,34 +13768,34 @@ async function loadTidalPlaylistStatesFromBackend() {
console.warn(`โ ๏ธ Playlist data not found for rehydration: ${stateInfo.playlist_id}`);
continue;
}
-
+
// Create the download modal using the Tidal-specific function
const spotifyTracks = tidalPlaylistStates[stateInfo.playlist_id]?.discovery_results
?.filter(result => result.spotify_data)
?.map(result => result.spotify_data) || [];
-
+
if (spotifyTracks.length > 0) {
await openDownloadMissingModalForTidal(
convertedPlaylistId,
playlistData.name,
spotifyTracks
);
-
+
// Set the modal to running state with the correct batch ID
const process = activeDownloadProcesses[convertedPlaylistId];
if (process) {
process.status = 'running';
process.batchId = stateInfo.download_process_id;
-
+
// Update UI to running state
const beginBtn = document.getElementById(`begin-analysis-btn-${convertedPlaylistId}`);
const cancelBtn = document.getElementById(`cancel-all-btn-${convertedPlaylistId}`);
if (beginBtn) beginBtn.style.display = 'none';
if (cancelBtn) cancelBtn.style.display = 'inline-block';
-
+
// Start polling for this process
startModalDownloadPolling(convertedPlaylistId);
-
+
console.log(`โ Rehydrated Tidal download modal for batch ${stateInfo.download_process_id}`);
}
} else {
@@ -13785,9 +13807,9 @@ async function loadTidalPlaylistStatesFromBackend() {
}
}
}
-
+
console.log('โ Tidal playlist states loaded and applied');
-
+
} catch (error) {
console.error('โ Error loading Tidal playlist states:', error);
}
@@ -13795,17 +13817,17 @@ async function loadTidalPlaylistStatesFromBackend() {
async function applyTidalPlaylistState(stateInfo) {
const { playlist_id, phase, discovery_progress, spotify_matches, discovery_results, converted_spotify_playlist_id, download_process_id } = stateInfo;
-
+
try {
console.log(`๐ต Applying saved state for Tidal playlist: ${playlist_id}, Phase: ${phase}`);
-
+
// Find the playlist data from the loaded playlists
const playlistData = tidalPlaylists.find(p => p.id === playlist_id);
if (!playlistData) {
console.warn(`โ ๏ธ Playlist data not found for state ${playlist_id} - skipping`);
return;
}
-
+
// Update local state
if (!tidalPlaylistStates[playlist_id]) {
// Initialize state if it doesn't exist
@@ -13814,7 +13836,7 @@ async function applyTidalPlaylistState(stateInfo) {
phase: 'fresh'
};
}
-
+
// Update with backend state
tidalPlaylistStates[playlist_id].phase = phase;
tidalPlaylistStates[playlist_id].discovery_progress = discovery_progress;
@@ -13823,7 +13845,7 @@ async function applyTidalPlaylistState(stateInfo) {
tidalPlaylistStates[playlist_id].convertedSpotifyPlaylistId = converted_spotify_playlist_id;
tidalPlaylistStates[playlist_id].download_process_id = download_process_id;
tidalPlaylistStates[playlist_id].playlist = playlistData; // Ensure playlist data is set
-
+
// Fetch full discovery results for non-fresh playlists (matching YouTube pattern)
if (phase !== 'fresh' && phase !== 'discovering') {
try {
@@ -13832,7 +13854,7 @@ async function applyTidalPlaylistState(stateInfo) {
if (stateResponse.ok) {
const fullState = await stateResponse.json();
console.log(`๐ Retrieved full Tidal state with ${fullState.discovery_results?.length || 0} discovery results`);
-
+
// Store full discovery results in local state (matching YouTube pattern)
if (fullState.discovery_results && tidalPlaylistStates[playlist_id]) {
tidalPlaylistStates[playlist_id].discovery_results = fullState.discovery_results;
@@ -13849,10 +13871,10 @@ async function applyTidalPlaylistState(stateInfo) {
console.warn(`โ ๏ธ Error fetching full discovery results for Tidal playlist ${playlistData.name}:`, error.message);
}
}
-
+
// Update the card UI to reflect the saved state
updateTidalCardPhase(playlist_id, phase);
-
+
// Update card progress if we have discovery results
if (phase === 'discovered' && tidalPlaylistStates[playlist_id]) {
const progressInfo = {
@@ -13872,9 +13894,9 @@ async function applyTidalPlaylistState(stateInfo) {
const fakeUrlHash = `tidal_${playlist_id}`;
startTidalSyncPolling(fakeUrlHash);
}
-
+
console.log(`โ Applied saved state for Tidal playlist: ${playlist_id} -> ${phase}`);
-
+
} catch (error) {
console.error(`โ Error applying Tidal playlist state for ${playlist_id}:`, error);
}
@@ -13883,21 +13905,21 @@ async function applyTidalPlaylistState(stateInfo) {
function updateTidalCardProgress(playlistId, progress) {
const state = tidalPlaylistStates[playlistId];
if (!state) return;
-
+
const card = document.getElementById(`tidal-card-${playlistId}`);
if (!card) return;
-
+
const progressElement = card.querySelector('.playlist-card-progress');
if (!progressElement) return;
-
+
const total = progress.spotify_total || 0;
const matches = progress.spotify_matches || 0;
const failed = total - matches;
const percentage = total > 0 ? Math.round((matches / total) * 100) : 0;
-
+
progressElement.textContent = `โช ${total} / โ ${matches} / โ ${failed} / ${percentage}%`;
progressElement.classList.remove('hidden'); // Show progress during discovery
-
+
console.log('๐ต Updated Tidal card progress:', playlistId, `${matches}/${total} (${percentage}%)`);
}
@@ -13908,36 +13930,36 @@ function updateTidalCardProgress(playlistId, progress) {
async function startTidalPlaylistSync(urlHash) {
try {
console.log('๐ต Starting Tidal playlist sync:', urlHash);
-
+
const state = youtubePlaylistStates[urlHash];
if (!state || !state.is_tidal_playlist) {
console.error('โ Invalid Tidal playlist state for sync');
return;
}
-
+
const playlistId = state.tidal_playlist_id;
const response = await fetch(`/api/tidal/sync/start/${playlistId}`, {
method: 'POST'
});
-
+
const result = await response.json();
-
+
if (result.error) {
showToast(`Error starting sync: ${result.error}`, 'error');
return;
}
-
+
// Update card and modal to syncing phase
updateTidalCardPhase(playlistId, 'syncing');
-
+
// Update modal buttons if modal is open
updateTidalModalButtons(urlHash, 'syncing');
-
+
// Start sync polling
startTidalSyncPolling(urlHash);
-
+
showToast('Tidal playlist sync started!', 'success');
-
+
} catch (error) {
console.error('โ Error starting Tidal sync:', error);
showToast(`Error starting sync: ${error.message}`, 'error');
@@ -13949,7 +13971,7 @@ function startTidalSyncPolling(urlHash) {
if (activeYouTubePollers[urlHash]) {
clearInterval(activeYouTubePollers[urlHash]);
}
-
+
const state = youtubePlaylistStates[urlHash];
const playlistId = state.tidal_playlist_id;
@@ -13958,25 +13980,25 @@ function startTidalSyncPolling(urlHash) {
try {
const response = await fetch(`/api/tidal/sync/status/${playlistId}`);
const status = await response.json();
-
+
if (status.error) {
console.error('โ Error polling Tidal sync status:', status.error);
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
return;
}
-
+
// Update card progress with sync stats
updateTidalCardSyncProgress(playlistId, status.progress);
-
+
// Update modal sync display if open
updateTidalModalSyncProgress(urlHash, status.progress);
-
+
// Check if complete
if (status.complete) {
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
-
+
// Update both states to sync_complete
if (tidalPlaylistStates[playlistId]) {
tidalPlaylistStates[playlistId].phase = 'sync_complete';
@@ -13984,19 +14006,19 @@ function startTidalSyncPolling(urlHash) {
if (youtubePlaylistStates[urlHash]) {
youtubePlaylistStates[urlHash].phase = 'sync_complete';
}
-
+
// Update card phase to sync complete
updateTidalCardPhase(playlistId, 'sync_complete');
-
+
// Update modal buttons
updateTidalModalButtons(urlHash, 'sync_complete');
-
+
console.log('โ Tidal sync complete:', urlHash);
showToast('Tidal playlist sync complete!', 'success');
} else if (status.sync_status === 'error') {
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
-
+
// Update both states to discovered (revert on error)
if (tidalPlaylistStates[playlistId]) {
tidalPlaylistStates[playlistId].phase = 'discovered';
@@ -14004,14 +14026,14 @@ function startTidalSyncPolling(urlHash) {
if (youtubePlaylistStates[urlHash]) {
youtubePlaylistStates[urlHash].phase = 'discovered';
}
-
+
// Revert to discovered phase on error
updateTidalCardPhase(playlistId, 'discovered');
updateTidalModalButtons(urlHash, 'discovered');
-
+
showToast(`Sync failed: ${status.error || 'Unknown error'}`, 'error');
}
-
+
} catch (error) {
console.error('โ Error polling Tidal sync:', error);
if (activeYouTubePollers[urlHash]) {
@@ -14032,37 +14054,37 @@ function startTidalSyncPolling(urlHash) {
async function cancelTidalSync(urlHash) {
try {
console.log('โ Cancelling Tidal sync:', urlHash);
-
+
const state = youtubePlaylistStates[urlHash];
if (!state || !state.is_tidal_playlist) {
console.error('โ Invalid Tidal playlist state');
return;
}
-
+
const playlistId = state.tidal_playlist_id;
const response = await fetch(`/api/tidal/sync/cancel/${playlistId}`, {
method: 'POST'
});
-
+
const result = await response.json();
-
+
if (result.error) {
showToast(`Error cancelling sync: ${result.error}`, 'error');
return;
}
-
+
// Stop polling
if (activeYouTubePollers[urlHash]) {
clearInterval(activeYouTubePollers[urlHash]);
delete activeYouTubePollers[urlHash];
}
-
+
// Revert to discovered phase
updateTidalCardPhase(playlistId, 'discovered');
updateTidalModalButtons(urlHash, 'discovered');
-
+
showToast('Tidal sync cancelled', 'info');
-
+
} catch (error) {
console.error('โ Error cancelling Tidal sync:', error);
showToast(`Error cancelling sync: ${error.message}`, 'error');
@@ -14072,15 +14094,15 @@ async function cancelTidalSync(urlHash) {
function updateTidalCardSyncProgress(playlistId, progress) {
const state = tidalPlaylistStates[playlistId];
if (!state || !state.playlist || !progress) return;
-
+
// Save the progress for later restoration
state.lastSyncProgress = progress;
-
+
const card = document.getElementById(`tidal-card-${playlistId}`);
if (!card) return;
-
+
const progressElement = card.querySelector('.playlist-card-progress');
-
+
// Build clean status counter HTML exactly like YouTube cards
let statusCounterHTML = '';
if (progress && progress.total_tracks > 0) {
@@ -14089,7 +14111,7 @@ function updateTidalCardSyncProgress(playlistId, progress) {
const total = progress.total_tracks || 0;
const processed = matched + failed;
const percentage = total > 0 ? Math.round((processed / total) * 100) : 0;
-
+
statusCounterHTML = `
โช ${total}
@@ -14101,49 +14123,49 @@ function updateTidalCardSyncProgress(playlistId, progress) {
`;
}
-
+
// Only update if we have valid sync progress, otherwise preserve existing discovery results
if (statusCounterHTML) {
progressElement.innerHTML = statusCounterHTML;
}
-
+
console.log(`๐ต Updated Tidal card sync progress: โช ${progress?.total_tracks || 0} / โ ${progress?.matched_tracks || 0} / โ ${progress?.failed_tracks || 0}`);
}
function updateTidalModalSyncProgress(urlHash, progress) {
const statusDisplay = document.getElementById(`tidal-sync-status-${urlHash}`);
if (!statusDisplay || !progress) return;
-
+
console.log(`๐ Updating Tidal modal sync progress for ${urlHash}:`, progress);
-
+
// Update individual counters exactly like YouTube sync
const totalEl = document.getElementById(`tidal-total-${urlHash}`);
const matchedEl = document.getElementById(`tidal-matched-${urlHash}`);
const failedEl = document.getElementById(`tidal-failed-${urlHash}`);
const percentageEl = document.getElementById(`tidal-percentage-${urlHash}`);
-
+
const total = progress.total_tracks || 0;
const matched = progress.matched_tracks || 0;
const failed = progress.failed_tracks || 0;
-
+
if (totalEl) totalEl.textContent = total;
if (matchedEl) matchedEl.textContent = matched;
if (failedEl) failedEl.textContent = failed;
-
+
// Calculate percentage like YouTube sync
if (total > 0) {
const processed = matched + failed;
const percentage = Math.round((processed / total) * 100);
if (percentageEl) percentageEl.textContent = percentage;
}
-
+
console.log(`๐ Tidal modal updated: โช ${total} / โ ${matched} / โ ${failed} (${Math.round((matched + failed) / total * 100)}%)`);
}
function updateTidalModalButtons(urlHash, phase) {
const modal = document.getElementById(`youtube-discovery-modal-${urlHash}`);
if (!modal) return;
-
+
const footerLeft = modal.querySelector('.modal-footer-left');
if (footerLeft) {
footerLeft.innerHTML = getModalActionButtons(urlHash, phase);
@@ -14194,7 +14216,7 @@ async function startTidalDownloadMissing(urlHash) {
});
}
}
-
+
if (spotifyTracks.length === 0) {
showToast('No Spotify matches found for download', 'error');
return;
@@ -14206,19 +14228,19 @@ async function startTidalDownloadMissing(urlHash) {
// Store reference for card navigation (same as YouTube)
state.convertedSpotifyPlaylistId = virtualPlaylistId;
-
+
// Close the discovery modal if it's open (same as YouTube)
const discoveryModal = document.getElementById(`youtube-discovery-modal-${urlHash}`);
if (discoveryModal) {
discoveryModal.classList.add('hidden');
console.log('๐ Closed Tidal discovery modal to show download modal');
}
-
+
// Open download missing tracks modal for Tidal playlist
await openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, spotifyTracks);
-
+
// Phase will change to 'downloading' when user clicks "Begin Analysis" button
-
+
} catch (error) {
console.error('โ Error starting download missing tracks:', error);
showToast(`Error starting downloads: ${error.message}`, 'error');
@@ -14241,19 +14263,19 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName,
}
console.log(`๐ฅ Opening Download Missing Tracks modal for Tidal playlist: ${virtualPlaylistId}`);
-
+
// Create virtual playlist object for compatibility with existing modal logic
const virtualPlaylist = {
id: virtualPlaylistId,
name: playlistName,
track_count: spotifyTracks.length
};
-
+
// Store the tracks in the cache for the modal to use
playlistTrackCache[virtualPlaylistId] = spotifyTracks;
currentPlaylistTracks = spotifyTracks;
currentModalPlaylistId = virtualPlaylistId;
-
+
let modal = document.createElement('div');
modal.id = `download-missing-modal-${virtualPlaylistId}`;
modal.className = 'download-missing-modal';
@@ -14272,14 +14294,14 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName,
// Generate hero section with dynamic source detection (same as YouTube/Beatport)
const source = virtualPlaylistId.startsWith('beatport_') ? 'Beatport' :
- virtualPlaylistId.startsWith('tidal_') ? 'Tidal' :
- virtualPlaylistId.startsWith('listenbrainz_') ? 'ListenBrainz' :
- virtualPlaylistId.startsWith('discover_') ? 'SoulSync' :
- virtualPlaylistId.startsWith('seasonal_') ? 'SoulSync' :
- virtualPlaylistId.startsWith('build_playlist_') ? 'SoulSync' :
- virtualPlaylistId.startsWith('decade_') ? 'SoulSync' :
- virtualPlaylistId === 'build_playlist_custom' ? 'SoulSync' :
- 'YouTube';
+ virtualPlaylistId.startsWith('tidal_') ? 'Tidal' :
+ virtualPlaylistId.startsWith('listenbrainz_') ? 'ListenBrainz' :
+ virtualPlaylistId.startsWith('discover_') ? 'SoulSync' :
+ virtualPlaylistId.startsWith('seasonal_') ? 'SoulSync' :
+ virtualPlaylistId.startsWith('build_playlist_') ? 'SoulSync' :
+ virtualPlaylistId.startsWith('decade_') ? 'SoulSync' :
+ virtualPlaylistId === 'build_playlist_custom' ? 'SoulSync' :
+ 'YouTube';
const heroContext = {
type: 'playlist',
@@ -14574,13 +14596,13 @@ function initializeSyncPage() {
if (startSyncBtn) {
startSyncBtn.addEventListener('click', startSequentialSync);
}
-
+
// Logic for the YouTube parse button
const youtubeParseBtn = document.getElementById('youtube-parse-btn');
if (youtubeParseBtn) {
youtubeParseBtn.addEventListener('click', parseYouTubePlaylist);
}
-
+
// Logic for YouTube URL input (Enter key support)
const youtubeUrlInput = document.getElementById('youtube-url-input');
if (youtubeUrlInput) {
@@ -14668,19 +14690,19 @@ async function handleDbUpdateButtonClick() {
async function handleWishlistButtonClick() {
try {
const playlistId = 'wishlist';
-
+
console.log('๐ต [Wishlist Button] User clicked wishlist button - checking server state first');
-
+
// STEP 1: Always check server state first to detect any active wishlist processes
const response = await fetch('/api/active-processes');
if (!response.ok) {
throw new Error(`Failed to fetch active processes: ${response.status}`);
}
-
+
const data = await response.json();
const processes = data.active_processes || [];
const serverWishlistProcess = processes.find(p => p.playlist_id === playlistId);
-
+
// STEP 2: Handle active server process - show current state immediately
if (serverWishlistProcess) {
console.log('๐ฏ [Wishlist Button] Server has active wishlist process:', {
@@ -14689,17 +14711,17 @@ async function handleWishlistButtonClick() {
auto_initiated: serverWishlistProcess.auto_initiated,
should_show: serverWishlistProcess.should_show_modal
});
-
+
// Clear any user-closed state since user explicitly requested to see modal
WishlistModalState.clearUserClosed();
-
+
// Check if we need to create/sync the frontend modal
const clientWishlistProcess = activeDownloadProcesses[playlistId];
- const needsRehydration = !clientWishlistProcess ||
+ const needsRehydration = !clientWishlistProcess ||
clientWishlistProcess.batchId !== serverWishlistProcess.batch_id ||
!clientWishlistProcess.modalElement ||
!document.body.contains(clientWishlistProcess.modalElement);
-
+
if (needsRehydration) {
console.log('๐ [Wishlist Button] Frontend modal needs sync/creation');
await rehydrateModal(serverWishlistProcess, true); // user-requested = true
@@ -14710,25 +14732,25 @@ async function handleWishlistButtonClick() {
}
return;
}
-
+
// STEP 3: No active server process - check wishlist count and create fresh modal
console.log('๐ญ [Wishlist Button] No active server process, checking wishlist content');
-
+
const countResponse = await fetch('/api/wishlist/count');
if (!countResponse.ok) {
throw new Error(`Failed to fetch wishlist count: ${countResponse.status}`);
}
-
+
const countData = await countResponse.json();
if (countData.count === 0) {
showToast('Wishlist is empty. No tracks to download.', 'info');
return;
}
-
+
// STEP 4: Open wishlist overview modal (NEW - category selection)
console.log(`๐ [Wishlist Button] Opening wishlist overview for ${countData.count} tracks`);
await openWishlistOverviewModal();
-
+
} catch (error) {
console.error('โ [Wishlist Button] Error handling wishlist button click:', error);
showToast(`Error opening wishlist: ${error.message}`, 'error');
@@ -14745,39 +14767,39 @@ async function cleanupWishlist(playlistId) {
"This is a safe operation that only removes tracks you already have. " +
"Continue with cleanup?"
);
-
+
if (!confirmed) {
return;
}
-
+
// Disable the cleanup button during the operation
const cleanupBtn = document.getElementById(`cleanup-wishlist-btn-${playlistId}`);
if (cleanupBtn) {
cleanupBtn.disabled = true;
cleanupBtn.textContent = '๐งน Cleaning...';
}
-
+
const response = await fetch('/api/wishlist/cleanup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
-
+
const result = await response.json();
-
+
if (result.success) {
const removedCount = result.removed_count || 0;
const processedCount = result.processed_count || 0;
-
+
if (removedCount > 0) {
showToast(`Wishlist cleanup completed: ${removedCount} tracks removed (${processedCount} checked)`, 'success');
-
+
// Refresh the modal content to show updated state
setTimeout(() => {
openDownloadMissingWishlistModal();
}, 500);
-
+
// Update the wishlist count in the main dashboard
await updateWishlistCount();
} else {
@@ -14786,7 +14808,7 @@ async function cleanupWishlist(playlistId) {
} else {
showToast(`Error cleaning wishlist: ${result.error}`, 'error');
}
-
+
} catch (error) {
console.error('Error cleaning wishlist:', error);
showToast(`Error cleaning wishlist: ${error.message}`, 'error');
@@ -14809,18 +14831,18 @@ async function clearWishlist(playlistId) {
"This will permanently remove all failed tracks from the wishlist. " +
"This action cannot be undone."
);
-
+
if (!confirmed) {
return;
}
-
+
// Disable the clear button during the operation
const clearBtn = document.getElementById(`clear-wishlist-btn-${playlistId}`);
if (clearBtn) {
clearBtn.disabled = true;
clearBtn.textContent = 'Clearing...';
}
-
+
// Call the clear API endpoint
const response = await fetch('/api/wishlist/clear', {
method: 'POST',
@@ -14828,22 +14850,22 @@ async function clearWishlist(playlistId) {
'Content-Type': 'application/json'
}
});
-
+
const result = await response.json();
-
+
if (result.success) {
showToast('Wishlist cleared successfully', 'success');
-
+
// Close the modal since there are no more tracks
closeDownloadMissingModal(playlistId);
-
+
// Update the wishlist count in the main dashboard
await updateWishlistCount();
-
+
} else {
showToast(`Failed to clear wishlist: ${result.error || 'Unknown error'}`, 'error');
}
-
+
} catch (error) {
console.error('Error clearing wishlist:', error);
showToast(`Error clearing wishlist: ${error.message}`, 'error');
@@ -14991,7 +15013,7 @@ function handleBeatportCategoryClick(category) {
console.log(`๐ต Beatport category clicked: ${category}`);
// Only handle genres category now - homepage has direct chart buttons
- switch(category) {
+ switch (category) {
case 'genres':
showBeatportSubView('genres');
loadBeatportGenres(); // Load genres dynamically
@@ -15645,7 +15667,7 @@ async function getRebuildPageTrackData(trackDataKey) {
// Hook into the loadBeatportTop10Lists function to cache track data
const originalLoadBeatportTop10Lists = window.loadBeatportTop10Lists;
if (originalLoadBeatportTop10Lists) {
- window.loadBeatportTop10Lists = async function() {
+ window.loadBeatportTop10Lists = async function () {
const result = await originalLoadBeatportTop10Lists.apply(this, arguments);
// If the load was successful, we can potentially cache the track data
@@ -17471,24 +17493,24 @@ async function handleGenreChartTypeClick(genreSlug, genreId, genreName, chartTyp
async function parseYouTubePlaylist() {
const urlInput = document.getElementById('youtube-url-input');
const url = urlInput.value.trim();
-
+
if (!url) {
showToast('Please enter a YouTube playlist URL', 'error');
return;
}
-
+
// Validate URL format
if (!url.includes('youtube.com/playlist') && !url.includes('music.youtube.com/playlist')) {
showToast('Please enter a valid YouTube playlist URL', 'error');
return;
}
-
+
try {
console.log('๐ฌ Parsing YouTube playlist:', url);
-
+
// Create card immediately in 'fresh' phase
createYouTubeCard(url, 'fresh');
-
+
// Parse playlist via API
const response = await fetch('/api/youtube/parse', {
method: 'POST',
@@ -17497,27 +17519,27 @@ async function parseYouTubePlaylist() {
},
body: JSON.stringify({ url: url })
});
-
+
const result = await response.json();
-
+
if (result.error) {
showToast(`Error parsing YouTube playlist: ${result.error}`, 'error');
removeYouTubeCard(url);
return;
}
-
+
console.log('โ YouTube playlist parsed:', result.name, `(${result.tracks.length} tracks)`);
-
+
// Update card with parsed data and stay in 'fresh' phase
updateYouTubeCardData(result.url_hash, result);
updateYouTubeCardPhase(result.url_hash, 'fresh');
-
+
// Clear input
urlInput.value = '';
-
+
// Show success message
showToast(`YouTube playlist parsed: ${result.name} (${result.tracks.length} tracks)`, 'success');
-
+
} catch (error) {
console.error('โ Error parsing YouTube playlist:', error);
showToast(`Error parsing YouTube playlist: ${error.message}`, 'error');
@@ -17528,15 +17550,15 @@ async function parseYouTubePlaylist() {
function createYouTubeCard(url, phase = 'fresh') {
const container = document.getElementById('youtube-playlist-container');
const placeholder = container.querySelector('.playlist-placeholder');
-
+
// Remove placeholder if it exists
if (placeholder) {
placeholder.style.display = 'none';
}
-
+
// Create temporary URL hash for initial card
const tempHash = btoa(url).substring(0, 8);
-
+
const cardHtml = `
โถ
@@ -17553,9 +17575,9 @@ function createYouTubeCard(url, phase = 'fresh') {
`;
-
+
container.insertAdjacentHTML('beforeend', cardHtml);
-
+
// Store temporary state
youtubePlaylistStates[tempHash] = {
phase: phase,
@@ -17563,7 +17585,7 @@ function createYouTubeCard(url, phase = 'fresh') {
cardElement: document.getElementById(`youtube-card-${tempHash}`),
tempHash: tempHash
};
-
+
console.log('๐ Created YouTube card for URL:', url);
}
@@ -17578,56 +17600,56 @@ function updateYouTubeCardData(urlHash, playlistData) {
delete youtubePlaylistStates[tempState.tempHash];
youtubePlaylistStates[urlHash] = tempState;
state = tempState;
-
+
// Update card ID
if (state.cardElement) {
state.cardElement.id = `youtube-card-${urlHash}`;
}
}
}
-
+
if (!state || !state.cardElement) {
console.error('โ Could not find YouTube card for hash:', urlHash);
return;
}
-
+
const card = state.cardElement;
-
+
// Update card content
const nameElement = card.querySelector('.playlist-card-name');
const trackCountElement = card.querySelector('.playlist-card-track-count');
-
+
nameElement.textContent = playlistData.name;
trackCountElement.textContent = `${playlistData.tracks.length} tracks`;
-
+
// Store playlist data
state.playlist = playlistData;
state.urlHash = urlHash;
-
+
// Add click handler for card and action button
const handleCardClick = () => handleYouTubeCardClick(urlHash);
const actionBtn = card.querySelector('.playlist-card-action-btn');
-
+
card.addEventListener('click', handleCardClick);
actionBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent card click
handleCardClick();
});
-
+
console.log('๐ Updated YouTube card data:', playlistData.name);
}
function updateYouTubeCardPhase(urlHash, phase) {
const state = youtubePlaylistStates[urlHash];
if (!state || !state.cardElement) return;
-
+
const card = state.cardElement;
const phaseTextElement = card.querySelector('.playlist-card-phase-text');
const actionBtn = card.querySelector('.playlist-card-action-btn');
const progressElement = card.querySelector('.playlist-card-progress');
-
+
state.phase = phase;
-
+
switch (phase) {
case 'fresh':
phaseTextElement.textContent = 'Ready to discover';
@@ -17636,7 +17658,7 @@ function updateYouTubeCardPhase(urlHash, phase) {
actionBtn.disabled = false;
progressElement.classList.add('hidden');
break;
-
+
case 'discovering':
phaseTextElement.textContent = 'Discovering...';
phaseTextElement.style.color = '#ffa500'; // Orange
@@ -17644,7 +17666,7 @@ function updateYouTubeCardPhase(urlHash, phase) {
actionBtn.disabled = false;
progressElement.classList.remove('hidden');
break;
-
+
case 'discovered':
phaseTextElement.textContent = 'Discovery Complete';
phaseTextElement.style.color = '#1db954'; // Green
@@ -17652,7 +17674,7 @@ function updateYouTubeCardPhase(urlHash, phase) {
actionBtn.disabled = false;
progressElement.classList.add('hidden');
break;
-
+
case 'syncing':
phaseTextElement.textContent = 'Syncing...';
phaseTextElement.style.color = '#ffa500'; // Orange
@@ -17660,7 +17682,7 @@ function updateYouTubeCardPhase(urlHash, phase) {
actionBtn.disabled = false;
progressElement.classList.remove('hidden');
break;
-
+
case 'sync_complete':
phaseTextElement.textContent = 'Sync Complete';
phaseTextElement.style.color = '#1db954'; // Green
@@ -17668,7 +17690,7 @@ function updateYouTubeCardPhase(urlHash, phase) {
actionBtn.disabled = false;
progressElement.classList.add('hidden');
break;
-
+
case 'downloading':
phaseTextElement.textContent = 'Downloading...';
phaseTextElement.style.color = '#ffa500'; // Orange
@@ -17676,7 +17698,7 @@ function updateYouTubeCardPhase(urlHash, phase) {
actionBtn.disabled = false;
progressElement.classList.remove('hidden');
break;
-
+
case 'download_complete':
phaseTextElement.textContent = 'Download Complete';
phaseTextElement.style.color = '#1db954'; // Green
@@ -17685,14 +17707,14 @@ function updateYouTubeCardPhase(urlHash, phase) {
progressElement.classList.add('hidden');
break;
}
-
+
console.log('๐ Updated YouTube card phase:', urlHash, phase);
}
function handleYouTubeCardClick(urlHash) {
const state = youtubePlaylistStates[urlHash];
if (!state) return;
-
+
switch (state.phase) {
case 'fresh':
// First click: Start discovery and open modal
@@ -17701,7 +17723,7 @@ function handleYouTubeCardClick(urlHash) {
startYouTubeDiscovery(urlHash);
openYouTubeDiscoveryModal(urlHash);
break;
-
+
case 'discovering':
case 'discovered':
case 'syncing':
@@ -17710,7 +17732,7 @@ function handleYouTubeCardClick(urlHash) {
console.log('๐ฌ Opening YouTube discovery modal:', urlHash);
openYouTubeDiscoveryModal(urlHash);
break;
-
+
case 'downloading':
case 'download_complete':
// Open download missing tracks modal
@@ -17727,7 +17749,7 @@ function handleYouTubeCardClick(urlHash) {
if (fullState.discovery_results) {
state.discoveryResults = fullState.discovery_results;
console.log(`โ Loaded ${state.discoveryResults.length} discovery results`);
-
+
// Now open the modal with the loaded data
const playlistName = state.playlist.name;
const spotifyTracks = state.discoveryResults
@@ -17762,17 +17784,17 @@ function handleYouTubeCardClick(urlHash) {
function updateYouTubeCardProgress(urlHash, progress) {
const state = youtubePlaylistStates[urlHash];
if (!state || !state.cardElement) return;
-
+
const card = state.cardElement;
const progressElement = card.querySelector('.playlist-card-progress');
-
+
const total = progress.spotify_total || 0;
const matches = progress.spotify_matches || 0;
const failed = total - matches;
const percentage = total > 0 ? Math.round((matches / total) * 100) : 0;
-
+
progressElement.textContent = `โช ${total} / โ ${matches} / โ ${failed} / ${percentage}%`;
-
+
console.log('๐ Updated YouTube card progress:', urlHash, `${matches}/${total} (${percentage}%)`);
}
@@ -17780,7 +17802,7 @@ function removeYouTubeCard(url) {
const state = Object.values(youtubePlaylistStates).find(s => s.url === url);
if (state && state.cardElement) {
state.cardElement.remove();
-
+
// Remove from state
if (state.urlHash) {
delete youtubePlaylistStates[state.urlHash];
@@ -17788,12 +17810,12 @@ function removeYouTubeCard(url) {
delete youtubePlaylistStates[state.tempHash];
}
}
-
+
// Show placeholder if no cards left
const container = document.getElementById('youtube-playlist-container');
const cards = container.querySelectorAll('.youtube-playlist-card');
const placeholder = container.querySelector('.playlist-placeholder');
-
+
if (cards.length === 0 && placeholder) {
placeholder.style.display = 'block';
}
@@ -17802,24 +17824,24 @@ function removeYouTubeCard(url) {
async function startYouTubeDiscovery(urlHash) {
try {
console.log('๐ Starting YouTube Spotify discovery for:', urlHash);
-
+
const response = await fetch(`/api/youtube/discovery/start/${urlHash}`, {
method: 'POST'
});
-
+
const result = await response.json();
-
+
if (result.error) {
showToast(`Error starting discovery: ${result.error}`, 'error');
return;
}
-
+
// Start polling for progress
startYouTubeDiscoveryPolling(urlHash);
-
+
// Open discovery modal
openYouTubeDiscoveryModal(urlHash);
-
+
} catch (error) {
console.error('โ Error starting YouTube discovery:', error);
showToast(`Error starting discovery: ${error.message}`, 'error');
@@ -17831,22 +17853,22 @@ function startYouTubeDiscoveryPolling(urlHash) {
if (activeYouTubePollers[urlHash]) {
clearInterval(activeYouTubePollers[urlHash]);
}
-
+
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/youtube/discovery/status/${urlHash}`);
const status = await response.json();
-
+
if (status.error) {
console.error('โ Error polling YouTube discovery status:', status.error);
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
return;
}
-
+
// Update card progress
updateYouTubeCardProgress(urlHash, status);
-
+
// Store discovery results and progress in state
const state = youtubePlaylistStates[urlHash];
if (state) {
@@ -17854,32 +17876,32 @@ function startYouTubeDiscoveryPolling(urlHash) {
state.discoveryProgress = status.progress || 0;
state.spotifyMatches = status.spotify_matches || 0;
}
-
+
// Update modal if open
updateYouTubeDiscoveryModal(urlHash, status);
-
+
// Check if complete
if (status.complete) {
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
-
+
// Update card phase to discovered
updateYouTubeCardPhase(urlHash, 'discovered');
-
+
// Update modal buttons to show sync and download buttons
updateYouTubeModalButtons(urlHash, 'discovered');
-
+
console.log('โ YouTube discovery complete:', urlHash);
showToast('YouTube discovery complete!', 'success');
}
-
+
} catch (error) {
console.error('โ Error polling YouTube discovery:', error);
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
}
}, 1000);
-
+
activeYouTubePollers[urlHash] = pollInterval;
}
@@ -17932,14 +17954,14 @@ function openYouTubeDiscoveryModal(urlHash) {
const isBeatport = state.is_beatport_playlist;
const isListenBrainz = state.is_listenbrainz_playlist;
const modalTitle = isTidal ? '๐ต Tidal Playlist Discovery' :
- isBeatport ? '๐ต Beatport Chart Discovery' :
- isListenBrainz ? '๐ต ListenBrainz Playlist Discovery' :
- '๐ต YouTube Playlist Discovery';
+ isBeatport ? '๐ต Beatport Chart Discovery' :
+ isListenBrainz ? '๐ต ListenBrainz Playlist Discovery' :
+ '๐ต YouTube Playlist Discovery';
const sourceLabel = isTidal ? 'Tidal' :
- isBeatport ? 'Beatport' :
- isListenBrainz ? 'LB' :
- 'YT';
-
+ isBeatport ? 'Beatport' :
+ isListenBrainz ? 'LB' :
+ 'YT';
+
const modalHtml = `
@@ -18049,14 +18071,14 @@ function openYouTubeDiscoveryModal(urlHash) {
`;
-
+
// Add modal to DOM
document.body.insertAdjacentHTML('beforeend', modalHtml);
modal = document.getElementById(`youtube-discovery-modal-${urlHash}`);
-
+
// Store modal reference
state.modalElement = modal;
-
+
// Set initial progress if we have discovery results
if (state.discoveryResults && state.discoveryResults.length > 0) {
const progressData = {
@@ -18093,12 +18115,12 @@ function getModalActionButtons(urlHash, phase, state = null) {
const isTidal = state && state.is_tidal_playlist;
const isBeatport = state && state.is_beatport_playlist;
const isListenBrainz = state && state.is_listenbrainz_playlist;
-
+
// Validate data availability for buttons (support both naming conventions)
const hasDiscoveryResults = state && ((state.discoveryResults && state.discoveryResults.length > 0) || (state.discovery_results && state.discovery_results.length > 0));
const hasSpotifyMatches = state && ((state.spotifyMatches > 0) || (state.spotify_matches > 0));
const hasConvertedPlaylistId = state && state.convertedSpotifyPlaylistId;
-
+
switch (phase) {
case 'fresh':
case 'discovering':
@@ -18123,9 +18145,9 @@ function getModalActionButtons(urlHash, phase, state = null) {
if (!hasDiscoveryResults) {
return `
โ ๏ธ No discovery results available. Try starting discovery again.
`;
}
-
+
let buttons = '';
-
+
// Only show sync button if there are Spotify matches
if (hasSpotifyMatches) {
if (isListenBrainz) {
@@ -18152,13 +18174,13 @@ function getModalActionButtons(urlHash, phase, state = null) {
buttons += ``;
}
}
-
+
if (!buttons) {
buttons = `
โน๏ธ No Spotify matches found. Discovery complete but no tracks could be matched.
`;
}
-
+
return buttons;
-
+
case 'syncing':
if (isListenBrainz) {
return `
@@ -18209,7 +18231,7 @@ function getModalActionButtons(urlHash, phase, state = null) {