fix: album/artist download now saves individual tracks with full metadata

- Remove broken 'full album' fallback that downloaded everything as one MP3
- Add searchItunesAlbumTracks() so albums without a collectionId can still
  be resolved to individual tracks via iTunes search
- Prefix filenames with zero-padded track number (01 - Title.mp3)
- Write track tag as 'N/total' format (e.g. 3/12) plus totaltracks tag
  so Navidrome gets correct track order and total count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-04-25 21:34:41 +02:00
1 parent 09ce590abd
commit c470ff9d12
1 file changed
+35 -8
+35 -8
View File
@@ -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}"`);
@@ -157,7 +158,8 @@ async function downloadTrack({ jobId, artist, title, album, year, trackNum, artw
];
if (album) ffArgs.push('-metadata', `album=${album}`);
if (year) ffArgs.push('-metadata', `date=${String(year)}`);
if (trackNum) ffArgs.push('-metadata', `track=${String(trackNum)}`);
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);
@@ -229,6 +231,7 @@ async function downloadAlbumTracks({ jobId, artist, albumTitle, tracks, artworkU
album: albumTitle,
year: track.releaseDate ? new Date(track.releaseDate).getFullYear() : undefined,
trackNum: track.trackNumber || undefined,
totalTracks: total,
artworkUrl: artworkUrl || track.artworkUrl100,
outputDir: CONFIG.musicPath,
});
@@ -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') {