fix sync for listenbrainz

This commit is contained in:
Broque Thomas 2025-11-18 10:30:53 -08:00
parent 333d984527
commit c281382a3b
2 changed files with 120 additions and 46 deletions

View file

@ -15285,9 +15285,13 @@ def get_listenbrainz_playlist_tracks(playlist_mbid):
# Extract tracks from JSPF format # Extract tracks from JSPF format
jspf_tracks = playlist.get('track', []) jspf_tracks = playlist.get('track', [])
# Convert to our standard format # Convert to our standard format - prepare tracks first without cover art
tracks = [] tracks = []
for track in jspf_tracks: print(f"🎵 Processing {len(jspf_tracks)} tracks from playlist")
# First pass: extract all track data without cover art
track_data_list = []
for idx, track in enumerate(jspf_tracks):
# Get recording MBID from identifier # Get recording MBID from identifier
recording_mbid = None recording_mbid = None
identifiers = track.get('identifier', []) identifiers = track.get('identifier', [])
@ -15300,10 +15304,23 @@ def get_listenbrainz_playlist_tracks(playlist_mbid):
extension = track.get('extension', {}) extension = track.get('extension', {})
mb_data = extension.get('https://musicbrainz.org/doc/jspf#track', {}) mb_data = extension.get('https://musicbrainz.org/doc/jspf#track', {})
# Extract album cover from extension if available if idx == 0:
album_cover_url = None print(f"📋 Sample track extension data: {extension}")
if mb_data and 'album_cover_url' in mb_data: print(f"📋 Sample mb_data keys: {mb_data.keys() if mb_data else 'None'}")
album_cover_url = mb_data['album_cover_url']
# Extract release MBID for cover art
release_mbid = None
if mb_data:
# Check in additional_metadata first
additional_metadata = mb_data.get('additional_metadata', {})
if 'caa_release_mbid' in additional_metadata:
release_mbid = additional_metadata['caa_release_mbid']
# Fallback to top-level release_mbid
elif 'release_mbid' in mb_data:
release_mbid = mb_data['release_mbid']
if idx == 0:
print(f"🆔 First track release_mbid: {release_mbid}")
track_data = { track_data = {
'track_name': track.get('title', 'Unknown Track'), 'track_name': track.get('title', 'Unknown Track'),
@ -15311,11 +15328,66 @@ def get_listenbrainz_playlist_tracks(playlist_mbid):
'album_name': track.get('album', 'Unknown Album'), 'album_name': track.get('album', 'Unknown Album'),
'duration_ms': track.get('duration', 0), 'duration_ms': track.get('duration', 0),
'mbid': recording_mbid, 'mbid': recording_mbid,
'album_cover_url': album_cover_url, 'release_mbid': release_mbid,
'album_cover_url': None, # Will be fetched in parallel
'additional_metadata': mb_data 'additional_metadata': mb_data
} }
tracks.append(track_data) track_data_list.append(track_data)
# Second pass: fetch cover art in parallel using threading (much faster)
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fetch_cover_art(track_data):
"""Fetch cover art for a single track"""
release_mbid = track_data.get('release_mbid')
if not release_mbid:
return None
try:
cover_art_url = f"https://coverartarchive.org/release/{release_mbid}"
cover_response = requests.get(cover_art_url, timeout=3)
if cover_response.status_code == 200:
cover_data = cover_response.json()
images = cover_data.get('images', [])
# Get front cover
for img in images:
if img.get('front'):
return img.get('thumbnails', {}).get('small') or img.get('image')
# Fallback to first image
if images:
return images[0].get('thumbnails', {}).get('small') or images[0].get('image')
except:
pass
return None
print(f"🎨 Fetching cover art for {len(track_data_list)} tracks in parallel...")
start_time = time.time()
# Fetch up to 10 covers at a time
with ThreadPoolExecutor(max_workers=10) as executor:
future_to_track = {executor.submit(fetch_cover_art, track): idx
for idx, track in enumerate(track_data_list)}
for future in as_completed(future_to_track):
idx = future_to_track[future]
try:
cover_url = future.result()
if cover_url:
track_data_list[idx]['album_cover_url'] = cover_url
except Exception as e:
pass
elapsed = time.time() - start_time
covers_found = sum(1 for t in track_data_list if t.get('album_cover_url'))
print(f"✅ Fetched {covers_found}/{len(track_data_list)} covers in {elapsed:.2f}s")
tracks = track_data_list
return jsonify({ return jsonify({
"success": True, "success": True,

View file

@ -26590,54 +26590,52 @@ async function startListenBrainzPlaylistSync(identifier, title, playlistId) {
console.log(`✅ Found ${tracks.length} tracks to sync`); console.log(`✅ Found ${tracks.length} tracks to sync`);
// Show sync status // Convert ListenBrainz tracks to Spotify format (same as other discover playlists)
const statusDisplay = document.getElementById(`${playlistId}-sync-status`); const spotifyTracks = tracks.map(track => ({
const syncButton = document.getElementById(`${playlistId}-sync-btn`); id: null, // No Spotify ID for ListenBrainz tracks
console.log('UI Elements:', {
statusDisplay: !!statusDisplay,
syncButton: !!syncButton
});
if (statusDisplay) statusDisplay.style.display = 'block';
if (syncButton) syncButton.disabled = true;
// Prepare tracks for sync
const tracksForSync = tracks.map(track => ({
name: track.track_name, name: track.track_name,
artist: track.artist_name, artists: [track.artist_name], // Array of strings for sync compatibility
album: track.album_name, album: {
mbid: track.mbid || null, name: track.album_name,
duration_ms: track.duration_ms || 0 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}`; const virtualPlaylistId = `discover_lb_${identifier}`;
console.log(`🆔 Virtual playlist ID: ${virtualPlaylistId}`); console.log(`🆔 Virtual playlist ID: ${virtualPlaylistId}`);
// Start sync // Store in cache for sync function (CRITICAL - same as other discover playlists)
console.log('📤 Sending sync request...'); playlistTrackCache[virtualPlaylistId] = spotifyTracks;
const response = await fetch('/api/sync/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
virtual_playlist_id: virtualPlaylistId,
playlist_name: title,
tracks: tracksForSync
})
});
console.log(`📡 Response status: ${response.status}`); // Create virtual playlist object (CRITICAL - same as other discover playlists)
const result = await response.json(); const virtualPlaylist = {
console.log('📋 Response data:', result); id: virtualPlaylistId,
name: title,
track_count: spotifyTracks.length
};
if (!result.success) { // Add to spotify playlists array if not already there (CRITICAL)
throw new Error(result.error || 'Failed to start sync'); if (!spotifyPlaylists.find(p => p.id === virtualPlaylistId)) {
spotifyPlaylists.push(virtualPlaylist);
} }
console.log(`✅ Sync started successfully for ${title}`); // Show sync status display
showToast(`Syncing "${title}" to media server`, 'success'); const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
const syncButton = document.getElementById(`${playlistId}-sync-btn`);
// Start polling for status 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);
// Start polling for progress updates (using discover playlist pattern)
startListenBrainzSyncPolling(playlistId, virtualPlaylistId); startListenBrainzSyncPolling(playlistId, virtualPlaylistId);
} catch (error) { } catch (error) {
@ -26647,7 +26645,11 @@ async function startListenBrainzPlaylistSync(identifier, title, playlistId) {
const statusDisplay = document.getElementById(`${playlistId}-sync-status`); const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
const syncButton = document.getElementById(`${playlistId}-sync-btn`); const syncButton = document.getElementById(`${playlistId}-sync-btn`);
if (statusDisplay) statusDisplay.style.display = 'none'; if (statusDisplay) statusDisplay.style.display = 'none';
if (syncButton) syncButton.disabled = false; if (syncButton) {
syncButton.disabled = false;
syncButton.style.opacity = '1';
syncButton.style.cursor = 'pointer';
}
} }
} }