listenbrainz fully connected to discovery modal
This commit is contained in:
parent
ffe02d5330
commit
a61c581c25
2 changed files with 336 additions and 206 deletions
|
|
@ -16777,9 +16777,10 @@ def get_listenbrainz_playlist_state(playlist_mbid):
|
||||||
'spotify_matches': state['spotify_matches'],
|
'spotify_matches': state['spotify_matches'],
|
||||||
'spotify_total': state['spotify_total'],
|
'spotify_total': state['spotify_total'],
|
||||||
'discovery_results': state['discovery_results'],
|
'discovery_results': state['discovery_results'],
|
||||||
'sync_playlist_id': state['sync_playlist_id'],
|
'sync_playlist_id': state.get('sync_playlist_id'),
|
||||||
'converted_spotify_playlist_id': state['converted_spotify_playlist_id'],
|
'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'),
|
||||||
'sync_progress': state['sync_progress'],
|
'download_process_id': state.get('download_process_id'),
|
||||||
|
'sync_progress': state.get('sync_progress', {}),
|
||||||
'created_at': state['created_at'],
|
'created_at': state['created_at'],
|
||||||
'last_accessed': state['last_accessed']
|
'last_accessed': state['last_accessed']
|
||||||
}
|
}
|
||||||
|
|
@ -16931,6 +16932,44 @@ def get_listenbrainz_discovery_status(playlist_mbid):
|
||||||
print(f"❌ Error getting ListenBrainz discovery status: {e}")
|
print(f"❌ Error getting ListenBrainz discovery status: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/listenbrainz/update-phase/<playlist_mbid>', methods=['POST'])
|
||||||
|
def update_listenbrainz_phase(playlist_mbid):
|
||||||
|
"""Update ListenBrainz playlist phase (for phase transitions and persistence)"""
|
||||||
|
try:
|
||||||
|
if playlist_mbid not in listenbrainz_playlist_states:
|
||||||
|
return jsonify({"error": "ListenBrainz playlist not found"}), 404
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
new_phase = data.get('phase')
|
||||||
|
|
||||||
|
if not new_phase:
|
||||||
|
return jsonify({"error": "Phase is required"}), 400
|
||||||
|
|
||||||
|
state = listenbrainz_playlist_states[playlist_mbid]
|
||||||
|
state['phase'] = new_phase
|
||||||
|
state['last_accessed'] = time.time()
|
||||||
|
|
||||||
|
# Update download process ID if provided (for download persistence)
|
||||||
|
if 'download_process_id' in data:
|
||||||
|
state['download_process_id'] = data['download_process_id']
|
||||||
|
logger.info(f"🎵 Updated ListenBrainz download_process_id: {data['download_process_id']}")
|
||||||
|
|
||||||
|
# Update converted Spotify playlist ID if provided (for download persistence)
|
||||||
|
if 'converted_spotify_playlist_id' in data:
|
||||||
|
state['converted_spotify_playlist_id'] = data['converted_spotify_playlist_id']
|
||||||
|
logger.info(f"🎵 Updated ListenBrainz converted_spotify_playlist_id: {data['converted_spotify_playlist_id']}")
|
||||||
|
|
||||||
|
logger.info(f"🎵 Updated ListenBrainz playlist {playlist_mbid} phase to: {new_phase}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"phase": new_phase
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error updating ListenBrainz playlist phase: {e}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/listenbrainz/discovery/update_match', methods=['POST'])
|
@app.route('/api/listenbrainz/discovery/update_match', methods=['POST'])
|
||||||
def update_listenbrainz_discovery_match():
|
def update_listenbrainz_discovery_match():
|
||||||
"""Update a ListenBrainz discovery result with manually selected Spotify track"""
|
"""Update a ListenBrainz discovery result with manually selected Spotify track"""
|
||||||
|
|
|
||||||
|
|
@ -3094,7 +3094,7 @@ async function loadListenBrainzPlaylistsFromBackend() {
|
||||||
for (const playlistInfo of playlists) {
|
for (const playlistInfo of playlists) {
|
||||||
const playlistMbid = playlistInfo.playlist_mbid;
|
const playlistMbid = playlistInfo.playlist_mbid;
|
||||||
|
|
||||||
console.log(`🎵 Hydrating ListenBrainz playlist: ${playlistInfo.playlist.name} (Phase: ${playlistInfo.phase})`);
|
console.log(`🎵 Hydrating ListenBrainz playlist: ${playlistInfo.playlist.name} (Phase: ${playlistInfo.phase}, MBID: ${playlistMbid})`);
|
||||||
|
|
||||||
// Fetch full state for non-fresh playlists to restore discovery results
|
// Fetch full state for non-fresh playlists to restore discovery results
|
||||||
if (playlistInfo.phase !== 'fresh') {
|
if (playlistInfo.phase !== 'fresh') {
|
||||||
|
|
@ -3155,6 +3155,15 @@ async function loadListenBrainzPlaylistsFromBackend() {
|
||||||
console.log(`🔄 [Backend Loading] Auto-starting polling for discovering playlist: ${playlistInfo.playlist.name}`);
|
console.log(`🔄 [Backend Loading] Auto-starting polling for discovering playlist: ${playlistInfo.playlist.name}`);
|
||||||
startListenBrainzDiscoveryPolling(playlistInfo.playlist_mbid);
|
startListenBrainzDiscoveryPolling(playlistInfo.playlist_mbid);
|
||||||
}
|
}
|
||||||
|
// Show sync button for discovered playlists (hidden by default)
|
||||||
|
else if (playlistInfo.phase === 'discovered' || playlistInfo.phase === 'syncing' || playlistInfo.phase === 'sync_complete') {
|
||||||
|
const playlistId = `discover-lb-playlist-${playlistInfo.playlist_mbid}`;
|
||||||
|
const syncBtn = document.getElementById(`${playlistId}-sync-btn`);
|
||||||
|
if (syncBtn) {
|
||||||
|
syncBtn.style.display = 'inline-block';
|
||||||
|
console.log(`✅ Showing sync button for discovered playlist: ${playlistInfo.playlist.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listenbrainzPlaylistsLoaded = true;
|
listenbrainzPlaylistsLoaded = true;
|
||||||
|
|
@ -4746,6 +4755,45 @@ async function closeDownloadMissingModal(playlistId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset ListenBrainz playlist phase to 'discovered' when modal is closed
|
||||||
|
if (playlistId.startsWith('listenbrainz_')) {
|
||||||
|
const playlistMbid = playlistId.replace('listenbrainz_', '');
|
||||||
|
|
||||||
|
console.log(`🧹 [Modal Close] Processing ListenBrainz playlist close: playlistId="${playlistId}", mbid="${playlistMbid}"`);
|
||||||
|
|
||||||
|
// Clear download-specific state but preserve discovery results
|
||||||
|
if (listenbrainzPlaylistStates[playlistMbid]) {
|
||||||
|
const currentPhase = listenbrainzPlaylistStates[playlistMbid].phase;
|
||||||
|
console.log(`🧹 [Modal Close] Current phase before reset: ${currentPhase}`);
|
||||||
|
|
||||||
|
// Reset to discovered phase (unless download actually completed successfully)
|
||||||
|
if (currentPhase !== 'download_complete') {
|
||||||
|
// Clear download-specific fields
|
||||||
|
delete listenbrainzPlaylistStates[playlistMbid].download_process_id;
|
||||||
|
delete listenbrainzPlaylistStates[playlistMbid].convertedSpotifyPlaylistId;
|
||||||
|
|
||||||
|
// Set back to discovered
|
||||||
|
listenbrainzPlaylistStates[playlistMbid].phase = 'discovered';
|
||||||
|
|
||||||
|
// Update backend state
|
||||||
|
try {
|
||||||
|
await fetch(`/api/listenbrainz/update-phase/${playlistMbid}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phase: 'discovered' })
|
||||||
|
});
|
||||||
|
console.log(`✅ [Modal Close] Updated backend phase for ListenBrainz playlist ${playlistMbid} to 'discovered'`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [Modal Close] Error updating backend phase for ListenBrainz playlist ${playlistMbid}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔄 [Modal Close] Reset ListenBrainz playlist ${playlistMbid} to discovered phase`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`❌ [Modal Close] No ListenBrainz state found for mbid: ${playlistMbid}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear wishlist modal state when modal is fully closed
|
// Clear wishlist modal state when modal is fully closed
|
||||||
if (playlistId === 'wishlist') {
|
if (playlistId === 'wishlist') {
|
||||||
WishlistModalState.clear(); // Clear all tracking since modal is fully closed
|
WishlistModalState.clear(); // Clear all tracking since modal is fully closed
|
||||||
|
|
@ -5511,6 +5559,30 @@ async function startMissingTracksProcess(playlistId) {
|
||||||
console.log(`🔄 Updated Beatport chart ${chartHash} to downloading phase`);
|
console.log(`🔄 Updated Beatport chart ${chartHash} to downloading phase`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update ListenBrainz playlist phase to 'downloading' if this is a ListenBrainz playlist
|
||||||
|
if (playlistId.startsWith('listenbrainz_')) {
|
||||||
|
const playlistMbid = playlistId.replace('listenbrainz_', '');
|
||||||
|
const state = listenbrainzPlaylistStates[playlistMbid];
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
// Update frontend state
|
||||||
|
state.phase = 'downloading';
|
||||||
|
|
||||||
|
// Update backend state
|
||||||
|
try {
|
||||||
|
fetch(`/api/listenbrainz/update-phase/${playlistMbid}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phase: 'downloading' })
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Error updating backend ListenBrainz phase to downloading:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔄 Updated ListenBrainz playlist ${playlistMbid} to downloading phase`);
|
||||||
|
}
|
||||||
|
}
|
||||||
document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none';
|
document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none';
|
||||||
document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block';
|
document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block';
|
||||||
|
|
||||||
|
|
@ -5605,6 +5677,33 @@ async function startMissingTracksProcess(playlistId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update ListenBrainz backend state with download_process_id and convertedSpotifyPlaylistId
|
||||||
|
if (playlistId.startsWith('listenbrainz_')) {
|
||||||
|
const playlistMbid = playlistId.replace('listenbrainz_', '');
|
||||||
|
const state = listenbrainzPlaylistStates[playlistMbid];
|
||||||
|
if (state) {
|
||||||
|
// Store in frontend state
|
||||||
|
state.download_process_id = data.batch_id;
|
||||||
|
state.convertedSpotifyPlaylistId = playlistId;
|
||||||
|
|
||||||
|
// Update backend state
|
||||||
|
try {
|
||||||
|
fetch(`/api/listenbrainz/update-phase/${playlistMbid}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
phase: 'downloading',
|
||||||
|
download_process_id: data.batch_id,
|
||||||
|
converted_spotify_playlist_id: playlistId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log(`🔄 Updated ListenBrainz backend with download_process_id: ${data.batch_id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Error updating ListenBrainz backend with download_process_id:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
startModalDownloadPolling(playlistId);
|
startModalDownloadPolling(playlistId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(`Failed to start process: ${error.message}`, 'error');
|
showToast(`Failed to start process: ${error.message}`, 'error');
|
||||||
|
|
@ -16819,7 +16918,19 @@ function startListenBrainzDiscoveryPolling(playlistMbid) {
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
delete activeYouTubePollers[playlistMbid];
|
delete activeYouTubePollers[playlistMbid];
|
||||||
|
|
||||||
// Update phase
|
// Update phase in backend for persistence (like Beatport does)
|
||||||
|
try {
|
||||||
|
await fetch(`/api/listenbrainz/update-phase/${playlistMbid}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phase: 'discovered' })
|
||||||
|
});
|
||||||
|
console.log('✅ Updated ListenBrainz backend phase to discovered');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Failed to update backend phase:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update phase in frontend state
|
||||||
if (listenbrainzPlaylistStates[playlistMbid]) {
|
if (listenbrainzPlaylistStates[playlistMbid]) {
|
||||||
listenbrainzPlaylistStates[playlistMbid].phase = 'discovered';
|
listenbrainzPlaylistStates[playlistMbid].phase = 'discovered';
|
||||||
}
|
}
|
||||||
|
|
@ -16827,6 +16938,14 @@ function startListenBrainzDiscoveryPolling(playlistMbid) {
|
||||||
// Update modal buttons to show sync and download buttons
|
// Update modal buttons to show sync and download buttons
|
||||||
updateYouTubeModalButtons(playlistMbid, 'discovered');
|
updateYouTubeModalButtons(playlistMbid, 'discovered');
|
||||||
|
|
||||||
|
// Show sync button in playlist listing (hidden by default until discovered)
|
||||||
|
const playlistId = `discover-lb-playlist-${playlistMbid}`;
|
||||||
|
const syncBtn = document.getElementById(`${playlistId}-sync-btn`);
|
||||||
|
if (syncBtn) {
|
||||||
|
syncBtn.style.display = 'inline-block';
|
||||||
|
console.log('✅ Showing sync button after discovery completion');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('✅ ListenBrainz discovery complete:', playlistMbid);
|
console.log('✅ ListenBrainz discovery complete:', playlistMbid);
|
||||||
showToast('ListenBrainz discovery complete!', 'success');
|
showToast('ListenBrainz discovery complete!', 'success');
|
||||||
}
|
}
|
||||||
|
|
@ -16962,6 +17081,22 @@ async function startListenBrainzPlaylistSync(playlistMbid) {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Starting ListenBrainz sync for:', state.playlist.name);
|
console.log('🔄 Starting ListenBrainz sync for:', state.playlist.name);
|
||||||
|
|
||||||
|
// Check if being called from playlist listing (has UI elements) or modal
|
||||||
|
const listingPlaylistId = `discover-lb-playlist-${playlistMbid}`;
|
||||||
|
const statusDisplay = document.getElementById(`${listingPlaylistId}-sync-status`);
|
||||||
|
const isFromListing = statusDisplay !== null;
|
||||||
|
|
||||||
|
if (isFromListing) {
|
||||||
|
console.log('🔄 Sync initiated from playlist listing');
|
||||||
|
// Show status display in listing
|
||||||
|
statusDisplay.style.display = 'block';
|
||||||
|
const syncButton = document.getElementById(`${listingPlaylistId}-sync-btn`);
|
||||||
|
if (syncButton) {
|
||||||
|
syncButton.disabled = true;
|
||||||
|
syncButton.style.opacity = '0.5';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Call backend to start sync
|
// Call backend to start sync
|
||||||
const response = await fetch(`/api/listenbrainz/sync/start/${playlistMbid}`, {
|
const response = await fetch(`/api/listenbrainz/sync/start/${playlistMbid}`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
|
@ -16976,10 +17111,12 @@ async function startListenBrainzPlaylistSync(playlistMbid) {
|
||||||
state.phase = 'syncing';
|
state.phase = 'syncing';
|
||||||
|
|
||||||
// Start polling for sync progress
|
// Start polling for sync progress
|
||||||
startListenBrainzSyncPolling(playlistMbid);
|
if (isFromListing) {
|
||||||
|
startListenBrainzListingSyncPolling(playlistMbid, listingPlaylistId);
|
||||||
// Update modal
|
} else {
|
||||||
updateYouTubeModalButtons(playlistMbid, 'syncing');
|
startListenBrainzSyncPolling(playlistMbid);
|
||||||
|
updateYouTubeModalButtons(playlistMbid, 'syncing');
|
||||||
|
}
|
||||||
|
|
||||||
showToast('Starting ListenBrainz sync...', 'info');
|
showToast('Starting ListenBrainz sync...', 'info');
|
||||||
|
|
||||||
|
|
@ -16989,6 +17126,86 @@ async function startListenBrainzPlaylistSync(playlistMbid) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startListenBrainzListingSyncPolling(playlistMbid, listingPlaylistId) {
|
||||||
|
console.log(`🔄 Starting listing sync polling for: ${playlistMbid} (UI: ${listingPlaylistId})`);
|
||||||
|
|
||||||
|
// Stop any existing polling
|
||||||
|
if (activeYouTubePollers[playlistMbid]) {
|
||||||
|
clearInterval(activeYouTubePollers[playlistMbid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/listenbrainz/sync/status/${playlistMbid}`);
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
if (status.error) {
|
||||||
|
console.error('❌ Error polling ListenBrainz sync status:', status.error);
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
delete activeYouTubePollers[playlistMbid];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI elements in listing
|
||||||
|
const totalEl = document.getElementById(`${listingPlaylistId}-sync-total`);
|
||||||
|
const matchedEl = document.getElementById(`${listingPlaylistId}-sync-matched`);
|
||||||
|
const failedEl = document.getElementById(`${listingPlaylistId}-sync-failed`);
|
||||||
|
const percentageEl = document.getElementById(`${listingPlaylistId}-sync-percentage`);
|
||||||
|
|
||||||
|
console.log(`📊 ListenBrainz listing sync progress:`, {
|
||||||
|
total: status.progress?.total_tracks,
|
||||||
|
matched: status.progress?.matched_tracks,
|
||||||
|
failed: status.progress?.failed_tracks,
|
||||||
|
complete: status.complete
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalEl) totalEl.textContent = status.progress?.total_tracks || 0;
|
||||||
|
if (matchedEl) matchedEl.textContent = status.progress?.matched_tracks || 0;
|
||||||
|
if (failedEl) failedEl.textContent = status.progress?.failed_tracks || 0;
|
||||||
|
|
||||||
|
const percentage = status.progress?.total_tracks > 0
|
||||||
|
? Math.round(((status.progress?.matched_tracks || 0) / status.progress.total_tracks) * 100)
|
||||||
|
: 0;
|
||||||
|
if (percentageEl) percentageEl.textContent = percentage;
|
||||||
|
|
||||||
|
// Check if complete
|
||||||
|
if (status.complete) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
delete activeYouTubePollers[playlistMbid];
|
||||||
|
|
||||||
|
const statusDisplay = document.getElementById(`${listingPlaylistId}-sync-status`);
|
||||||
|
const syncButton = document.getElementById(`${listingPlaylistId}-sync-btn`);
|
||||||
|
|
||||||
|
if (statusDisplay) {
|
||||||
|
setTimeout(() => {
|
||||||
|
statusDisplay.style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncButton) {
|
||||||
|
syncButton.disabled = false;
|
||||||
|
syncButton.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
if (listenbrainzPlaylistStates[playlistMbid]) {
|
||||||
|
listenbrainzPlaylistStates[playlistMbid].phase = 'sync_complete';
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`Sync complete: ${status.progress?.matched_tracks || 0}/${status.progress?.total_tracks || 0} tracks matched`, 'success');
|
||||||
|
console.log('✅ ListenBrainz listing sync complete:', playlistMbid);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error polling ListenBrainz listing sync:', error);
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
delete activeYouTubePollers[playlistMbid];
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
activeYouTubePollers[playlistMbid] = pollInterval;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ARTISTS PAGE FUNCTIONALITY - ELEGANT SEARCH & DISCOVERY
|
// ARTISTS PAGE FUNCTIONALITY - ELEGANT SEARCH & DISCOVERY
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -28026,7 +28243,7 @@ async function loadListenBrainzTabContent(tabId) {
|
||||||
trackCount = playlistData.track.length;
|
trackCount = playlistData.track.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playlistId = `lb-${tabId}-${index}`;
|
const playlistId = `discover-lb-playlist-${identifier}`; // Use consistent MBID-based ID
|
||||||
const virtualPlaylistId = `discover_lb_${tabId}_${identifier}`;
|
const virtualPlaylistId = `discover_lb_${tabId}_${identifier}`;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
|
|
@ -28045,8 +28262,9 @@ async function loadListenBrainzTabContent(tabId) {
|
||||||
</button>
|
</button>
|
||||||
<button class="action-button primary"
|
<button class="action-button primary"
|
||||||
id="${playlistId}-sync-btn"
|
id="${playlistId}-sync-btn"
|
||||||
onclick="startListenBrainzPlaylistSync('${identifier}', '${escapeHtml(title)}', '${playlistId}')"
|
onclick="startListenBrainzPlaylistSync('${identifier}')"
|
||||||
title="Sync to media server">
|
title="Sync to media server"
|
||||||
|
style="display: none;">
|
||||||
<span class="button-icon">⟳</span>
|
<span class="button-icon">⟳</span>
|
||||||
<span class="button-text">Sync</span>
|
<span class="button-text">Sync</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -28082,7 +28300,7 @@ async function loadListenBrainzTabContent(tabId) {
|
||||||
playlists.forEach((playlist, index) => {
|
playlists.forEach((playlist, index) => {
|
||||||
const playlistData = playlist.playlist || playlist;
|
const playlistData = playlist.playlist || playlist;
|
||||||
const identifier = playlistData.identifier?.split('/').pop() || '';
|
const identifier = playlistData.identifier?.split('/').pop() || '';
|
||||||
const playlistId = `lb-${tabId}-${index}`;
|
const playlistId = `discover-lb-playlist-${identifier}`; // Use consistent MBID-based ID
|
||||||
loadListenBrainzPlaylistTracks(identifier, playlistId);
|
loadListenBrainzPlaylistTracks(identifier, playlistId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -28208,147 +28426,6 @@ function displayListenBrainzTracks(tracks, playlistId) {
|
||||||
playlistContainer.innerHTML = html;
|
playlistContainer.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startListenBrainzPlaylistSync(identifier, title, playlistId) {
|
|
||||||
try {
|
|
||||||
console.log(`🔄 Starting sync for ListenBrainz playlist:`, { identifier, title, playlistId });
|
|
||||||
|
|
||||||
const tracks = listenbrainzTracksCache[identifier];
|
|
||||||
console.log(`📊 Cached tracks for ${identifier}:`, tracks?.length || 0);
|
|
||||||
|
|
||||||
if (!tracks || tracks.length === 0) {
|
|
||||||
console.error('❌ No tracks found in cache');
|
|
||||||
showToast('No tracks to sync', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Found ${tracks.length} tracks to sync`);
|
|
||||||
|
|
||||||
// Convert ListenBrainz tracks to Spotify format (same as other discover playlists)
|
|
||||||
const spotifyTracks = tracks.map(track => ({
|
|
||||||
id: null, // No Spotify ID for ListenBrainz tracks
|
|
||||||
name: track.track_name,
|
|
||||||
artists: [cleanArtistName(track.artist_name)], // Clean featured artists, array of strings for sync compatibility
|
|
||||||
album: {
|
|
||||||
name: track.album_name,
|
|
||||||
images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
|
|
||||||
},
|
|
||||||
duration_ms: track.duration_ms || 0,
|
|
||||||
mbid: track.mbid // Include MusicBrainz ID for matching
|
|
||||||
}));
|
|
||||||
|
|
||||||
const virtualPlaylistId = `discover_lb_${identifier}`;
|
|
||||||
console.log(`🆔 Virtual playlist ID: ${virtualPlaylistId}`);
|
|
||||||
|
|
||||||
// Store in cache for sync function (CRITICAL - same as other discover playlists)
|
|
||||||
playlistTrackCache[virtualPlaylistId] = spotifyTracks;
|
|
||||||
|
|
||||||
// Create virtual playlist object (CRITICAL - same as other discover playlists)
|
|
||||||
const virtualPlaylist = {
|
|
||||||
id: virtualPlaylistId,
|
|
||||||
name: title,
|
|
||||||
track_count: spotifyTracks.length
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add to spotify playlists array if not already there (CRITICAL)
|
|
||||||
if (!spotifyPlaylists.find(p => p.id === virtualPlaylistId)) {
|
|
||||||
spotifyPlaylists.push(virtualPlaylist);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show sync status display
|
|
||||||
const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
|
|
||||||
const syncButton = document.getElementById(`${playlistId}-sync-btn`);
|
|
||||||
|
|
||||||
if (statusDisplay) statusDisplay.style.display = 'block';
|
|
||||||
if (syncButton) {
|
|
||||||
syncButton.disabled = true;
|
|
||||||
syncButton.style.opacity = '0.5';
|
|
||||||
syncButton.style.cursor = 'not-allowed';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the same sync function as all other discover playlists
|
|
||||||
await startPlaylistSync(virtualPlaylistId);
|
|
||||||
|
|
||||||
// Extract image URL from first track for download bar bubble
|
|
||||||
let imageUrl = null;
|
|
||||||
if (spotifyTracks && spotifyTracks.length > 0) {
|
|
||||||
const firstTrack = spotifyTracks[0];
|
|
||||||
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
|
|
||||||
imageUrl = firstTrack.album.images[0].url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to discover download bar
|
|
||||||
addDiscoverDownload(virtualPlaylistId, title, 'listenbrainz', imageUrl);
|
|
||||||
|
|
||||||
// Start polling for progress updates (using discover playlist pattern)
|
|
||||||
startListenBrainzSyncPolling(playlistId, virtualPlaylistId);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error starting ListenBrainz playlist sync:', error);
|
|
||||||
showToast('Failed to start sync', 'error');
|
|
||||||
|
|
||||||
const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
|
|
||||||
const syncButton = document.getElementById(`${playlistId}-sync-btn`);
|
|
||||||
if (statusDisplay) statusDisplay.style.display = 'none';
|
|
||||||
if (syncButton) {
|
|
||||||
syncButton.disabled = false;
|
|
||||||
syncButton.style.opacity = '1';
|
|
||||||
syncButton.style.cursor = 'pointer';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startListenBrainzSyncPolling(playlistId, virtualPlaylistId) {
|
|
||||||
const pollInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/sync/status/${virtualPlaylistId}`);
|
|
||||||
if (!response.ok) {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await response.json();
|
|
||||||
|
|
||||||
// Update UI
|
|
||||||
const totalEl = document.getElementById(`${playlistId}-sync-total`);
|
|
||||||
const matchedEl = document.getElementById(`${playlistId}-sync-matched`);
|
|
||||||
const failedEl = document.getElementById(`${playlistId}-sync-failed`);
|
|
||||||
const percentageEl = document.getElementById(`${playlistId}-sync-percentage`);
|
|
||||||
|
|
||||||
if (totalEl) totalEl.textContent = status.total_tracks || 0;
|
|
||||||
if (matchedEl) matchedEl.textContent = status.matched_tracks || 0;
|
|
||||||
if (failedEl) failedEl.textContent = status.failed_tracks || 0;
|
|
||||||
|
|
||||||
const percentage = status.total_tracks > 0
|
|
||||||
? Math.round(((status.matched_tracks || 0) / status.total_tracks) * 100)
|
|
||||||
: 0;
|
|
||||||
if (percentageEl) percentageEl.textContent = percentage;
|
|
||||||
|
|
||||||
// Check if complete
|
|
||||||
if (status.is_complete) {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
|
|
||||||
const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
|
|
||||||
const syncButton = document.getElementById(`${playlistId}-sync-btn`);
|
|
||||||
|
|
||||||
if (statusDisplay) {
|
|
||||||
setTimeout(() => {
|
|
||||||
statusDisplay.style.display = 'none';
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncButton) syncButton.disabled = false;
|
|
||||||
|
|
||||||
showToast(`Sync complete: ${status.matched_tracks}/${status.total_tracks} tracks matched`, 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error polling sync status:', error);
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDownloadModalForListenBrainzPlaylist(identifier, title) {
|
async function openDownloadModalForListenBrainzPlaylist(identifier, title) {
|
||||||
try {
|
try {
|
||||||
const tracks = listenbrainzTracksCache[identifier];
|
const tracks = listenbrainzTracksCache[identifier];
|
||||||
|
|
@ -28358,9 +28435,12 @@ async function openDownloadModalForListenBrainzPlaylist(identifier, title) {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🎵 Opening ListenBrainz discovery modal: ${title}`);
|
console.log(`🎵 Opening ListenBrainz discovery modal: ${title}`);
|
||||||
|
console.log(`🔍 Looking for existing state with identifier: ${identifier}`);
|
||||||
|
console.log(`📋 All ListenBrainz states:`, Object.keys(listenbrainzPlaylistStates));
|
||||||
|
|
||||||
// Check if state already exists from backend hydration (like Beatport does)
|
// Check if state already exists from backend hydration (like Beatport does)
|
||||||
const existingState = listenbrainzPlaylistStates[identifier];
|
const existingState = listenbrainzPlaylistStates[identifier];
|
||||||
|
console.log(`🔍 Existing state found:`, existingState ? `Phase: ${existingState.phase}` : 'None');
|
||||||
|
|
||||||
if (existingState && existingState.phase !== 'fresh') {
|
if (existingState && existingState.phase !== 'fresh') {
|
||||||
// State exists - rehydrate the modal with existing data
|
// State exists - rehydrate the modal with existing data
|
||||||
|
|
@ -28376,66 +28456,77 @@ async function openDownloadModalForListenBrainzPlaylist(identifier, title) {
|
||||||
const convertedPlaylistId = existingState.convertedSpotifyPlaylistId;
|
const convertedPlaylistId = existingState.convertedSpotifyPlaylistId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if modal already exists (user just closed it)
|
||||||
|
if (activeDownloadProcesses[convertedPlaylistId]) {
|
||||||
|
console.log(`✅ Download modal already exists, just showing it`);
|
||||||
|
const process = activeDownloadProcesses[convertedPlaylistId];
|
||||||
|
if (process.modalElement) {
|
||||||
|
process.modalElement.style.display = 'flex';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the download modal using the ListenBrainz state
|
// Create the download modal using the ListenBrainz state
|
||||||
if (!activeDownloadProcesses[convertedPlaylistId]) {
|
console.log(`🆕 Creating new download modal for rehydration`);
|
||||||
// Get tracks from the existing state
|
// Get tracks from the existing state
|
||||||
let spotifyTracks = [];
|
let spotifyTracks = [];
|
||||||
|
|
||||||
if (existingState && existingState.discovery_results) {
|
if (existingState && existingState.discovery_results) {
|
||||||
spotifyTracks = existingState.discovery_results
|
spotifyTracks = existingState.discovery_results
|
||||||
.filter(result => result.spotify_data)
|
.filter(result => result.spotify_data)
|
||||||
.map(result => {
|
.map(result => {
|
||||||
const track = result.spotify_data;
|
const track = result.spotify_data;
|
||||||
// Ensure artists is an array of strings
|
// Ensure artists is an array of strings
|
||||||
if (track.artists && Array.isArray(track.artists)) {
|
if (track.artists && Array.isArray(track.artists)) {
|
||||||
track.artists = track.artists.map(artist =>
|
track.artists = track.artists.map(artist =>
|
||||||
typeof artist === 'string' ? artist : (artist.name || artist)
|
typeof artist === 'string' ? artist : (artist.name || artist)
|
||||||
);
|
);
|
||||||
} else if (track.artists && typeof track.artists === 'string') {
|
} else if (track.artists && typeof track.artists === 'string') {
|
||||||
track.artists = [track.artists];
|
track.artists = [track.artists];
|
||||||
} else {
|
} else {
|
||||||
track.artists = ['Unknown Artist'];
|
track.artists = ['Unknown Artist'];
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: track.id,
|
id: track.id,
|
||||||
name: track.name,
|
name: track.name,
|
||||||
artists: track.artists,
|
artists: track.artists,
|
||||||
album: track.album || 'Unknown Album',
|
album: track.album || 'Unknown Album',
|
||||||
duration_ms: track.duration_ms || 0,
|
duration_ms: track.duration_ms || 0,
|
||||||
external_urls: track.external_urls || {}
|
external_urls: track.external_urls || {}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spotifyTracks.length > 0) {
|
if (spotifyTracks.length > 0) {
|
||||||
await openDownloadMissingModalForYouTube(
|
await openDownloadMissingModalForYouTube(
|
||||||
convertedPlaylistId,
|
convertedPlaylistId,
|
||||||
`[ListenBrainz] ${title}`,
|
`[ListenBrainz] ${title}`,
|
||||||
spotifyTracks
|
spotifyTracks
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the modal to running state with the correct batch ID
|
// Set the modal to running state with the correct batch ID
|
||||||
const process = activeDownloadProcesses[convertedPlaylistId];
|
const process = activeDownloadProcesses[convertedPlaylistId];
|
||||||
if (process) {
|
if (process) {
|
||||||
process.status = existingState.phase === 'download_complete' ? 'complete' : 'running';
|
process.status = existingState.phase === 'download_complete' ? 'complete' : 'running';
|
||||||
process.batchId = existingState.download_process_id;
|
process.batchId = existingState.download_process_id;
|
||||||
|
|
||||||
// Update UI to running state
|
// Update UI to running state
|
||||||
const beginBtn = document.getElementById(`begin-analysis-btn-${convertedPlaylistId}`);
|
const beginBtn = document.getElementById(`begin-analysis-btn-${convertedPlaylistId}`);
|
||||||
const cancelBtn = document.getElementById(`cancel-all-btn-${convertedPlaylistId}`);
|
const cancelBtn = document.getElementById(`cancel-all-btn-${convertedPlaylistId}`);
|
||||||
if (beginBtn) beginBtn.style.display = 'none';
|
if (beginBtn) beginBtn.style.display = 'none';
|
||||||
if (cancelBtn) cancelBtn.style.display = 'inline-block';
|
if (cancelBtn) cancelBtn.style.display = 'inline-block';
|
||||||
|
|
||||||
// Start polling for this process
|
// Start polling for this process
|
||||||
startModalDownloadPolling(convertedPlaylistId);
|
startModalDownloadPolling(convertedPlaylistId);
|
||||||
|
|
||||||
// Hide modal since this is background rehydration
|
// Show modal since user clicked the download button (different from background rehydration)
|
||||||
process.modalElement.style.display = 'none';
|
if (process.modalElement) {
|
||||||
console.log(`✅ Rehydrated download modal for ListenBrainz playlist: ${title}`);
|
process.modalElement.style.display = 'flex';
|
||||||
}
|
}
|
||||||
} else {
|
console.log(`✅ Rehydrated download modal for ListenBrainz playlist: ${title}`);
|
||||||
console.warn(`⚠️ No Spotify tracks found for ListenBrainz download modal: ${title}`);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ No Spotify tracks found for ListenBrainz download modal: ${title}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`⚠️ Error setting up download process for ListenBrainz playlist "${title}":`, error.message);
|
console.warn(`⚠️ Error setting up download process for ListenBrainz playlist "${title}":`, error.message);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue