From 252d2d3071c91704e0f0b47a739a03e493a44f32 Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 2 May 2026 18:03:29 +0200 Subject: [PATCH] feat: download playlist tracks in parallel batches (MAX_PARALLEL) Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 40 +++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index fefb4ee..e697dbf 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -831,35 +831,41 @@ app.post('/api/download', async (req, res) => { if (!plTracks.length) throw new Error('Keine Tracks übergeben'); log(jobId, `🎵 Playlist "${title}" — ${plTracks.length} Tracks`); let done = 0, skipped = 0; - for (const track of plTracks) { - try { - const trackTitle = track.trackName || ''; + const CONC = CONFIG.maxParallel || 2; + for (let i = 0; i < plTracks.length; i += CONC) { + const batch = plTracks.slice(i, i + CONC); + const batchResults = await Promise.allSettled(batch.map(async track => { + const trackTitle = track.trackName || ''; // subtitle gives "Artist1, Artist2" — use first for iTunes search, keep full for tags - const trackArtist = track.artistName || artist || ''; + const trackArtist = track.artistName || artist || ''; const searchArtist = trackArtist.split(',')[0].trim(); - - // Always look up iTunes for correct album, artwork, year (Spotify flat format has none) log(jobId, `📋 iTunes: "${trackArtist} – ${trackTitle}"`); const itunes = await lookupItunesTrackMeta(searchArtist, trackTitle); - - const result = await downloadTrack({ + return downloadTrack({ jobId, artist: trackArtist, albumArtist: trackArtist, title: trackTitle, - album: itunes.album || track.collectionName || '', - year: itunes.year || (track.releaseDate ? new Date(track.releaseDate).getFullYear() : undefined), - trackNum: itunes.trackNum || track.trackNumber || undefined, + album: itunes.album || track.collectionName || '', + year: itunes.year || (track.releaseDate ? new Date(track.releaseDate).getFullYear() : undefined), + trackNum: itunes.trackNum || track.trackNumber || undefined, totalTracks: itunes.totalTracks || undefined, - genre: itunes.genre || track.primaryGenreName || undefined, - artworkUrl: itunes.artworkUrl || track.artworkUrl100 || '', + genre: itunes.genre || track.primaryGenreName || undefined, + artworkUrl: itunes.artworkUrl || track.artworkUrl100 || '', explicit: track.trackExplicitness === 'explicit', - durationMs: itunes.durationMs || track.durationMs || undefined, + durationMs: itunes.durationMs || track.durationMs || undefined, }); - if (result?.skipped) skipped++; else done++; + })); + for (let j = 0; j < batchResults.length; j++) { + const r = batchResults[j]; + if (r.status === 'rejected') { + log(jobId, `⚠️ Fehler bei "${batch[j].trackName || '?'}": ${r.reason?.message || r.reason}`); + } else if (r.value?.skipped) { + skipped++; + } else { + done++; + } updateJob(jobId, { progress: Math.round(((done + skipped) / plTracks.length) * 90) }); - } catch(e) { - log(jobId, `⚠️ Fehler bei "${track.trackName || '?'}": ${e.message}`); } } const allSkipped = skipped === plTracks.length;