From 8d400822bd7623a54e946d5cc8f98fcf9e81e5dd Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Sun, 11 Oct 2026 22:41:54 +0200 Subject: [PATCH] Unquoted album tracks: the name starts at the track number "A Strange Light from the East - Eastern Influenced Western Music (1965-72) - CD2 - 07 - Rascals - Sattva.mp3 (1/43)": unquotedName cut at the last " - " ("Sattva.mp3"), leaving track number and artist in the subject, so every track was a release (85665085, 85665050). With a " - NN - " track or part number the name starts there ("07 - Rascals - Sattva.mp3"), the rest names the album and disc. Both implementations; tests; fixtures identical. Co-Authored-By: Claude Opus 5.5 --- edge/src/indexer.rs | 18 +++++++++++++++++- lib/headers/subject.ts | 5 ++++- tests/unit/headers/subject.spec.ts | 1 + tests/unit/release/identity.spec.ts | 7 +++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/edge/src/indexer.rs b/edge/src/indexer.rs index b035d4a..6fbb180 100644 --- a/edge/src/indexer.rs +++ b/edge/src/indexer.rs @@ -259,6 +259,20 @@ fn has_extension(s: &str) -> bool { s[..dot].chars().last().is_some_and(|c| c != '.' && !js::is_space(c)) } +/// The first / - \d{1,3} - / in `text`: the index of its " - ". +fn first_track(text: &str) -> Option { + let b = text.as_bytes(); + let mut from = 0; + while let Some(i) = text[from..].find(" - ").map(|i| i + from) { + let digits = b[i + 3..].iter().take_while(|c| c.is_ascii_digit()).count(); + if (1..=3).contains(&digits) && b[i + 3 + digits..].starts_with(b" - ") { + return Some(i); + } + from = i + 1; + } + None +} + /// unquotedName (lib/headers/subject.ts): the file name of a subject without a quoted one, from the text before its counter. pub fn unquoted_name(before_counter: &str) -> Option { let mut text = before_counter.trim_end_matches(js::is_space); @@ -300,7 +314,8 @@ pub fn unquoted_name(before_counter: &str) -> Option { } if has_extension(text) { let bracket = text.rfind("] ").map_or(0, |i| i + 2); - let dash = text.rfind(" - ").map_or(0, |i| i + 3); + // A track or part number (/ - \d{1,3} - /): the name starts there; else after the last " - ". + let dash = first_track(text).or_else(|| text.rfind(" - ")).map_or(0, |i| i + 3); let name = js::trim(&text[bracket.max(dash)..]); return (!name.is_empty() && has_extension(name)).then(|| name.to_string()); } @@ -870,6 +885,7 @@ mod unquoted_tests { ("New_Super_Mario_Bros._[Keygen].exe ", Some("New_Super_Mario_Bros._[Keygen].exe")), ("Emily_18_-_Bed.part01.rar Emily 18 bed video FULL NUDE ", Some("Emily_18_-_Bed.part01.rar")), ("[33/117] 674a37548b09.dat ", Some("674a37548b09.dat")), + ("A Strange Light from the East - Eastern Influenced Western Music (1965-72) - CD2 - 07 - Rascals - Sattva.mp3 ", Some("07 - Rascals - Sattva.mp3")), ("pal! [Mivina high(1).wmv\" yEnc] ", Some("Mivina high(1).wmv")), ("[03/21 Geroellheimer - S03E16.part01.rar\" yEnc ", Some("Geroellheimer - S03E16.part01.rar")), ("[7w] - 9BbEV2MrTi59GBTEFlVrssBrXNwXf2SK9cQEeDjg yEnc ", None), diff --git a/lib/headers/subject.ts b/lib/headers/subject.ts index 756fb4c..c6214f8 100644 --- a/lib/headers/subject.ts +++ b/lib/headers/subject.ts @@ -48,7 +48,10 @@ export function unquotedName(beforeCounter: string): string | null { } if (EXTENSION.test(text)) { const bracket = text.lastIndexOf('] ') - const dash = text.lastIndexOf(' - ') + // A track or part number ("Album - CD2 - 07 - Artist - Title.mp3"): the name starts there, so the rest + // is the album, the same for every track (LOG.md 2026-10-11 (10)); else after the last " - ". + const track = / - \d{1,3} - /.exec(text) + const dash = track ? track.index : text.lastIndexOf(' - ') const cut = Math.max(bracket === -1 ? 0 : bracket + 2, dash === -1 ? 0 : dash + 3) const name = text.slice(cut).trim() return name && EXTENSION.test(name) ? name : null diff --git a/tests/unit/headers/subject.spec.ts b/tests/unit/headers/subject.spec.ts index 5880c7a..9dae40a 100644 --- a/tests/unit/headers/subject.spec.ts +++ b/tests/unit/headers/subject.spec.ts @@ -53,6 +53,7 @@ test.group('subject / parseSubject', () => { ['New_Super_Mario_Bros._[Keygen].exe (1/2)', 'New_Super_Mario_Bros._[Keygen].exe', 1, 2], ['Emily_18_-_Bed.part01.rar Emily 18 bed video FULL NUDE (01/24)', 'Emily_18_-_Bed.part01.rar', 1, 24], ['[33/117] 674a37548b09.dat (1/1)', '674a37548b09.dat', 1, 1, 33], + ['A Strange Light from the East - Eastern Influenced Western Music (1965-72) - CD2 - 07 - Rascals - Sattva.mp3 (1/43)', '07 - Rascals - Sattva.mp3', 1, 43], ["pal! [Mivina high(1).wmv\" yEnc] (323/349)", 'Mivina high(1).wmv', 323, 349], ['[03/21 Geroellheimer - S03E16.part01.rar" yEnc (1/50)', 'Geroellheimer - S03E16.part01.rar', 1, 50], ] diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index 41c13eb..baf9104 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -187,6 +187,13 @@ test.group('release / par2 names', () => { assert.isFalse(randomPoster('w.merklein@freenet.de (muffin)')) }) + test('album tracks with unquoted names are one release per disc', async ({ assert }) => { + const { parseSubject } = await import('#lib/headers/index') + const id = (subject: string) => releaseIdentity({ poster: 'Hannah_Qe@hotmail.com (Spidey)', subject: subject.replace(/ \(\d+\/\d+\)$/, ''), name: parseSubject(subject)!.name, weak: false })! + const album = 'A Strange Light from the East - Eastern Influenced Western Music (1965-72) - CD2' + assert.equal(id(`${album} - 07 - Rascals - Sattva.mp3 (1/43)`).key, id(`${album} - 08 - Incredible String Band - Koeeoaddi There.mp3 (1/47)`).key) + }) + test('image sets: sizes in brackets and one-word set names', ({ assert }) => { const id = (subject: string, name: string) => releaseIdentity({ poster: 'p', subject, name, weak: false })! assert.equal(id('Aletta Ocean - "100628-035.jpg" yEnc [347K]', '100628-035.jpg').key, id('Aletta Ocean - "100628-036.jpg" yEnc [298K]', '100628-036.jpg').key)