diff --git a/edge/src/names.rs b/edge/src/names.rs index 7f59296..1c80f6e 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -579,10 +579,17 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { let image = test(&IMAGE, name); // The poster's constant tag outside the quotes: the file's stem says which upload it is (not for images and audio). // Not for numbered items of a set ("003 Defining Tools with JSON Schema.mp4"): /^\d{1,3}[ ._-]/. + // /^(?:[A-Z]{1,5}\d{2,6}[-_ ])?\d{1,3}[ ._-]/: also after a disc code ("SC8367-12 - …"). let numbered = { let b = stem.as_bytes(); - let digits = b.iter().take_while(|c| c.is_ascii_digit()).count(); - (1..=3).contains(&digits) && matches!(b.get(digits), Some(b' ' | b'.' | b'_' | b'-')) + let index_at = |at: usize| { + let digits = b[at..].iter().take_while(|c| c.is_ascii_digit()).count(); + (1..=3).contains(&digits) && matches!(b.get(at + digits), Some(b' ' | b'.' | b'_' | b'-')) + }; + let letters = b.iter().take_while(|c| c.is_ascii_uppercase()).count(); + let code_digits = b[letters..].iter().take_while(|c| c.is_ascii_digit()).count(); + let code = (1..=5).contains(&letters) && (2..=6).contains(&code_digits) && matches!(b.get(letters + code_digits), Some(b'-' | b'_' | b' ')); + index_at(0) || (code && index_at(letters + code_digits + 1)) }; let tag = !image && !numbered && !test(&AUDIO, name) && describes(&stem) && !looks_random(&stem) && is_poster_tag(&remainder, &stem); let prefix = if image { image_prefix(&stem) } else { String::new() }; @@ -1085,6 +1092,8 @@ mod numbered_tests { let b = release_identity(poster, r#"ai-engineer-complete [422/465] - "006 ai-lesson-4-5-web-search-tools.zip" yEnc"#, "006 ai-lesson-4-5-web-search-tools.zip"); assert_eq!(a.key, b.key); assert_eq!(a.name, "ai engineer complete"); + let sc = |t: &str| release_identity("p", &format!("Spotlight - 83xx's [16/19] - \"{t}.zip\" yEnc"), &format!("{t}.zip")); + assert_eq!(sc("SC8367-12 - Wynette, Tammy - Singing My Song").key, sc("SC8367-13 - Jones, George - He Stopped Loving Her Today").key); } } diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 44c18ac..93f22d3 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -237,7 +237,8 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { // subject names as a set or album ("IMG_3086.jpg", "072 - Die Teufelsuhr.MP3"), nor for numbered items of // a set ("ai-engineer-complete [401/465] - "003 Defining Tools with JSON Schema.mp4"": a course's 465 // lessons were releases of their own; LOG.md 2026-10-11 (7)). Uploads hours apart are apart anyway. - const numbered = /^\d{1,3}[ ._-]/.test(stem) + // Also after a disc code ("SC8367-12 - Wynette, Tammy - Singing My Song", karaoke discs). + const numbered = /^(?:[A-Z]{1,5}\d{2,6}[-_ ])?\d{1,3}[ ._-]/.test(stem) const tag = !image && !numbered && !AUDIO.test(input.name) && describes(stem) && !looksRandom(stem) && isPosterTag(remainder, stem) // An image set without a counter: the image's name up to its last number, without numbers // ("Kitchen Hottie 1 set233-76-lg" → "Kitchen Hottie set", "bm_jessica_blb_h087" → "bm jessica blb h"). diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index 6e41992..7cfba76 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -233,6 +233,9 @@ test.group('release / par2 names', () => { const b = id('ai-engineer-complete [422/465] - "006 ai-lesson-4-5-web-search-tools.zip" yEnc', '006 ai-lesson-4-5-web-search-tools.zip') assert.equal(a.key, b.key) assert.equal(a.name, 'ai engineer complete') + // Karaoke discs: a disc code before the track number. + const sc = (track: string) => releaseIdentity({ poster: 'p', subject: `Spotlight - 83xx's [16/19] - "${track}.zip" yEnc`, name: `${track}.zip`, weak: false })! + assert.equal(sc('SC8367-12 - Wynette, Tammy - Singing My Song').key, sc('SC8367-13 - Jones, George - He Stopped Loving Her Today').key) }) test("a poster's constant tag doesn't make one release of all their uploads", ({ assert }) => {