diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 803a17f..3208868 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -80,14 +80,15 @@ function log(jobId, msg) { // We do NOT use --postprocessor-args for metadata because yt-dlp splits that string on // spaces internally, so any metadata value with a space (e.g. album name) gets corrupted. // Instead: yt-dlp downloads, then we call ffmpeg ourselves with properly separated args. -async function downloadTrack({ jobId, artist, title, album, year, trackNum, artworkUrl, outputDir }) { +async function downloadTrack({ jobId, artist, title, album, year, trackNum, totalTracks, artworkUrl, outputDir }) { const safeArtist = sanitize(artist); const safeAlbum = sanitize(album || 'Singles'); const dir = path.join(outputDir || CONFIG.musicPath, safeArtist, safeAlbum); fs.mkdirSync(dir, { recursive: true }); const safeTitle = sanitize(title); - const outputTemplate = path.join(dir, `${safeTitle}.%(ext)s`); + const trackPrefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; + const outputTemplate = path.join(dir, `${trackPrefix}${safeTitle}.%(ext)s`); const query = `${artist} - ${title} official audio`; log(jobId, `\uD83D\uDD0D Suche: "${query}"`); @@ -155,9 +156,10 @@ async function downloadTrack({ jobId, artist, title, album, year, trackNum, artw '-metadata', `artist=${artist}`, '-metadata', `album_artist=${artist}`, ]; - if (album) ffArgs.push('-metadata', `album=${album}`); - if (year) ffArgs.push('-metadata', `date=${String(year)}`); - if (trackNum) ffArgs.push('-metadata', `track=${String(trackNum)}`); + if (album) ffArgs.push('-metadata', `album=${album}`); + if (year) ffArgs.push('-metadata', `date=${String(year)}`); + if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`); + if (totalTracks) ffArgs.push('-metadata', `totaltracks=${String(totalTracks)}`); ffArgs.push(tmpFile, '-y'); const ff = spawn('ffmpeg', ffArgs); @@ -224,13 +226,14 @@ async function downloadAlbumTracks({ jobId, artist, albumTitle, tracks, artworkU try { await downloadTrack({ jobId, - artist: track.artistName || artist, - title: track.trackName || track.title, - album: albumTitle, - year: track.releaseDate ? new Date(track.releaseDate).getFullYear() : undefined, - trackNum: track.trackNumber || undefined, - artworkUrl: artworkUrl || track.artworkUrl100, - outputDir: CONFIG.musicPath, + artist: track.artistName || artist, + title: track.trackName || track.title, + album: albumTitle, + year: track.releaseDate ? new Date(track.releaseDate).getFullYear() : undefined, + trackNum: track.trackNumber || undefined, + totalTracks: total, + artworkUrl: artworkUrl || track.artworkUrl100, + outputDir: CONFIG.musicPath, }); done++; updateJob(jobId, { progress: Math.round((done / total) * 90) }); @@ -240,6 +243,28 @@ async function downloadAlbumTracks({ jobId, artist, albumTitle, tracks, artworkU } } +// Search iTunes for an album by artist+name and return its tracks +async function searchItunesAlbumTracks(artist, album) { + return new Promise(resolve => { + const q = encodeURIComponent(`${artist} ${album}`); + https.get(`https://itunes.apple.com/search?term=${q}&media=music&entity=album&limit=5&country=de`, res => { + let d = ''; + res.on('data', c => d += c); + res.on('end', () => { + try { + const results = JSON.parse(d).results || []; + const match = results.find(r => + r.collectionName?.toLowerCase().includes(album.toLowerCase()) || + r.artistName?.toLowerCase().includes(artist.toLowerCase()) + ); + if (match) fetchItunesTracks(match.collectionId).then(resolve).catch(() => resolve([])); + else resolve([]); + } catch { resolve([]); } + }); + }).on('error', () => resolve([])); + }); +} + // Fetch iTunes album tracks async function fetchItunesTracks(collectionId) { return new Promise((resolve, reject) => { @@ -331,12 +356,14 @@ app.post('/api/download', async (req, res) => { log(jobId, '📋 Lade Track-Liste von iTunes…'); tracks = await fetchItunesTracks(collectionId); } - if (tracks.length) { - await downloadAlbumTracks({ jobId, artist, albumTitle: album || title, tracks, artworkUrl }); - } else { - // Fallback: download full album search - await downloadTrack({ jobId, artist, title: `${album || title} full album`, album, artworkUrl }); + if (!tracks.length) { + log(jobId, '🔍 Suche Album auf iTunes…'); + tracks = await searchItunesAlbumTracks(artist, album || title); } + if (!tracks.length) { + throw new Error(`Keine einzelnen Tracks für "${album || title}" gefunden. Bitte über die iTunes Global-Suche herunterladen.`); + } + await downloadAlbumTracks({ jobId, artist, albumTitle: album || title, tracks, artworkUrl }); updateJob(jobId, { status: 'done', progress: 100, message: `Album "${album || title}" fertig!` }); } else if (type === 'artist') {