feat: filter yt-dlp results by song duration to avoid wrong downloads
When iTunes/Spotify provides a track duration, search 5 YouTube results and keep only those within 60–150% of the expected length (filters remixes, prayer videos, etc.). Falls back to ytsearch1: if no match is found. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
dccf8a5dab
commit
fb02d3faf4
2 files changed
+46
-19
No files matched your search
+41
-15
@@ -156,12 +156,13 @@ async function lookupItunesTrackMeta(artist, title) {
|
||||
discTotal: match.discCount || undefined,
|
||||
genre: match.primaryGenreName || undefined,
|
||||
artworkUrl: (match.artworkUrl100 || '').replace('100x100bb', '1000x1000bb'),
|
||||
durationMs: match.trackTimeMillis || undefined,
|
||||
};
|
||||
} 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 }) {
|
||||
async function downloadTrack({ jobId, artist, albumArtist, title, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, outputDir, durationMs }) {
|
||||
const mainArtist = albumArtist || artist;
|
||||
const safeArtist = sanitize(mainArtist.split(',')[0].trim());
|
||||
const safeAlbum = sanitize(album || 'Singles');
|
||||
@@ -179,21 +180,20 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
|
||||
const outputTemplate = path.join(dir, `${trackPrefix}${safeTitle}.%(ext)s`);
|
||||
const query = `${mainArtist} - ${title} official audio`;
|
||||
log(jobId, `🔍 Suche: "${query}"`);
|
||||
|
||||
// Step 1: yt-dlp — audio only, no thumbnail (we embed iTunes artwork ourselves)
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
`ytsearch1:${query}`,
|
||||
'--extract-audio',
|
||||
'--audio-format', CONFIG.audioFmt,
|
||||
'--audio-quality', CONFIG.audioQuality,
|
||||
'-o', outputTemplate,
|
||||
'--no-playlist',
|
||||
'--playlist-items', '1',
|
||||
'--no-warnings',
|
||||
'--newline',
|
||||
];
|
||||
const baseArgs = [
|
||||
'--extract-audio',
|
||||
'--audio-format', CONFIG.audioFmt,
|
||||
'--audio-quality', CONFIG.audioQuality,
|
||||
'-o', outputTemplate,
|
||||
'--no-playlist',
|
||||
'--playlist-items', '1',
|
||||
'--no-warnings',
|
||||
'--newline',
|
||||
];
|
||||
|
||||
const spawnYtdlp = (args) => new Promise((resolve, reject) => {
|
||||
const proc = spawn(CONFIG.ytdlpBin, args, { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
|
||||
let stderr = '', lastPct = '';
|
||||
proc.stdout.on('data', d => {
|
||||
@@ -215,6 +215,28 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
);
|
||||
});
|
||||
|
||||
if (durationMs) {
|
||||
const durationSec = Math.round(durationMs / 1000);
|
||||
const minDur = Math.max(30, Math.round(durationSec * 0.6));
|
||||
const maxDur = Math.round(durationSec * 1.5);
|
||||
log(jobId, `🔍 Suche (~${durationSec}s): "${query}"`);
|
||||
await spawnYtdlp([
|
||||
`ytsearch5:${query}`,
|
||||
'--match-filter', `duration > ${minDur} & duration < ${maxDur} & !is_live`,
|
||||
'--max-downloads', '1',
|
||||
...baseArgs,
|
||||
]);
|
||||
// Fallback if duration filter matched nothing
|
||||
const hasFile = fs.readdirSync(dir).some(f => AUDIO_EXTS.some(e => f.endsWith(`.${e}`)));
|
||||
if (!hasFile) {
|
||||
log(jobId, '⚠️ Kein Treffer im Dauerbereich — Suche ohne Filter…');
|
||||
await spawnYtdlp([`ytsearch1:${query}`, ...baseArgs]);
|
||||
}
|
||||
} else {
|
||||
log(jobId, `🔍 Suche: "${query}"`);
|
||||
await spawnYtdlp([`ytsearch1:${query}`, ...baseArgs]);
|
||||
}
|
||||
|
||||
// Step 2: find the audio file yt-dlp wrote (newest audio file in dir)
|
||||
const audioFile = fs.readdirSync(dir)
|
||||
.filter(f => AUDIO_EXTS.some(e => f.endsWith(`.${e}`)))
|
||||
@@ -323,6 +345,7 @@ async function downloadAlbumTracks({ jobId, artist, albumTitle, tracks, artworkU
|
||||
artworkUrl: artworkUrl || track.artworkUrl100,
|
||||
explicit: track.trackExplicitness === 'explicit',
|
||||
outputDir: CONFIG.musicPath,
|
||||
durationMs: track.trackTimeMillis || undefined,
|
||||
});
|
||||
if (result?.skipped) skipped++; else done++;
|
||||
updateJob(jobId, { progress: Math.round(((done + skipped) / total) * 90) });
|
||||
@@ -480,6 +503,7 @@ function _spTrackItem(t, defaultAlbum, defaultArt, defaultAlbumArtist) {
|
||||
trackNumber: d?.trackNumber || d?.track_number,
|
||||
trackExplicitness: d?.contentRating?.label === 'EXPLICIT' || d?.isExplicit || d?.explicit ? 'explicit' : 'notExplicit',
|
||||
previewUrl: d?.previewUrl || d?.preview_url || d?.audioPreview?.url || null,
|
||||
durationMs: d?.duration_ms || d?.durationMs || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -590,7 +614,8 @@ app.post('/api/download', async (req, res) => {
|
||||
totalTracks: sp.trackCount || bodyTotalTracks || undefined,
|
||||
discNum: sp.discNumber || bodyDiscNum || undefined,
|
||||
discTotal: sp.discCount || bodyDiscTotal || undefined,
|
||||
artworkUrl: spArt, explicit: sp.trackExplicitness === 'explicit' || !!explicit };
|
||||
artworkUrl: spArt, explicit: sp.trackExplicitness === 'explicit' || !!explicit,
|
||||
durationMs: sp.durationMs || undefined };
|
||||
if (!meta.album) {
|
||||
// Use first artist only for iTunes search (multi-artist strings like "Kløn, Renzyx" confuse iTunes)
|
||||
const searchArtist = artist.split(',')[0].trim();
|
||||
@@ -679,6 +704,7 @@ app.post('/api/download', async (req, res) => {
|
||||
genre: itunes.genre || track.primaryGenreName || undefined,
|
||||
artworkUrl: itunes.artworkUrl || track.artworkUrl100 || '',
|
||||
explicit: track.trackExplicitness === 'explicit',
|
||||
durationMs: itunes.durationMs || track.durationMs || undefined,
|
||||
});
|
||||
if (result?.skipped) skipped++; else done++;
|
||||
updateJob(jobId, { progress: Math.round(((done + skipped) / plTracks.length) * 90) });
|
||||
|
||||
@@ -294,10 +294,11 @@ export async function executeDownload() {
|
||||
itunesId: S.downloadTarget.trackId || S.downloadTarget.collectionId || null,
|
||||
collectionId: S.downloadTarget.collectionId || null,
|
||||
explicit: S.downloadTarget.trackExplicitness === 'explicit',
|
||||
trackNum: S.downloadTarget.trackNumber || undefined,
|
||||
totalTracks: S.downloadTarget.trackCount || undefined,
|
||||
discNum: S.downloadTarget.discNumber || undefined,
|
||||
discTotal: S.downloadTarget.discCount || undefined,
|
||||
trackNum: S.downloadTarget.trackNumber || undefined,
|
||||
totalTracks: S.downloadTarget.trackCount || undefined,
|
||||
discNum: S.downloadTarget.discNumber || undefined,
|
||||
discTotal: S.downloadTarget.discCount || undefined,
|
||||
durationMs: S.downloadTarget.trackTimeMillis || undefined,
|
||||
};
|
||||
addJob(jobId, payload.title, payload.artist, payload.type);
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user