From c4f48a6d4c436ebb4f968fb03a9203f557c08f01 Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Mon, 12 Oct 2026 00:45:52 +0200 Subject: [PATCH] Image sets by their numbered name, digits kept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a file counter a subject-named image release was bounded by the image's name with all numbers removed: - STIRILING SILVER's sets stp250x…stp465x became one 44k-image release (all "stp x"); picture threads of many sets (keira_s04, keira_s05, perla_s01, apneatic-7, -8 …) one release each. - Free-text names without numbers ("JimAndJoey Kindly Husband-to-Be.JPG") made each image a set: one thread was 1,177 releases. The bound is now the name up to its last number with its digits ("stp250x", "keira_s05_l keira_s05_", "apneatic-7-"), and nothing for a name without numbers. models fixture: 172 -> 754 releases, by set. Both implementations; tests; fixtures identical. Co-Authored-By: Claude Opus 5.5 --- edge/src/names.rs | 24 +++++++++++++++++++++++- lib/release/identity.ts | 6 +++++- tests/unit/release/identity.spec.ts | 8 ++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/edge/src/names.rs b/edge/src/names.rs index ef2089c..101b245 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -414,6 +414,18 @@ static AUDIO: LazyLock = LazyLock::new(|| re(r"(?i)\.(mp3|flac|m4a|m4b|og static DOCUMENT: LazyLock = LazyLock::new(|| re(r"(?i)\.(pdf|epub|mobi|azw3?|djvu|cbr|cbz|cb7|chm|lit|docx?)$")); /// An image set's name without a counter: up to the last number, without numbers. +/// `/\d/.test(stem) ? stem.replace(/\d+\D*$/, '') : ''`: an image's set, digits kept. +fn image_set(stem: &str) -> String { + let b = stem.as_bytes(); + match b.iter().rposition(u8::is_ascii_digit) { + Some(last) => { + let start = b[..=last].iter().rposition(|c| !c.is_ascii_digit()).map_or(0, |i| i + 1); + stem[..start].to_string() + } + None => String::new(), + } +} + fn image_prefix(stem: &str) -> String { let b = stem.as_bytes(); let cut = match b.iter().rposition(u8::is_ascii_digit) { @@ -599,7 +611,8 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { // An image set's subject may be one word, the model's name: /^[A-Za-z]{3,}$/. let one_word = image && remainder.len() >= 3 && remainder.bytes().all(|c| c.is_ascii_alphabetic()); if !tag && (names_release(&remainder) || one_word) { - let bound = counter_total(subject).unwrap_or_else(|| prefix.clone()); + // Without a counter an image's set: its name up to its last number, digits kept; none without numbers. + let bound = counter_total(subject).unwrap_or_else(|| if image { image_set(&stem) } else { String::new() }); return Release { key: hash(&["subject", poster, &remainder, &bound]), name: remainder, source: "subject", file_total }; } if let Some(total) = post_total.filter(|&n| n != 0 && !tag) { @@ -1117,6 +1130,15 @@ mod image_set_tests { assert!(!random_poster("w.merklein@freenet.de (muffin)")); } + #[test] + fn image_sets_by_their_numbered_name() { + let silver = |n: &str| release_identity("p", &format!(r#"The excellent STIRILING SILVER - Always popular - "{n}" yEnc 320794 bytes"#), n); + assert_eq!(silver("stp250x001.jpg").key, silver("stp250x002.jpg").key); + assert_ne!(silver("stp250x001.jpg").key, silver("stp305x001.jpg").key); + let mint = |n: &str| release_identity("Cert", &format!(r#"I'm not a breath mint, but I am not all there either. [{n}" yEnc]"#), n); + assert_eq!(mint("JimAndJoey Kindly Husband-to-Be.JPG").key, mint("Oops OnlyOneMore.JPG").key); + } + #[test] fn dates_are_no_counters() { use super::counter_total; diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 43346d7..f82feae 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -254,8 +254,12 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { // Bounded by the counter's total, so the uploads a poster names alike are apart while the files // of one upload (par2 and nfo included) share it (LOG.md 2026-10-09 (9)). const total = counterTotal(input.subject) + // Without a counter an image's set bounds it: its name up to its last number, digits kept ("stp250x001.jpg" + // → "stp250x": STIRILING SILVER's 300 sets were one release of 44k images); a name without numbers names no + // set ("Kindly Husband-to-Be.JPG": a thread of 1,177 releases), so the subject alone (LOG.md 2026-10-12 (2)). + const set = image && /\d/.test(stem) ? stem.replace(/\d+\D*$/, '') : '' return { - key: hash('subject', poster, remainder, total ?? prefix), + key: hash('subject', poster, remainder, total ?? set), name: remainder, fileTotal, source: 'subject', diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index 444d456..e9c55a3 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -214,6 +214,14 @@ test.group('release / par2 names', () => { assert.isFalse(randomPoster('w.merklein@freenet.de (muffin)')) }) + test('image sets by their numbered name, digits kept; names without numbers name no set', ({ assert }) => { + const silver = (n: string) => releaseIdentity({ poster: 'p', subject: `The excellent STIRILING SILVER - Always popular - "${n}" yEnc 320794 bytes`, name: n, weak: false })! + assert.equal(silver('stp250x001.jpg').key, silver('stp250x002.jpg').key) + assert.notEqual(silver('stp250x001.jpg').key, silver('stp305x001.jpg').key) + const mint = (n: string) => releaseIdentity({ poster: 'Cert', subject: `I'm not a breath mint, but I am not all there either. [${n}" yEnc]`, name: n, weak: false })! + assert.equal(mint('JimAndJoey Kindly Husband-to-Be.JPG').key, mint('Oops OnlyOneMore.JPG').key) + }) + test('dates are no file counters', async ({ assert }) => { const { counterTotal } = await import('#lib/release/identity') assert.isUndefined(counterTotal('Avira AntiVir Rescue System 08-29-2011.rar'))