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:
return jsonify({"error": "Playlist has no tracks"}), 400
# Initialize or update discovery state
if playlist_id in tidal_discovery_states and tidal_discovery_states[playlist_id]['phase'] == 'discovering':
return jsonify({"error": "Discovery already in progress"}), 400
state = {
'playlist': target_playlist,
'phase': 'discovering',
'status': 'discovering',
'discovery_progress': 0,
'spotify_matches': 0,
'spotify_total': len(target_playlist.tracks),
'discovery_results': [],
'last_accessed': time.time()
}
tidal_discovery_states[playlist_id] = state
# Initialize discovery state if it doesn't exist, or update existing state
if playlist_id in tidal_discovery_states:
existing_state = tidal_discovery_states[playlist_id]
if existing_state['phase'] == 'discovering':
return jsonify({"error": "Discovery already in progress"}), 400
# Update existing state for discovery
existing_state['phase'] = 'discovering'
existing_state['status'] = 'discovering'
existing_state['last_accessed'] = time.time()
state = existing_state
else:
# Create new state for first-time discovery
state = {
'playlist': target_playlist,
'phase': 'discovering', # fresh -> discovering -> discovered -> syncing -> sync_complete -> downloading -> download_complete
'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
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}")
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):
"""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>
</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>
</div>
`;
@ -6436,13 +6439,7 @@ async function handleTidalCardClick(playlistId) {
console.log(`🎵 Using pre-loaded Tidal playlist data for: ${state.playlist.name}`);
console.log(`🎵 Ready with ${state.playlist.tracks.length} Tidal tracks for discovery`);
// Update phase to discovering
state.phase = 'discovering';
// Update card to show discovering state
updateTidalCardPhase(playlistId, 'discovering');
// Open YouTube discovery modal but with Tidal data (exact sync.py pattern)
// Open discovery modal - phase will be updated when discovery actually starts
openTidalDiscoveryModal(playlistId, state.playlist);
} 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
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 isAlreadyDiscovered = tidalCardState && tidalCardState.phase === 'discovered';
const isCurrentlyDiscovering = tidalCardState && tidalCardState.phase === 'discovering';
// Prepare discovery results in the correct format for modal
let transformedResults = [];
let actualMatches = 0;
if (isAlreadyDiscovered && tidalCardState.discovery_results) {
transformedResults = tidalCardState.discovery_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 : '-'
}));
transformedResults = tidalCardState.discovery_results.map((result, index) => {
const isFound = result.status === 'found';
if (isFound) actualMatches++;
return {
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: 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] = {
phase: isAlreadyDiscovered ? 'discovered' : 'discovering',
phase: modalPhase,
playlist: {
name: playlistData.name,
tracks: playlistData.tracks
},
is_tidal_playlist: true, // Flag to identify this as Tidal
tidal_playlist_id: playlistId,
discovery_progress: isAlreadyDiscovered ? (tidalCardState.discovery_progress || 100) : 0,
spotify_matches: isAlreadyDiscovered ? (tidalCardState.spotify_matches || 0) : 0,
discovery_progress: isAlreadyDiscovered ? 100 : 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,
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
if (!isAlreadyDiscovered) {
// 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}`);
@ -6534,6 +6542,13 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
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);
@ -6541,6 +6556,10 @@ async function openTidalDiscoveryModal(playlistId, playlistData) {
console.error('❌ Error starting Tidal discovery:', 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 {
console.log('✅ Using existing discovery results - no need to re-discover');
}
@ -6569,34 +6588,38 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
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
const state = youtubePlaylistStates[fakeUrlHash];
if (state) {
state.discovery_progress = status.progress;
state.spotify_matches = status.spotify_matches;
state.discovery_results = status.results;
state.discovery_progress = status.progress; // Backend format
state.discoveryProgress = status.progress; // Frontend format - for modal progress display
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;
// 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)
updateYouTubeDiscoveryModal(fakeUrlHash, transformedStatus);
// Update Tidal card phase and save discovery results
// Update Tidal card phase and save discovery results FIRST
if (tidalPlaylistStates[playlistId]) {
tidalPlaylistStates[playlistId].phase = status.phase;
tidalPlaylistStates[playlistId].discovery_results = status.results;
@ -6605,6 +6628,9 @@ function startTidalDiscoveryPolling(fakeUrlHash, playlistId) {
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)`);
}
@ -6688,9 +6714,42 @@ async function applyTidalPlaylistState(stateInfo) {
tidalPlaylistStates[playlist_id].discovery_results = discovery_results;
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
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}`);
} 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)
function startTidalPlaylistSync(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 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
progressBar.style.width = `${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
status.results.forEach(result => {
const row = document.getElementById(`discovery-row-${urlHash}-${result.index}`);
@ -7682,6 +7789,18 @@ function updateYouTubeDiscoveryModal(urlHash, status) {
spotifyArtistCell.textContent = result.spotify_artist || '-';
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) {

View file

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