Files

168 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require('node:test');
const assert = require('node:assert');
const { buildTitleKeywords } = require('../lib/ytdlp.js');
// The keywords become `title ~= (?i)<kw>` 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']);
});
test('Umschrift toleriert die ueblichen Schreibvarianten', () => {
const re = new RegExp(buildTitleKeywords('銀髪の少年', 'Ginpatsu No Shounen')[0], 'i');
assert.ok(re.test('Ginpatsu no Shounen'), 'wie der Tokenizer schreibt');
assert.ok(re.test('Ginpatsu no Shonen'), 'ohne langes o — die haeufigste Variante');
assert.ok(re.test('Ginpatsu-no-Shonen'), 'mit Bindestrichen');
assert.ok(re.test('GINPATSU NO SHŌNEN'), 'mit Makron');
assert.ok(!re.test('Ginpatsu no Otona'), 'anderer Titel faellt weiterhin durch');
});
test('Katakana-Laengungsstrich faellt aus der Umschrift', () => {
// romajiForStorage laesst "ー" stehen ("TeーZe"); in einem lateinischen Titel steht es nie.
const re = new RegExp(buildTitleKeywords('残酷な天使のテーゼ', 'Zankoku Na Tenshi No TeーZe')[0], 'i');
assert.ok(re.test('Zankoku na Tenshi no Teze'), 'ohne Strich geschrieben');
assert.ok(re.test('残酷な天使のテーゼ'), 'Originaltitel weiterhin');
});