fix(backend): Live-Mitschnitte aus der Suche ausschliessen
is_live meint nur "laeuft gerade als Stream" — eine Konzert-AUFNAHME kam
ungehindert durch und landete als Albumtrack in der Bibliothek ("Dat Adam - Ufo
Video Days 2015 Live" auf dem echten Kuenstlerkanal, Dauer im Fenster). Der
Variantenfilter bekommt daher eine live-Regel. Sie schaltet sich ab, wenn der
gesuchte Titel selbst live enthaelt; die Pruefung nutzt \blive\b statt includes,
sonst wuerde "deliver" die Regel aushebeln. Drei Tests dazu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d8a8a1d5e
commit
33b3df609a
4 files changed
+54
-2
No files matched your search
@@ -78,6 +78,12 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an
|
||||
to a qualifier ("… - 2020 Remaster") is exempt from the number rule: it appears on the release but
|
||||
never in the YouTube title. Measured against a 29-title sample, 24 titles keep their previous
|
||||
behaviour exactly.
|
||||
- **Downloader saved a concert recording as the album track** — searching DAT ADAM's "UFO" stored
|
||||
"Dat Adam - Ufo Video Days 2015 Live". It passed every gate legitimately: right channel, 209 s
|
||||
inside the duration window, "Ufo" in the title, and `!is_live` rejects only *running streams*, not
|
||||
live recordings. The variant blocklist (remix/cover/acoustic/…) now includes `live`. As with the
|
||||
other rules it switches itself off when the searched title says live, so "Live Forever" and
|
||||
deliberately requested live versions are unaffected.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -103,6 +103,13 @@ On the deploy host the music lives at `/opt/navidrome/music`.
|
||||
next to a qualifier** ("… - 2020 Remaster") is dropped again — it sits on the release but never in
|
||||
the YouTube title, so requiring it would reject the correct upload. A title that IS a year ("1999",
|
||||
"Berlin 2020") keeps it. Unit-tested in `backend/test/ytdlp.test.js`.
|
||||
- **Live recordings are excluded by title, not by `is_live`.** `!is_live` only means "not a running
|
||||
stream" — a concert RECORDING passes it. On the artist's own channel nothing else distinguishes it
|
||||
from the studio track, which is how "Dat Adam - Ufo Video Days 2015 Live" (channel `Dat Adam`,
|
||||
209 s, title contains UFO) got saved as the album track. `buildVariantFilter` therefore carries a
|
||||
`live` rule like remix/cover/…. It self-disables when the searched title itself says live, so
|
||||
"Live Forever" or a deliberately requested live version still works — that check uses a `\blive\b`
|
||||
regex (`skipRe`), because a plain substring test would let "deliver"/"delivery" switch the rule off.
|
||||
- **Size cap:** `--max-filesize 30M` on every yt-dlp call; Mittelweg steps also cap at 2×
|
||||
expected duration. Prevents saving a 28-min video (52 MB) when the wrong result slips through.
|
||||
- **Leading aesthetic characters:** artists often style their name with leading punctuation
|
||||
|
||||
+10
-2
@@ -71,8 +71,16 @@ function buildVariantFilter(title) {
|
||||
{ key: 'cover', expr: 'title !~= (?i)\\bcover\\b' },
|
||||
{ key: 'acoustic', expr: 'title !~= (?i)\\bacoustic\\b' },
|
||||
{ key: 'instrumental', expr: 'title !~= (?i)\\binstrumental\\b' },
|
||||
// Konzertmitschnitte: `!is_live` heisst nur "laeuft nicht gerade als Stream" und laesst eine
|
||||
// Live-AUFNAHME durch. Auf dem echten Kuenstlerkanal ist die sonst nicht vom Studiotrack zu
|
||||
// unterscheiden — so landete "Dat Adam - Ufo Video Days 2015 Live" in der Bibliothek.
|
||||
// skipRe statt includes(): "deliver"/"delivery" enthalten "live" als Teilstring und wuerden
|
||||
// die Regel sonst faelschlich abschalten.
|
||||
{ key: 'live', expr: 'title !~= (?i)\\blive\\b', skipRe: /\blive\b/i },
|
||||
];
|
||||
const parts = RULES.filter(r => !t.includes(r.key)).map(r => r.expr);
|
||||
// Eine Regel entfaellt, wenn der GESUCHTE Titel das Wort selbst traegt — wer "Live Forever" oder
|
||||
// bewusst eine Live-Version sucht, soll sie auch bekommen.
|
||||
const parts = RULES.filter(r => r.skipRe ? !r.skipRe.test(t) : !t.includes(r.key)).map(r => r.expr);
|
||||
return parts.length ? parts.join(' & ') : '';
|
||||
}
|
||||
|
||||
@@ -445,4 +453,4 @@ async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, ra
|
||||
return { success: true, dir };
|
||||
}
|
||||
|
||||
module.exports = { downloadTrack, embedMetadata, buildTitleKeywords };
|
||||
module.exports = { downloadTrack, embedMetadata, buildTitleKeywords, buildVariantFilter };
|
||||
@@ -45,3 +45,34 @@ 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);
|
||||
});
|
||||
Reference in new issue
Block a user