From 5987e1f30397d1f7d324fb7cedf0711bf4805c94 Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 26 Apr 2026 18:57:25 +0200 Subject: [PATCH] fix: auto-lookup album metadata from iTunes when missing on track download Songs from new releases (Apple RSS feed) have no collectionName, causing the album tag to be omitted from embedded metadata -> Navidrome shows [Unknown Album]. Backend now calls iTunes search to fill in album, year, trackNum, totalTracks, discNum, genre before downloading. Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 38 ++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 0a7d7bd..282c60a 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -130,6 +130,35 @@ async function fetchLyrics(artist, title, album) { } catch { return null; } } +// iTunes track lookup — fills in album, year, trackNum, totalTracks, genre when missing +async function lookupItunesTrackMeta(artist, title) { + try { + const url = `https://itunes.apple.com/search?term=${encodeURIComponent(artist + ' ' + title)}&media=music&entity=musicTrack&limit=5&country=us&explicit=Yes`; + const results = await new Promise((resolve, reject) => { + https.get(url, res => { + let d = ''; res.on('data', c => d += c); + res.on('end', () => { try { resolve(JSON.parse(d).results || []); } catch { reject(new Error('parse')); } }); + }).on('error', reject); + }); + const norm = s => s?.toLowerCase().trim() || ''; + const match = results.find(t => + norm(t.trackName) === norm(title) && + norm(t.artistName).split(' ')[0] === norm(artist).split(' ')[0] + ) || results[0]; + if (!match) return {}; + return { + album: match.collectionName || '', + year: match.releaseDate ? new Date(match.releaseDate).getFullYear() : undefined, + trackNum: match.trackNumber || undefined, + totalTracks: match.trackCount || undefined, + discNum: match.discNumber || undefined, + discTotal: match.discCount || undefined, + genre: match.primaryGenreName || undefined, + artworkUrl: (match.artworkUrl100 || '').replace('100x100bb', '1000x1000bb'), + }; + } catch { return {}; } +} + // Download a single track: yt-dlp audio + ffmpeg clean metadata + iTunes cover embedded async function downloadTrack({ jobId, artist, albumArtist, title, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, outputDir }) { const mainArtist = albumArtist || artist; @@ -380,7 +409,14 @@ app.post('/api/download', async (req, res) => { updateJob(jobId, { status: 'running', progress: 5, message: 'Gestartet…' }); try { if (type === 'track') { - const result = await downloadTrack({ jobId, artist, title, album, artworkUrl, explicit: !!explicit }); + let meta = { album, year: undefined, trackNum: undefined, totalTracks: undefined, discNum: undefined, discTotal: undefined, genre: undefined }; + if (!album) { + log(jobId, '📋 Suche Album-Metadaten von iTunes…'); + const extra = await lookupItunesTrackMeta(artist, title); + if (extra.album) log(jobId, `📋 Album: "${extra.album}" (${extra.year || '?'})`); + meta = { ...extra, artworkUrl: artworkUrl || extra.artworkUrl || '' }; + } + const result = await downloadTrack({ jobId, artist, title, artworkUrl: artworkUrl || meta.artworkUrl || '', explicit: !!explicit, ...meta }); if (result?.skipped) { updateJob(jobId, { status: 'done', progress: 100, message: 'Bereits vorhanden – übersprungen', skipped: true }); } else {