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:
1 parent
09ce590abd
commit
c470ff9d12
1 file changed
+44
-17
+44
-17
@@ -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') {
|
||||
|
||||
Reference in new issue
Block a user