consistency

This commit is contained in:
Broque Thomas 2025-09-02 14:14:51 -07:00
parent 417c76e68c
commit 2070e24394
3 changed files with 281 additions and 63 deletions

View file

@ -5676,22 +5676,35 @@ def start_tidal_discovery(playlist_id):
if not target_playlist.tracks: if not target_playlist.tracks:
return jsonify({"error": "Playlist has no tracks"}), 400 return jsonify({"error": "Playlist has no tracks"}), 400
# Initialize or update discovery state # Initialize discovery state if it doesn't exist, or update existing state
if playlist_id in tidal_discovery_states and tidal_discovery_states[playlist_id]['phase'] == 'discovering': if playlist_id in tidal_discovery_states:
return jsonify({"error": "Discovery already in progress"}), 400 existing_state = tidal_discovery_states[playlist_id]
if existing_state['phase'] == 'discovering':
state = { return jsonify({"error": "Discovery already in progress"}), 400
'playlist': target_playlist, # Update existing state for discovery
'phase': 'discovering', existing_state['phase'] = 'discovering'
'status': 'discovering', existing_state['status'] = 'discovering'
'discovery_progress': 0, existing_state['last_accessed'] = time.time()
'spotify_matches': 0, state = existing_state
'spotify_total': len(target_playlist.tracks), else:
'discovery_results': [], # Create new state for first-time discovery
'last_accessed': time.time() state = {
} 'playlist': target_playlist,
'phase': 'discovering', # fresh -> discovering -> discovered -> syncing -> sync_complete -> downloading -> download_complete
tidal_discovery_states[playlist_id] = state 'status': 'discovering',
'discovery_progress': 0,
'spotify_matches': 0,
'spotify_total': len(target_playlist.tracks),
'discovery_results': [],
'sync_playlist_id': None,
'converted_spotify_playlist_id': None,
'download_process_id': None, # Track associated download missing tracks process
'created_at': time.time(),
'last_accessed': time.time(),
'discovery_future': None,
'sync_progress': {}
}
tidal_discovery_states[playlist_id] = state
# Start discovery worker # Start discovery worker
future = tidal_discovery_executor.submit(_run_tidal_discovery_worker, playlist_id) future = tidal_discovery_executor.submit(_run_tidal_discovery_worker, playlist_id)
@ -5761,6 +5774,91 @@ def get_tidal_playlist_states():
print(f"❌ Error getting Tidal playlist states: {e}") print(f"❌ Error getting Tidal playlist states: {e}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/state/<playlist_id>', methods=['GET'])
def get_tidal_playlist_state(playlist_id):
"""Get specific Tidal playlist state (detailed version matching YouTube's state endpoint)"""
try:
if playlist_id not in tidal_discovery_states:
return jsonify({"error": "Tidal playlist not found"}), 404
state = tidal_discovery_states[playlist_id]
state['last_accessed'] = time.time()
# Return full state information (including results for modal hydration)
response = {
'playlist_id': playlist_id,
'playlist': state['playlist'].__dict__ if hasattr(state['playlist'], '__dict__') else state['playlist'],
'phase': state['phase'],
'status': state['status'],
'discovery_progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'discovery_results': state['discovery_results'],
'last_accessed': state['last_accessed']
}
return jsonify(response)
except Exception as e:
print(f"❌ Error getting Tidal playlist state: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/reset/<playlist_id>', methods=['POST'])
def reset_tidal_playlist(playlist_id):
"""Reset Tidal playlist to fresh phase (clear discovery/sync data)"""
try:
if playlist_id not in tidal_discovery_states:
return jsonify({"error": "Tidal playlist not found"}), 404
state = tidal_discovery_states[playlist_id]
# Stop any active discovery
if 'discovery_future' in state and state['discovery_future']:
state['discovery_future'].cancel()
# Reset state to fresh (preserve original playlist data)
state['phase'] = 'fresh'
state['status'] = 'fresh'
state['discovery_results'] = []
state['discovery_progress'] = 0
state['spotify_matches'] = 0
state['sync_playlist_id'] = None
state['converted_spotify_playlist_id'] = None
state['download_process_id'] = None
state['sync_progress'] = {}
state['discovery_future'] = None
state['last_accessed'] = time.time()
print(f"🔄 Reset Tidal playlist to fresh: {playlist_id}")
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
except Exception as e:
print(f"❌ Error resetting Tidal playlist: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/tidal/delete/<playlist_id>', methods=['POST'])
def delete_tidal_playlist(playlist_id):
"""Delete Tidal playlist state completely"""
try:
if playlist_id not in tidal_discovery_states:
return jsonify({"error": "Tidal playlist not found"}), 404
state = tidal_discovery_states[playlist_id]
# Stop any active discovery
if 'discovery_future' in state and state['discovery_future']:
state['discovery_future'].cancel()
# Remove from state dictionary
del tidal_discovery_states[playlist_id]
print(f"🗑️ Deleted Tidal playlist state: {playlist_id}")
return jsonify({"success": True, "message": "Playlist deleted"})
except Exception as e:
print(f"❌ Error deleting Tidal playlist: {e}")
return jsonify({"error": str(e)}), 500
def _run_tidal_discovery_worker(playlist_id): def _run_tidal_discovery_worker(playlist_id):
"""Background worker for Tidal Spotify discovery process (like sync.py)""" """Background worker for Tidal Spotify discovery process (like sync.py)"""

View file

@ -6420,6 +6420,9 @@ function createTidalCard(playlist) {
<span class="playlist-card-phase-text" style="color: ${phaseColor};">${phaseText}</span> <span class="playlist-card-phase-text" style="color: ${phaseColor};">${phaseText}</span>
</div> </div>
</div> </div>
<div class="playlist-card-progress ${phase === 'fresh' ? 'hidden' : ''}">
${playlist.track_count} / 0 / ${playlist.track_count} / 0%
</div>
<button class="playlist-card-action-btn">${buttonText}</button> <button class="playlist-card-action-btn">${buttonText}</button>
</div> </div>
`; `;
@ -6436,13 +6439,7 @@ async function handleTidalCardClick(playlistId) {
console.log(`🎵 Using pre-loaded Tidal playlist data for: ${state.playlist.name}`); console.log(`🎵 Using pre-loaded Tidal playlist data for: ${state.playlist.name}`);
console.log(`🎵 Ready with ${state.playlist.tracks.length} Tidal tracks for discovery`); console.log(`🎵 Ready with ${state.playlist.tracks.length} Tidal tracks for discovery`);
// Update phase to discovering // Open discovery modal - phase will be updated when discovery actually starts
state.phase = 'discovering';
// Update card to show discovering state
updateTidalCardPhase(playlistId, 'discovering');
// Open YouTube discovery modal but with Tidal data (exact sync.py pattern)
openTidalDiscoveryModal(playlistId, state.playlist); openTidalDiscoveryModal(playlistId, state.playlist);
} else if (state.phase === 'discovering' || state.phase === 'discovered') { } else if (state.phase === 'discovering' || state.phase === 'discovered') {
@ -6479,43 +6476,54 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
// Create a fake YouTube-style urlHash for the modal system // Create a fake YouTube-style urlHash for the modal system
const fakeUrlHash = `tidal_${playlistId}`; const fakeUrlHash = `tidal_${playlistId}`;
// Get current Tidal card state to check if discovery is already done // Get current Tidal card state to check if discovery is already done or in progress
const tidalCardState = tidalPlaylistStates[playlistId]; const tidalCardState = tidalPlaylistStates[playlistId];
const isAlreadyDiscovered = tidalCardState && tidalCardState.phase === 'discovered'; const isAlreadyDiscovered = tidalCardState && tidalCardState.phase === 'discovered';
const isCurrentlyDiscovering = tidalCardState && tidalCardState.phase === 'discovering';
// Prepare discovery results in the correct format for modal // Prepare discovery results in the correct format for modal
let transformedResults = []; let transformedResults = [];
let actualMatches = 0;
if (isAlreadyDiscovered && tidalCardState.discovery_results) { if (isAlreadyDiscovered && tidalCardState.discovery_results) {
transformedResults = tidalCardState.discovery_results.map((result, index) => ({ transformedResults = tidalCardState.discovery_results.map((result, index) => {
index: index, const isFound = result.status === 'found';
yt_track: result.tidal_track ? result.tidal_track.name : 'Unknown', if (isFound) actualMatches++;
yt_artist: result.tidal_track ? (result.tidal_track.artists ? result.tidal_track.artists.join(', ') : 'Unknown') : 'Unknown',
status: result.status === 'found' ? '✅ Found' : '❌ Not Found', return {
status_class: result.status === 'found' ? 'found' : 'not-found', index: index,
spotify_track: result.spotify_data ? result.spotify_data.name : '-', yt_track: result.tidal_track ? result.tidal_track.name : 'Unknown',
spotify_artist: result.spotify_data ? result.spotify_data.artists.join(', ') : '-', yt_artist: result.tidal_track ? (result.tidal_track.artists ? result.tidal_track.artists.join(', ') : 'Unknown') : 'Unknown',
spotify_album: result.spotify_data ? result.spotify_data.album : '-' status: isFound ? '✅ Found' : '❌ Not Found',
})); status_class: isFound ? 'found' : 'not-found',
spotify_track: result.spotify_data ? result.spotify_data.name : '-',
spotify_artist: result.spotify_data ? result.spotify_data.artists.join(', ') : '-',
spotify_album: result.spotify_data ? result.spotify_data.album : '-'
};
});
console.log(`🎵 Tidal modal: Calculated ${actualMatches} matches from ${transformedResults.length} results`);
} }
// Create YouTube-compatible state structure // Create YouTube-compatible state structure
const modalPhase = isAlreadyDiscovered ? 'discovered' : (isCurrentlyDiscovering ? 'discovering' : 'fresh');
youtubePlaylistStates[fakeUrlHash] = { youtubePlaylistStates[fakeUrlHash] = {
phase: isAlreadyDiscovered ? 'discovered' : 'discovering', phase: modalPhase,
playlist: { playlist: {
name: playlistData.name, name: playlistData.name,
tracks: playlistData.tracks tracks: playlistData.tracks
}, },
is_tidal_playlist: true, // Flag to identify this as Tidal is_tidal_playlist: true, // Flag to identify this as Tidal
tidal_playlist_id: playlistId, tidal_playlist_id: playlistId,
discovery_progress: isAlreadyDiscovered ? (tidalCardState.discovery_progress || 100) : 0, discovery_progress: isAlreadyDiscovered ? 100 : 0,
spotify_matches: isAlreadyDiscovered ? (tidalCardState.spotify_matches || 0) : 0, spotify_matches: isAlreadyDiscovered ? actualMatches : 0, // Backend format (snake_case)
spotifyMatches: isAlreadyDiscovered ? actualMatches : 0, // Frontend format (camelCase) - for button logic
spotify_total: playlistData.tracks.length, spotify_total: playlistData.tracks.length,
discovery_results: transformedResults, discovery_results: transformedResults,
discoveryResults: transformedResults // Both formats for compatibility discoveryResults: transformedResults, // Both formats for compatibility
discoveryProgress: isAlreadyDiscovered ? 100 : 0 // Frontend format for modal progress display
}; };
// Only start discovery if not already discovered // Only start discovery if not already discovered AND not currently discovering
if (!isAlreadyDiscovered) { if (!isAlreadyDiscovered && !isCurrentlyDiscovering) {
// Start Tidal discovery process automatically (like sync.py) // Start Tidal discovery process automatically (like sync.py)
try { try {
console.log(`🔍 Starting Tidal discovery for: ${playlistData.name}`); console.log(`🔍 Starting Tidal discovery for: ${playlistData.name}`);
@ -6534,6 +6542,13 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
console.log('✅ Tidal discovery started, beginning polling...'); 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 // Start polling for progress
startTidalDiscoveryPolling(fakeUrlHash, playlistId); startTidalDiscoveryPolling(fakeUrlHash, playlistId);
@ -6541,6 +6556,10 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
console.error('❌ Error starting Tidal discovery:', error); console.error('❌ Error starting Tidal discovery:', error);
showToast(`Error starting discovery: ${error.message}`, 'error'); showToast(`Error starting discovery: ${error.message}`, 'error');
} }
} else if (isCurrentlyDiscovering) {
// Resume polling if discovery is already in progress (like YouTube)
console.log(`🔄 Resuming Tidal discovery polling for: ${playlistData.name}`);
startTidalDiscoveryPolling(fakeUrlHash, playlistId);
} else { } else {
console.log('✅ Using existing discovery results - no need to re-discover'); console.log('✅ Using existing discovery results - no need to re-discover');
} }
@ -6569,34 +6588,38 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
return; return;
} }
// Transform Tidal results to YouTube modal format first
const transformedStatus = {
progress: status.progress,
spotify_matches: status.spotify_matches,
spotify_total: status.spotify_total,
results: status.results.map((result, index) => ({
index: index,
yt_track: result.tidal_track ? result.tidal_track.name : 'Unknown',
yt_artist: result.tidal_track ? (result.tidal_track.artists ? result.tidal_track.artists.join(', ') : 'Unknown') : 'Unknown',
status: result.status === 'found' ? '✅ Found' : '❌ Not Found',
status_class: result.status === 'found' ? 'found' : 'not-found',
spotify_track: result.spotify_data ? result.spotify_data.name : '-',
spotify_artist: result.spotify_data ? result.spotify_data.artists.join(', ') : '-',
spotify_album: result.spotify_data ? result.spotify_data.album : '-'
}))
};
// Update fake YouTube state with Tidal discovery results // Update fake YouTube state with Tidal discovery results
const state = youtubePlaylistStates[fakeUrlHash]; const state = youtubePlaylistStates[fakeUrlHash];
if (state) { if (state) {
state.discovery_progress = status.progress; state.discovery_progress = status.progress; // Backend format
state.spotify_matches = status.spotify_matches; state.discoveryProgress = status.progress; // Frontend format - for modal progress display
state.discovery_results = status.results; state.spotify_matches = status.spotify_matches; // Backend format
state.spotifyMatches = status.spotify_matches; // Frontend format - for button logic
state.discovery_results = status.results; // Backend format
state.discoveryResults = transformedStatus.results; // Frontend format - for button logic
state.phase = status.phase; state.phase = status.phase;
// Transform Tidal results to YouTube modal format
const transformedStatus = {
progress: status.progress,
spotify_matches: status.spotify_matches,
spotify_total: status.spotify_total,
results: status.results.map((result, index) => ({
index: index,
status: result.status === 'found' ? '✅ Found' : '❌ Not Found',
status_class: result.status === 'found' ? 'found' : 'not-found',
spotify_track: result.spotify_data ? result.spotify_data.name : '-',
spotify_artist: result.spotify_data ? result.spotify_data.artists.join(', ') : '-',
spotify_album: result.spotify_data ? result.spotify_data.album : '-'
// Note: No duration column for Tidal (matches GUI version)
}))
};
// Update modal with transformed data (reuse YouTube modal update logic) // Update modal with transformed data (reuse YouTube modal update logic)
updateYouTubeDiscoveryModal(fakeUrlHash, transformedStatus); updateYouTubeDiscoveryModal(fakeUrlHash, transformedStatus);
// Update Tidal card phase and save discovery results // Update Tidal card phase and save discovery results FIRST
if (tidalPlaylistStates[playlistId]) { if (tidalPlaylistStates[playlistId]) {
tidalPlaylistStates[playlistId].phase = status.phase; tidalPlaylistStates[playlistId].phase = status.phase;
tidalPlaylistStates[playlistId].discovery_results = status.results; tidalPlaylistStates[playlistId].discovery_results = status.results;
@ -6605,6 +6628,9 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
updateTidalCardPhase(playlistId, status.phase); 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)`); console.log(`🔄 Tidal discovery progress: ${status.progress}% (${status.spotify_matches}/${status.spotify_total} found)`);
} }
@ -6688,9 +6714,42 @@ async function applyTidalPlaylistState(stateInfo) {
tidalPlaylistStates[playlist_id].discovery_results = discovery_results; tidalPlaylistStates[playlist_id].discovery_results = discovery_results;
tidalPlaylistStates[playlist_id].playlist = playlistData; // Ensure playlist data is set 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 {
console.log(`🔍 Fetching full discovery results for Tidal playlist: ${playlistData.name}`);
const stateResponse = await fetch(`/api/tidal/state/${playlist_id}`);
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;
tidalPlaylistStates[playlist_id].discovery_progress = fullState.discovery_progress;
tidalPlaylistStates[playlist_id].spotify_matches = fullState.spotify_matches;
console.log(`✅ Restored ${fullState.discovery_results.length} discovery results for Tidal playlist: ${playlistData.name}`);
}
} else {
console.warn(`⚠️ Could not fetch full discovery results for Tidal playlist: ${playlistData.name}`);
}
} catch (error) {
console.warn(`⚠️ Error fetching full discovery results for Tidal playlist ${playlistData.name}:`, error.message);
}
}
// Update the card UI to reflect the saved state // Update the card UI to reflect the saved state
updateTidalCardPhase(playlist_id, phase); updateTidalCardPhase(playlist_id, phase);
// Update card progress if we have discovery results
if (phase === 'discovered' && tidalPlaylistStates[playlist_id]) {
const progressInfo = {
spotify_total: playlistData.track_count || playlistData.tracks?.length || 0,
spotify_matches: tidalPlaylistStates[playlist_id].spotify_matches || 0
};
updateTidalCardProgress(playlist_id, progressInfo);
}
console.log(`✅ Applied saved state for Tidal playlist: ${playlist_id} -> ${phase}`); console.log(`✅ Applied saved state for Tidal playlist: ${playlist_id} -> ${phase}`);
} catch (error) { } catch (error) {
@ -6698,6 +6757,27 @@ 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}%)`);
}
// Tidal-specific sync and download functions (placeholder implementations) // Tidal-specific sync and download functions (placeholder implementations)
function startTidalPlaylistSync(urlHash) { function startTidalPlaylistSync(urlHash) {
console.log(`🎵 Starting Tidal playlist sync for: ${urlHash}`); console.log(`🎵 Starting Tidal playlist sync for: ${urlHash}`);
@ -7659,12 +7739,39 @@ function updateYouTubeDiscoveryModal(urlHash, status) {
const progressText = document.getElementById(`youtube-discovery-progress-text-${urlHash}`); const progressText = document.getElementById(`youtube-discovery-progress-text-${urlHash}`);
const tableBody = document.getElementById(`youtube-discovery-table-${urlHash}`); const tableBody = document.getElementById(`youtube-discovery-table-${urlHash}`);
if (!progressBar || !progressText || !tableBody) return; if (!progressBar || !progressText || !tableBody) {
console.warn(`⚠️ Missing modal elements for ${urlHash}:`, {
progressBar: !!progressBar,
progressText: !!progressText,
tableBody: !!tableBody
});
return;
}
// Update progress bar // Update progress bar
progressBar.style.width = `${status.progress}%`; progressBar.style.width = `${status.progress}%`;
progressText.textContent = `${status.spotify_matches} / ${status.spotify_total} tracks matched (${status.progress}%)`; progressText.textContent = `${status.spotify_matches} / ${status.spotify_total} tracks matched (${status.progress}%)`;
// Ensure progress bar container has proper height (CSS might not be applying correctly)
const progressContainer = progressBar.parentElement;
if (progressContainer && progressContainer.classList.contains('progress-bar-container')) {
progressContainer.style.height = '8px';
progressContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
progressContainer.style.borderRadius = '8px';
progressContainer.style.overflow = 'hidden';
progressContainer.style.marginBottom = '8px';
// Ensure progress bar fill has proper styling
progressBar.style.height = '100%';
progressBar.style.background = 'linear-gradient(90deg, #1db954 0%, #1ed760 100%)';
progressBar.style.transition = 'width 0.5s ease';
}
console.log(`📊 Updated modal progress for ${urlHash}: ${status.progress}% (${status.spotify_matches}/${status.spotify_total})`);
console.log(`📊 Progress bar width set to: ${progressBar.style.width}`);
console.log(`📊 Progress bar element:`, progressBar);
console.log(`📊 Progress bar computed styles:`, window.getComputedStyle(progressBar));
// Update table rows // Update table rows
status.results.forEach(result => { status.results.forEach(result => {
const row = document.getElementById(`discovery-row-${urlHash}-${result.index}`); const row = document.getElementById(`discovery-row-${urlHash}-${result.index}`);
@ -7682,6 +7789,18 @@ function updateYouTubeDiscoveryModal(urlHash, status) {
spotifyArtistCell.textContent = result.spotify_artist || '-'; spotifyArtistCell.textContent = result.spotify_artist || '-';
spotifyAlbumCell.textContent = result.spotify_album || '-'; spotifyAlbumCell.textContent = result.spotify_album || '-';
}); });
// Update action buttons if discovery is complete (progress = 100%)
if (status.progress >= 100) {
const state = youtubePlaylistStates[urlHash];
if (state && state.phase === 'discovered') {
const actionButtonsContainer = document.querySelector(`#youtube-discovery-modal-${urlHash} .modal-footer-left`);
if (actionButtonsContainer) {
actionButtonsContainer.innerHTML = getModalActionButtons(urlHash, 'discovered', state);
console.log(`✨ Updated action buttons for completed discovery: ${urlHash}`);
}
}
}
} }
function refreshYouTubeDiscoveryModalTable(urlHash) { function refreshYouTubeDiscoveryModalTable(urlHash) {

View file

@ -4657,10 +4657,11 @@ body {
} }
.youtube-discovery-modal .progress-bar-container { .youtube-discovery-modal .progress-bar-container {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
height: 8px; height: 20px!important;
margin-bottom: 8px; margin-bottom: 8px;
} }