const test = require('node:test'); const assert = require('node:assert'); const { buildTitleKeywords } = require('../lib/ytdlp.js'); // The keywords become `title ~= (?i)` match-filters in yt-dlp, i.e. every keyword must match // the candidate title case-insensitively. This mirrors that check. const accepts = (title, candidate) => buildTitleKeywords(title).every(kw => new RegExp(kw, 'i').test(candidate)); test('a number in the title is always required, however short', () => { const kws = buildTitleKeywords('700 Main Street'); assert.ok(kws.some(k => k.includes('700')), `700 fehlt in ${JSON.stringify(kws)}`); }); test('rejects a same-shaped title with a different number', () => { // The real regression: "777 Main Street" (Release - Topic) was downloaded for DAT ADAM's // "700 Main Street", because only the two longest words (Street, Main) were required. assert.equal(accepts('700 Main Street', '777 Main Street'), false); assert.equal(accepts('700 Main Street', '700 Main Street'), true); }); test('accepts the abbreviated spelling uploaders use', () => { assert.equal(accepts('700 Main Street', '700 Main st (cr-Version)'), true); assert.equal(accepts('700 Main Street', 'DAT ADAM - 700 Main St [FULL SONG]'), true); }); test('the abbreviation does not match inside another word', () => { assert.equal(accepts('700 Main Street', '700 Main Stuff'), false); }); test('titles without numbers keep the previous two-longest-words behaviour', () => { assert.deepEqual(buildTitleKeywords('Bohemian Rhapsody'), ['Bohemian', 'Rhapsody']); assert.equal(accepts('Bohemian Rhapsody', 'Queen - Bohemian Rhapsody (Official Video)'), true); assert.equal(accepts('Bohemian Rhapsody', 'Queen - Another One Bites The Dust'), false); }); test('a remaster year is not required — YouTube titles never carry it', () => { // "… - 2020 Remaster" steht auf der Veröffentlichung, der Upload heißt schlicht // "Take Me Home, Country Roads". Vor dieser Ausnahme hätte die 2020 ihn abgelehnt. assert.equal(accepts('Take Me Home, Country Roads - 2020 Remaster', 'John Denver - Take Me Home, Country Roads'), true); }); test('a year that IS the title stays required', () => { assert.equal(accepts('1999', 'Prince - 1999 (Official Music Video)'), true); assert.equal(accepts('1999', 'Prince - 1984 (Official Music Video)'), false); }); const { buildVariantFilter } = require('../lib/ytdlp.js'); // Der Variantenfilter ist eine yt-dlp-Ausdruckskette "title !~= (?i)\bx\b & …". // Diese Hilfsfunktion wertet sie so aus, wie yt-dlp es tut. const variantAccepts = (searchTitle, candidate) => { const f = buildVariantFilter(searchTitle); if (!f) return true; return f.split(' & ').every(part => { const m = /^title !~= \(\?i\)(.+)$/.exec(part); return !new RegExp(m[1], 'i').test(candidate); }); }; test('rejects a live recording — is_live only covers running streams', () => { // Der echte Fall: auf dem Kuenstlerkanal, Dauer im Fenster, Titel passend. assert.equal(variantAccepts('UFO', 'Dat Adam - Ufo Video Days 2015 Live'), false); assert.equal(variantAccepts('UFO', 'DAT ADAM - 700 Main St | ROCK AM RING 2017 [LIVE]'), false); assert.equal(variantAccepts('UFO', 'UFO'), true); }); test('a deliberately live title keeps its live results', () => { assert.equal(variantAccepts('Live Forever', 'Oasis - Live Forever'), true); assert.equal(variantAccepts('Unplugged in New York (Live)', 'Nirvana - All Apologies (Live)'), true); }); test('"live" inside another word does not disable the rule', () => { // "deliver" enthaelt "live" als Teilstring — die Regel muss trotzdem greifen. assert.equal(variantAccepts('Deliver', 'Artist - Deliver (Live at Wembley)'), false); assert.equal(variantAccepts('Deliver', 'Artist - Deliver'), true); }); const { pickYtmVideoUrls } = require('../lib/ytdlp.js'); // Echte Ausgabe der YTM-Suche "DAT ADAM UFO": Songs, ein Album (MPREb_) und der // Kuenstlerkanal (UC…) auf Platz 9. const YTM_OUT = [ 'https://music.youtube.com/watch?v=ZG4XhPn3Yb4', 'https://music.youtube.com/watch?v=2KVU6aikZF0', 'https://music.youtube.com/browse/MPREb_IhxJPixC1QT', 'https://music.youtube.com/watch?v=phLOTtJaqZk', 'https://music.youtube.com/watch?v=p935nH6aj8Y', 'https://music.youtube.com/watch?v=Pb2YcDBm5fA', 'https://music.youtube.com/browse/MPREb_S7MMKVMduma', 'https://music.youtube.com/watch?v=mNfsX2RbZ2M', 'https://music.youtube.com/browse/UC5hilNPCUHEPf4VpG5_YpYA', 'https://music.youtube.com/watch?v=O034JUlf1J4', ].join('\n'); test('a channel hit no longer discards the whole result list', () => { const { urls, skipped } = pickYtmVideoUrls(YTM_OUT); assert.equal(urls.length, 7); assert.equal(skipped, 3); // 2 Alben + 1 Kanal assert.match(urls[0], /ZG4XhPn3Yb4/); // der offizielle Art Track assert.ok(!urls.some(u => u.includes('/browse/'))); }); test('nothing but list entries yields no candidates', () => { const only = 'https://music.youtube.com/browse/UC5hilNPCUHEPf4VpG5_YpYA'; assert.deepEqual(pickYtmVideoUrls(only).urls, []); assert.deepEqual(pickYtmVideoUrls('').urls, []); }); // ─── Japanische Titel bekommen wieder eine Titelpruefung ──────────────────── // Regression zum 24.9.2026: "銀髪の少年" (HUNTER×HUNTER, Yoshihisa Hirano) lieferte ein anderes // Stueck desselben Komponisten. Ursache: fuer japanische Titel gab buildTitleKeywords eine LEERE // Liste zurueck, und die Kaskadenschritte 3/4 lassen die Kuenstlerpruefung fallen — uebrig blieb // allein das Dauerfenster, in das auf einem Soundtrack-Album fast jeder Titel passt. test('japanischer Titel liefert jetzt ein Muster statt gar nichts', () => { const kws = buildTitleKeywords('銀髪の少年'); assert.equal(kws.length, 1); assert.ok(new RegExp(kws[0], 'i').test('銀髪の少年'), 'Originaltitel muss passen'); assert.ok(!new RegExp(kws[0], 'i').test('闘う者たち'), 'anderes Stueck darf NICHT passen'); }); test('Romaji-Fassung wird als Alternative akzeptiert', () => { const kws = buildTitleKeywords('銀髪の少年', 'ginpatsu no shounen'); const re = new RegExp(kws[0], 'i'); assert.ok(re.test('銀髪の少年'), 'Original'); assert.ok(re.test('Ginpatsu no Shounen'), 'Romaji mit Leerzeichen'); assert.ok(re.test('Ginpatsu-no-Shounen'), 'Romaji mit Bindestrichen'); assert.ok(re.test('Hunter x Hunter OST - Ginpatsu no Shounen'), 'als Teil eines laengeren Titels'); assert.ok(!re.test('Requiem Aranea'), 'anderes Stueck desselben Albums'); }); test('Romaji, die gar keine sind, werden nicht als Alternative genommen', () => { // romajiForStorage gibt den Text unveraendert zurueck, wenn der Tokenizer fehlt const kws = buildTitleKeywords('銀髪の少年', '銀髪の少年'); assert.equal(kws.length, 1); assert.ok(!kws[0].includes('|'), 'keine doppelte Alternative'); }); test('Sonderzeichen im japanischen Titel bleiben woertlich', () => { const kws = buildTitleKeywords('君の名は。(Movie ver.)'); const re = new RegExp(kws[0], 'i'); assert.ok(re.test('君の名は。(Movie ver.)')); assert.ok(!re.test('君の名はX Movie verY'), 'Punkt darf nicht als Platzhalter wirken'); }); test('lateinische Titel bleiben unveraendert', () => { assert.deepEqual(buildTitleKeywords('Bohemian Rhapsody'), ['Bohemian', 'Rhapsody']); assert.deepEqual(buildTitleKeywords('Dearly Beloved'), ['Beloved', 'Dearly']); });