test(backend): Filterprobe fragt auch YT Music ab — erste Fassung meldete erfundene Fehler

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-25 17:03:14 +02:00
1 parent 2f87a12f08
commit 7879e37a7b
2 files changed
+50 -12

No files matched your search

+10 -3
View File
@@ -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 `artist`-Feld den Song selbst benennen. Es fällt **kein** Schritt weg — was vorher gefunden
wurde, wird weiter gefunden, gegebenenfalls eine Stufe später. wurde, wird weiter gefunden, gegebenenfalls eine Stufe später.
- **Belegt statt behauptet:** `backend/scripts/filter-probe.mjs` holt für bekannte Fälle die - **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 echten Suchtreffer — **aus beiden Quellen, YT Music zuerst** — und spielt beide Reihenfolgen
gemeldete), **null** Regressionen — BTS auf dem Label-Kanal BANGTANTV, der japanische durch. Fünf Fälle, **ein** Unterschied (der gemeldete), **null** Regressionen: BTS auf dem
Soundtrack-Titel und Rising Insane kommen unverändert heraus, Letzterer nur eine Stufe später. 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)** — - **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 gemeldet mit „銀髪の少年" (Yoshihisa Hirano, HUNTER×HUNTER) und „Dearly Beloved" (TPR): beide Male
kam etwas anderes an. kam etwas anderes an.
+40 -9
View File
@@ -9,6 +9,11 @@
// das `artist`-Feld der Art Tracks vorliegt) und spielt dann die Kaskadenstufen lokal durch — // 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. // 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 <backend-image> /app/scripts/filter-probe.mjs // docker run --rm --network host --entrypoint node <backend-image> /app/scripts/filter-probe.mjs
// //
// Die Filterbedingungen sind hier nachgebaut, nicht importiert: geprueft werden soll, was yt-dlp // Die Filterbedingungen sind hier nachgebaut, nicht importiert: geprueft werden soll, was yt-dlp
@@ -55,11 +60,35 @@ const FAELLE = [
]; ];
// ── Suchtreffer holen ───────────────────────────────────────────────────────── // ── 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)}&params=${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) { 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 => { return new Promise(resolve => {
const felder = '%(duration)s%(channel)s%(artist)s%(title)s'; const felder = '%(duration)s%(channel)s%(artist)s%(title)s';
const p = spawn('yt-dlp', [ const p = spawn('yt-dlp', [
`ytsearch${n}:${query}`, '--simulate', '--no-warnings', '--ignore-errors', ...ziele, '--simulate', '--no-warnings', '--ignore-errors',
'--print', felder, '--print', felder,
]); ]);
let aus = ''; let aus = '';
@@ -115,13 +144,14 @@ function pruefer(fall) {
const ersterTreffer = (kandidaten, pruefung) => kandidaten.find(pruefung) || null; const ersterTreffer = (kandidaten, pruefung) => kandidaten.find(pruefung) || null;
function kaskade(kandidaten, p, reihenfolge) { function kaskade(ytm, yt, p, reihenfolge) {
const stufen = reihenfolge === 'neu' 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 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]]; : [['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) { for (const [stufe, pruefung] of stufen) {
const t = ersterTreffer(kandidaten, pruefung); // Jede Stufe fragt erst YT Music, dann die gewoehnliche YouTube-Suche — so laeuft die Kaskade.
if (t) return { stufe, treffer: t }; 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 }; 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) { for (const fall of FAELLE) {
const query = `${fall.artist} ${fall.titel}`; const query = `${fall.artist} ${fall.titel}`;
process.stdout.write(`\n▶ ${fall.name}\n Suche: "${query}" `); process.stdout.write(`\n▶ ${fall.name}\n Suche: "${query}" `);
const kandidaten = await suche(query, 10); const ytm = await sucheYtm(query, 10);
console.log(`(${kandidaten.length} Treffer)`); const yt = await suche(query, 10);
if (!kandidaten.length) { console.log(' ⚠️ keine Suchtreffer — übersprungen'); continue; } console.log(`(YTM ${ytm.length}, YouTube ${yt.length})`);
if (!ytm.length && !yt.length) { console.log(' ⚠️ keine Suchtreffer — übersprungen'); continue; }
const p = pruefer(fall); const p = pruefer(fall);
console.log(` Dauerfenster ${p.fenster.min}–${p.fenster.max}s`); console.log(` Dauerfenster ${p.fenster.min}–${p.fenster.max}s`);
const alt = kaskade(kandidaten, p, 'alt'); const alt = kaskade(ytm, yt, p, 'alt');
const neu = kaskade(kandidaten, p, 'neu'); const neu = kaskade(ytm, yt, p, 'neu');
console.log(` ALT [${alt.stufe}] ${zeig(alt.treffer)}`); console.log(` ALT [${alt.stufe}] ${zeig(alt.treffer)}`);
console.log(` NEU [${neu.stufe}] ${zeig(neu.treffer)}`); console.log(` NEU [${neu.stufe}] ${zeig(neu.treffer)}`);