diff --git a/CHANGELOG.md b/CHANGELOG.md index fa87792..4fc2526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -231,9 +231,16 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d `artist`-Feld den Song selbst benennen. Es fällt **kein** Schritt weg — was vorher gefunden wurde, wird weiter gefunden, gegebenenfalls eine Stufe später. - **Belegt statt behauptet:** `backend/scripts/filter-probe.mjs` holt für bekannte Fälle die - echten Suchtreffer und spielt beide Reihenfolgen durch. Fünf Fälle, **ein** Unterschied (der - gemeldete), **null** Regressionen — BTS auf dem Label-Kanal BANGTANTV, der japanische - Soundtrack-Titel und Rising Insane kommen unverändert heraus, Letzterer nur eine Stufe später. + echten Suchtreffer — **aus beiden Quellen, YT Music zuerst** — und spielt beide Reihenfolgen + durch. Fünf Fälle, **ein** Unterschied (der gemeldete), **null** Regressionen: BTS auf dem + Label-Kanal BANGTANTV, der japanische Soundtrack-Titel, Rising Insane und Daft Punk kommen + unverändert heraus. + - Die erste Fassung der Probe sah **nur** die gewöhnliche YouTube-Suche und meldete deshalb für + „Get Lucky" einen Grammy-Mitschnitt und für TPR ein Tutorial-Video. Beides war erfunden: über + YT Music steht dort an erster Stelle die offizielle Tonspur („Get Lucky (feat. Pharrell + Williams and Nile Rodgers)", 370 s) beziehungsweise TPRs echtes „Dearly Beloved" (138 s) — und + genau die lädt die Kaskade. Eine Probe, die eine Stufe auslässt, erfindet Fehler; deshalb fragt + sie jetzt beide Quellen ab. - **Der Download-Filter konnte den falschen Titel speichern — zwei Löcher, ein Grund (Backend)** — gemeldet mit „銀髪の少年" (Yoshihisa Hirano, HUNTER×HUNTER) und „Dearly Beloved" (TPR): beide Male kam etwas anderes an. diff --git a/backend/scripts/filter-probe.mjs b/backend/scripts/filter-probe.mjs index 8cc9493..da72103 100644 --- a/backend/scripts/filter-probe.mjs +++ b/backend/scripts/filter-probe.mjs @@ -9,6 +9,11 @@ // das `artist`-Feld der Art Tracks vorliegt) und spielt dann die Kaskadenstufen lokal durch — // in der ALTEN und der NEUEN Reihenfolge. Gemeldet wird, welcher Treffer jeweils gewinnt. // +// WICHTIG: beide Quellen werden abgefragt, YT MUSIC ZUERST. Die erste Fassung dieser Probe sah nur +// die gewoehnliche YouTube-Suche und meldete deshalb fuer "Get Lucky" einen Grammy-Mitschnitt als +// Treffer — in Wahrheit liefert YT Music dort die offizielle Tonspur an erster Stelle, und genau +// die laedt die Kaskade. Eine Probe, die eine Stufe auslaesst, erfindet Fehler. +// // docker run --rm --network host --entrypoint node /app/scripts/filter-probe.mjs // // Die Filterbedingungen sind hier nachgebaut, nicht importiert: geprueft werden soll, was yt-dlp @@ -55,11 +60,35 @@ const FAELLE = [ ]; // ── Suchtreffer holen ───────────────────────────────────────────────────────── +const YTM_SONGS_PARAM = 'EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D'; + +/** YT-Music-Suche: erst die URLs der Songs-Leiste, dann deren Metadaten. */ +async function sucheYtm(query, n) { + const url = `https://music.youtube.com/search?q=${encodeURIComponent(query)}¶ms=${YTM_SONGS_PARAM}`; + const urls = await new Promise(resolve => { + const p = spawn('yt-dlp', [ + url, '--flat-playlist', '--playlist-items', `1-${n}`, '--print', 'webpage_url', + '--no-warnings', '--extractor-args', 'youtube:player_client=web_music,default', + ]); + let aus = ''; + p.stdout.on('data', d => aus += d.toString()); + p.on('error', () => resolve([])); + p.on('close', () => resolve(aus.split('\n').map(z => z.trim()).filter(z => z.includes('watch?v=')))); + }); + if (!urls.length) return []; + return metadaten(urls); +} + function suche(query, n) { + return metadaten([`ytsearch${n}:${query}`]); +} + +/** Metadaten zu einer Liste von URLs (oder einer Suchangabe) holen. */ +function metadaten(ziele) { return new Promise(resolve => { const felder = '%(duration)s%(channel)s%(artist)s%(title)s'; const p = spawn('yt-dlp', [ - `ytsearch${n}:${query}`, '--simulate', '--no-warnings', '--ignore-errors', + ...ziele, '--simulate', '--no-warnings', '--ignore-errors', '--print', felder, ]); let aus = ''; @@ -115,13 +144,14 @@ function pruefer(fall) { const ersterTreffer = (kandidaten, pruefung) => kandidaten.find(pruefung) || null; -function kaskade(kandidaten, p, reihenfolge) { +function kaskade(ytm, yt, p, reihenfolge) { const stufen = reihenfolge === 'neu' ? [['1/2 streng', p.streng], ['3/4 ohne Dauer, mit Künstler', p.ohneDauer], ['5/6 mit Dauer, ohne Künstler', p.ohneKuenstler]] : [['1/2 streng', p.streng], ['3/4 mit Dauer, ohne Künstler', p.ohneKuenstler], ['5/6 ohne Dauer, mit Künstler', p.ohneDauer]]; for (const [stufe, pruefung] of stufen) { - const t = ersterTreffer(kandidaten, pruefung); - if (t) return { stufe, treffer: t }; + // Jede Stufe fragt erst YT Music, dann die gewoehnliche YouTube-Suche — so laeuft die Kaskade. + const t = ersterTreffer(ytm, pruefung) || ersterTreffer(yt, pruefung); + if (t) return { stufe: `${stufe}${ytm.includes(t) ? ' · YTM' : ' · YT'}`, treffer: t }; } return { stufe: 'kein Treffer', treffer: null }; } @@ -133,14 +163,15 @@ const zeig = t => t ? `${String(t.dauer).padStart(4)}s | ${t.kanal || '?'} | ${t for (const fall of FAELLE) { const query = `${fall.artist} ${fall.titel}`; process.stdout.write(`\n▶ ${fall.name}\n Suche: "${query}" `); - const kandidaten = await suche(query, 10); - console.log(`(${kandidaten.length} Treffer)`); - if (!kandidaten.length) { console.log(' ⚠️ keine Suchtreffer — übersprungen'); continue; } + const ytm = await sucheYtm(query, 10); + const yt = await suche(query, 10); + console.log(`(YTM ${ytm.length}, YouTube ${yt.length})`); + if (!ytm.length && !yt.length) { console.log(' ⚠️ keine Suchtreffer — übersprungen'); continue; } const p = pruefer(fall); console.log(` Dauerfenster ${p.fenster.min}–${p.fenster.max}s`); - const alt = kaskade(kandidaten, p, 'alt'); - const neu = kaskade(kandidaten, p, 'neu'); + const alt = kaskade(ytm, yt, p, 'alt'); + const neu = kaskade(ytm, yt, p, 'neu'); console.log(` ALT [${alt.stufe}] ${zeig(alt.treffer)}`); console.log(` NEU [${neu.stufe}] ${zeig(neu.treffer)}`);