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
|
// 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.
|
// 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.
|
// 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 safeArtist = sanitize(artist);
|
||||||
const safeAlbum = sanitize(album || 'Singles');
|
const safeAlbum = sanitize(album || 'Singles');
|
||||||
const dir = path.join(outputDir || CONFIG.musicPath, safeArtist, safeAlbum);
|
const dir = path.join(outputDir || CONFIG.musicPath, safeArtist, safeAlbum);
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
const safeTitle = sanitize(title);
|
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`;
|
const query = `${artist} - ${title} official audio`;
|
||||||
log(jobId, `\uD83D\uDD0D Suche: "${query}"`);
|
log(jobId, `\uD83D\uDD0D Suche: "${query}"`);
|
||||||
|
|
||||||
@@ -155,9 +156,10 @@ async function downloadTrack({ jobId, artist, title, album, year, trackNum, artw
|
|||||||
'-metadata', `artist=${artist}`,
|
'-metadata', `artist=${artist}`,
|
||||||
'-metadata', `album_artist=${artist}`,
|
'-metadata', `album_artist=${artist}`,
|
||||||
];
|
];
|
||||||
if (album) ffArgs.push('-metadata', `album=${album}`);
|
if (album) ffArgs.push('-metadata', `album=${album}`);
|
||||||
if (year) ffArgs.push('-metadata', `date=${String(year)}`);
|
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');
|
ffArgs.push(tmpFile, '-y');
|
||||||
|
|
||||||
const ff = spawn('ffmpeg', ffArgs);
|
const ff = spawn('ffmpeg', ffArgs);
|
||||||
@@ -224,13 +226,14 @@ async function downloadAlbumTracks({ jobId, artist, albumTitle, tracks, artworkU
|
|||||||
try {
|
try {
|
||||||
await downloadTrack({
|
await downloadTrack({
|
||||||
jobId,
|
jobId,
|
||||||
artist: track.artistName || artist,
|
artist: track.artistName || artist,
|
||||||
title: track.trackName || track.title,
|
title: track.trackName || track.title,
|
||||||
album: albumTitle,
|
album: albumTitle,
|
||||||
year: track.releaseDate ? new Date(track.releaseDate).getFullYear() : undefined,
|
year: track.releaseDate ? new Date(track.releaseDate).getFullYear() : undefined,
|
||||||
trackNum: track.trackNumber || undefined,
|
trackNum: track.trackNumber || undefined,
|
||||||
artworkUrl: artworkUrl || track.artworkUrl100,
|
totalTracks: total,
|
||||||
outputDir: CONFIG.musicPath,
|
artworkUrl: artworkUrl || track.artworkUrl100,
|
||||||
|
outputDir: CONFIG.musicPath,
|
||||||
});
|
});
|
||||||
done++;
|
done++;
|
||||||
updateJob(jobId, { progress: Math.round((done / total) * 90) });
|
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
|
// Fetch iTunes album tracks
|
||||||
async function fetchItunesTracks(collectionId) {
|
async function fetchItunesTracks(collectionId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -331,12 +356,14 @@ app.post('/api/download', async (req, res) => {
|
|||||||
log(jobId, '📋 Lade Track-Liste von iTunes…');
|
log(jobId, '📋 Lade Track-Liste von iTunes…');
|
||||||
tracks = await fetchItunesTracks(collectionId);
|
tracks = await fetchItunesTracks(collectionId);
|
||||||
}
|
}
|
||||||
if (tracks.length) {
|
if (!tracks.length) {
|
||||||
await downloadAlbumTracks({ jobId, artist, albumTitle: album || title, tracks, artworkUrl });
|
log(jobId, '🔍 Suche Album auf iTunes…');
|
||||||
} else {
|
tracks = await searchItunesAlbumTracks(artist, album || title);
|
||||||
// Fallback: download full album search
|
|
||||||
await downloadTrack({ jobId, artist, title: `${album || title} full album`, album, artworkUrl });
|
|
||||||
}
|
}
|
||||||
|
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!` });
|
updateJob(jobId, { status: 'done', progress: 100, message: `Album "${album || title}" fertig!` });
|
||||||
|
|
||||||
} else if (type === 'artist') {
|
} else if (type === 'artist') {
|
||||||
|
|||||||
Reference in new issue
Block a user