From e1d21d1e7c20729e3372e4f22a3645e7dc8b6d2f Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Mon, 12 Oct 2026 06:46:19 +0200 Subject: [PATCH] Identity: a set's number before yEnc is no size; names after an arrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The unit-less size before the closing yEnc ('"x.mp3" - 8123 yEnc') took the set number in 'Newstar Destiny I set 1 [1 of 60] "destiny-001-001.jpg" yEnc' once the quotes were gone: 97 sets of 60 were one release. A number of fewer than 4 digits after a word stays. - unquotedName also cuts after "-> " ("… -> [0001 of 0076] -> Exclusive-tgirls_000000-lg.jpg"): the arrow was part of the file name. Co-Authored-By: Claude Opus 5.5 --- edge/src/indexer.rs | 5 ++++- edge/src/names.rs | 14 +++++++++++++- lib/headers/subject.ts | 4 +++- lib/release/identity.ts | 7 +++++-- tests/unit/headers/subject.spec.ts | 1 + tests/unit/release/identity.spec.ts | 11 +++++++++++ 6 files changed, 37 insertions(+), 5 deletions(-) diff --git a/edge/src/indexer.rs b/edge/src/indexer.rs index 0aa1e45..ef26f49 100644 --- a/edge/src/indexer.rs +++ b/edge/src/indexer.rs @@ -338,7 +338,9 @@ pub fn unquoted_name(before_counter: &str) -> Option { let bracket = text.rfind("] ").map_or(0, |i| i + 2); // 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)..]); + // Or after an arrow ("… -> Exclusive-tgirls_000000-lg.jpg"). + let arrow = text.rfind("-> ").map_or(0, |i| i + 3); + let name = js::trim(&text[bracket.max(dash).max(arrow)..]); return (!name.is_empty() && has_extension(name)).then(|| name.to_string()); } text.split(' ').find(|w| has_extension(w)).map(str::to_string) @@ -910,6 +912,7 @@ mod unquoted_tests { ("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")), + (" FP -> {EXL01~P~004} -> Alexia (3) -> VF -> [0001 of 0076] -> Exclusive-tgirls_000000-lg.jpg ", Some("Exclusive-tgirls_000000-lg.jpg")), ("[7w] - 9BbEV2MrTi59GBTEFlVrssBrXNwXf2SK9cQEeDjg yEnc ", None), ("Re: Will somebody answer the question? ", None), ]; diff --git a/edge/src/names.rs b/edge/src/names.rs index aaec84d..e7a527b 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -360,6 +360,11 @@ fn strip_number_before_yenc(s: &str) -> String { if digits_start > 0 && (b[digits_start - 1].is_ascii_alphanumeric() || b[digits_start - 1] == b'_') { return s.to_string(); } + // A short number after a word is a set's ("… set 1 [1 of 60] "destiny-001-001.jpg" yEnc"), not a size. + let word_before = b[..digits_start].iter().rev().find(|&&c| !js::is_space(c as char)).is_some_and(|c| c.is_ascii_alphanumeric()); + if digits_end - digits_start < 4 && word_before { + return s.to_string(); + } format!("{}{}", &s[..digits_start], &s[digits_end..]) } @@ -1273,7 +1278,14 @@ mod image_set_tests { let a = release_identity(mb, r#"Herb Ernst@256 - NMR -[07/17] "Herb Ernst - Dreamflight II - 05 - The Light of Forever.mp3" - 8123 yEnc"#, "Herb Ernst - Dreamflight II - 05 - The Light of Forever.mp3"); let b = release_identity(mb, r#"Herb Ernst@256 - NMR -[08/17] "Herb Ernst - Dreamflight II - 06 - Other.mp3" - 9001 yEnc"#, "Herb Ernst - Dreamflight II - 06 - Other.mp3"); assert_eq!(a.key, b.key); - for s in [r#"x - "a.mp3" - 8123 yEnc"#, "a8123 yEnc", "-8123 yEnc ", "yEnc"] { + let destiny = |set: u32, i: u32| { + let name = format!("destiny-{set:03}-{i:03}.jpg"); + release_identity("p", &format!(r#"Newstar Destiny I set {set} [{i} of 60] "{name}" yEnc"#), &name) + }; + assert_eq!(destiny(1, 1).key, destiny(1, 2).key); + assert_ne!(destiny(1, 1).key, destiny(63, 34).key); + assert_eq!(destiny(63, 34).name, "Newstar Destiny I set 63"); + for s in [r#"x - "a.mp3" - 8123 yEnc"#, "a8123 yEnc", "-8123 yEnc ", "yEnc", r#"set 1 [1 of 60] "x.jpg" yEnc"#, "Album 12 yEnc", "Album 1234 yEnc"] { assert_eq!(super::subject_remainder(s), super::subject_remainder_re(s), "{s}"); } assert!(random_poster("SOFZRw1-OTEd@EEFUbh.srG")); diff --git a/lib/headers/subject.ts b/lib/headers/subject.ts index 7d52b1e..44b52a0 100644 --- a/lib/headers/subject.ts +++ b/lib/headers/subject.ts @@ -63,7 +63,9 @@ export function unquotedName(beforeCounter: string): string | null { // 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) + // Or after an arrow ("… -> [0001 of 0076] -> Exclusive-tgirls_000000-lg.jpg"). + const arrow = text.lastIndexOf('-> ') + const cut = Math.max(bracket === -1 ? 0 : bracket + 2, dash === -1 ? 0 : dash + 3, arrow === -1 ? 0 : arrow + 3) const name = text.slice(cut).trim() return name && EXTENSION.test(name) ? name : null } diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 4a3a917..133060f 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -77,8 +77,11 @@ export function subjectRemainder(subject: string): string { // File sizes ("76.88 MB", "335555 Bytes", "336573 byt" cut off, "[347K]") differ per file. .replace(/\b\d+(?:[.,]\d+)?\s*(?:[kmgt]i?b|bytes?|byt)\b/gi, ' ') .replace(/\[\s*\d+(?:[.,]\d+)?\s*[kmgt]\s*\]/gi, ' ') - // A size without a unit right before the closing yEnc ("…mp3" - 8123 yEnc"). - .replace(/\b\d+(\s+yEnc\s*)$/i, '$1') + // A size without a unit right before the closing yEnc ("…mp3" - 8123 yEnc"); a short number after a word is a + // set's ("… set 1 [1 of 60] "destiny-001-001.jpg" yEnc": 97 sets were one release; LOG.md 2026-10-12 (8)). + .replace(/\b(\d+)(\s+yEnc\s*)$/i, (text, n: string, end: string, at: number, s: string) => + n.length >= 4 || !/[A-Za-z0-9]\s*$/.test(s.slice(0, at)) ? end : text + ) // File sizes follow "yEnc" at the end; other numbers (years, post ids) stay. .replace(/\byEnc\b(\s+\d+)?\s*$/i, ' ') .replace(/\byEnc\b/gi, ' ') diff --git a/tests/unit/headers/subject.spec.ts b/tests/unit/headers/subject.spec.ts index 0a5fd73..0f5fcde 100644 --- a/tests/unit/headers/subject.spec.ts +++ b/tests/unit/headers/subject.spec.ts @@ -64,6 +64,7 @@ test.group('subject / parseSubject', () => { ['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], + [' FP -> {EXL01~P~004} -> Alexia (3) -> VF -> [0001 of 0076] -> Exclusive-tgirls_000000-lg.jpg (1/1)', 'Exclusive-tgirls_000000-lg.jpg', 1, 1], ] for (const [subject, name, segment, total, fileIndex] of cases) { const parsed = parseSubject(subject) diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index 7f58d94..58b454a 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -251,6 +251,17 @@ test.group('release / par2 names', () => { ]) assert.equal(imageSet(stem), set, stem) }) + test("a set's number before the closing yEnc is no size", ({ assert }) => { + const id = (subject: string, name: string) => releaseIdentity({ poster: 'p', subject, name, weak: false })! + const destiny = (set: number, i: number) => { + const name = `destiny-${String(set).padStart(3, '0')}-${String(i).padStart(3, '0')}.jpg` + return id(`Newstar Destiny I set ${set} [${i} of 60] "${name}" yEnc`, name) + } + assert.equal(destiny(1, 1).key, destiny(1, 2).key) + assert.notEqual(destiny(1, 1).key, destiny(63, 34).key) + assert.equal(destiny(63, 34).name, 'Newstar Destiny I set 63') + }) + test('an image without words around it is keyed by its set', ({ assert }) => { const id = (name: string, counter: string) => releaseIdentity({ poster: 'p', subject: `##[ "${name}" (${counter})`, name, weak: false })! assert.equal(id('diana-040-001.jpg', '01/68').key, id('diana-040-002.jpg', '02/68').key)