From 358f5a8571acf9e7bba3d9fc4b40217ba4ef388c Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Mon, 12 Oct 2026 10:52:35 +0200 Subject: [PATCH] Names: split rar volumes; a file's size in brackets after its counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "rld-gtre.r13.002": a split volume after a rar volume is stripped like one after .rar (usenet-town's posts were 1,620 single files). - "[26/32] - [4929] "…part24.rar"": the bracketed size after the counter goes (Kingdn's episodes, 2,850 single files). Co-Authored-By: Claude Opus 5.5 --- edge/src/names.rs | 16 ++++++++++++++-- lib/release/identity.ts | 5 ++++- tests/unit/release/identity.spec.ts | 7 +++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/edge/src/names.rs b/edge/src/names.rs index 054d5ff..45e133a 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -36,7 +36,7 @@ static SUFFIXES: LazyLock> = LazyLock::new(|| { (re(r"(?i)\.7z(\.[0-9]{3})?$"), ""), (re(r"(?i)\.zip(\.[0-9]{3})?$"), ""), // JS: (?<=\.(7z|…))\.\d{3}$ — no lookbehind here, so the extension is matched and put back. - (re(r"(?i)(\.(?:7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob))\.[0-9]{2,3}$"), "$1"), + (re(r"(?i)(\.(?:7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob|r[0-9]{2,3}))\.[0-9]{2,3}$"), "$1"), (re(r"(?i)\.(nfo|sfv|nzb|srr|srs|txt|jpg|png|md5|sha1|srt|sub|idx|ass|ssa|m3u|m3u8|cue|log)$"), ""), (re(r"(?i)\.tar$"), ""), (re(r"(?i)\.(zst|gz|xz|bz2)$"), ""), @@ -174,7 +174,8 @@ fn strip_suffix_rule(s: &[u8], rule: usize) -> Option { // (?<=\.(7z|…))\.\d{2,3}$: only the volume number goes. 7 => { let m = strip_volume_digits(s)?; - strip_ext(&s[..m], &SPLIT_EXTS).map(|_| m) + // Also after a rar volume ("rld-gtre.r13.002"): \.r\d{2,3} before it. + strip_ext(&s[..m], &SPLIT_EXTS).or_else(|| strip_suffix_rule(&s[..m], 3)).map(|_| m) } 8 => strip_ext(s, &SIDE_EXTS), 9 => strip_ext(s, &[b"tar"]), @@ -652,6 +653,8 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { // The name repeated elsewhere ("(NELET11.par2) [01/58] - "NELET11.par2""). let without = if name.encode_utf16().count() >= 5 { without.replace(name, " ") } else { without }; let named = without.replace('"', " "); + // A file's size in brackets after its counter ("[26/32] - [4929] "…part24.rar""). + let named = replace(&SIZE_AFTER_COUNTER, &named, "$1", true); let remainder = subject_remainder(&named); // Counted without the file name: "shyla-shy-09-0195.jpg hardcore …" is no "9 of 195". let total = counter_total(&named); @@ -731,6 +734,7 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { Release { key: hash(&["stem", poster, &base]), name: stem, source: "stem", file_total } } +static SIZE_AFTER_COUNTER: LazyLock = LazyLock::new(|| re(r"(\[[0-9]+/[0-9]+\])SP*-SP*\[[0-9]+\]")); static HASHED: LazyLock = LazyLock::new(|| re(r"(?i)^[0-9a-f]{8,}(?:_[0-9a-f]{6,}){2,}(?:-[0-9a-z]+)?$")); static SCENE_TRACK: LazyLock = LazyLock::new(|| re(r"^[0-9]{1,3}[-_ .]+(.+?)(?:_-_| - |-)")); static VOLUME_NUMBER: LazyLock = LazyLock::new(|| re(r"^(.*[^0-9. ])[. ]([0-9]{1,5})\.?$")); @@ -1292,6 +1296,14 @@ mod image_set_tests { assert_ne!(vlad("185", 77, "P8250001.JPG").key, vlad("234", 77, "P5300085.JPG").key); } + #[test] + fn split_rar_volumes_and_sizes_after_the_counter() { + assert_eq!(super::file_stem("rld-gtre.r13.002"), "rld-gtre"); + assert_eq!(super::file_stem_re("rld-gtre.r13.002"), "rld-gtre"); + let k = |i: u32, size: u32, n: &str| release_identity("Kingdn", &format!(r#"Buffy The Vampire Slayer s02e04 [{i}/32] - [{size}] "{n}" yEnc"#), n); + assert_eq!(k(26, 4929, "Buffy s02e04 xvid-tns.part24.rar").key, k(27, 400, "Buffy s02e04 xvid-tns.vol00+01.PAR2").key); + } + #[test] fn totals_padded_wider_than_the_index_are_no_counters() { let who = |i: u32, n: &str| release_identity("p", &format!(r#"Very Best Of The Who [{i:02}/{:03}] - "{n}" yEnc"#, i * 10), n); diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 5883596..177fb3d 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -33,7 +33,8 @@ const SUFFIXES = [ /\.7z(\.\d{3})?$/i, /\.zip(\.\d{3})?$/i, // Split volumes (x.mkv.001), not codec names (H.264, H.265). - /(?<=\.(7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob))\.\d{2,3}$/i, + // Also a split rar volume ("rld-gtre.r13.002": 1,620 single files of usenet-town's posts). + /(?<=\.(7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob|r\d{2,3}))\.\d{2,3}$/i, /\.(nfo|sfv|nzb|srr|srs|txt|jpg|png|md5|sha1|srt|sub|idx|ass|ssa|m3u|m3u8|cue|log)$/i, /\.tar$/i, /\.(zst|gz|xz|bz2)$/i, @@ -299,6 +300,8 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { .split(input.name.length >= 5 ? input.name : '\0') .join(' ') .replace(/"/g, ' ') + // A file's size in brackets after its counter ("[26/32] - [4929] "…part24.rar"": each file a release; Kingdn). + .replace(/(\[\d+\/\d+\])\s*-\s*\[\d+\]/g, '$1') const remainder = subjectRemainder(named) // Counted without the file name, as quoted names are: "shyla-shy-09-0195.jpg hardcore …" is no "9 of 195" // (each of 866 images was a release of its own; LOG.md 2026-10-12 (5)). diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index 3bd96f9..c506ce8 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -276,6 +276,13 @@ test.group('release / par2 names', () => { assert.notEqual(vlad('185', 77, 'P8250001.JPG').key, vlad('234', 77, 'P5300085.JPG').key) }) + test("split rar volumes; a file's size in brackets after its counter", ({ assert }) => { + assert.equal(fileStem('rld-gtre.r13.002'), 'rld-gtre') + const k = (i: number, size: number, n: string) => + releaseIdentity({ poster: 'Kingdn', subject: `Buffy The Vampire Slayer s02e04 [${i}/32] - [${size}] "${n}" yEnc`, name: n, weak: false })! + assert.equal(k(26, 4929, 'Buffy s02e04 xvid-tns.part24.rar').key, k(27, 400, 'Buffy s02e04 xvid-tns.vol00+01.PAR2').key) + }) + test('a total padded wider than its index is no counter', ({ assert }) => { const who = (i: number, n: string) => releaseIdentity({ poster: 'p', subject: `Very Best Of The Who [${String(i).padStart(2, '0')}/${String(i * 10).padStart(3, '0')}] - "${n}" yEnc`, name: n, weak: false })!