From 7d9a81784e3a0580d624abc3618be8bfe20a3538 Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Fri, 16 Oct 2026 10:13:20 +0200 Subject: [PATCH] A folder's images posted with their sizes are one release: no image set under the subject Co-Authored-By: Claude Opus 5.5 --- edge/src/names.rs | 38 +++++++++++++++++++++++++++-- lib/release/identity.ts | 22 ++++++++++++++++- tests/unit/release/identity.spec.ts | 16 ++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/edge/src/names.rs b/edge/src/names.rs index 93dda87..bacb8b5 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -744,8 +744,25 @@ pub fn size_after_name(subject: &str, name: &str) -> String { } } +/// A name posted unquoted with its size in bytes right after it, at the subject's end ("… Nude Celebrity Pictures <<<<< - +/// !index0001.jpg 75220 bytes"): a tool that posts a folder under the subject typed for it (sizedName). Not a quoted name. +pub fn sized_name(subject: &str, name: &str) -> bool { + let s = subject.trim_end(); + let lower = s.to_ascii_lowercase(); + let unit = if lower.ends_with("bytes") { 5 } else if lower.ends_with("byte") { 4 } else { return false }; + let s = s[..s.len() - unit].trim_end(); + // /\d[\d.,]*$/: from the first digit of the trailing run of digits, dots and commas. + let run = s.len() - s.trim_end_matches(|c: char| c.is_ascii_digit() || c == '.' || c == ',').len(); + let tail = &s[s.len() - run..]; + let Some(first) = tail.find(|c: char| c.is_ascii_digit()) else { return false }; + let s = s[..s.len() - run + first].trim_end(); + let s = s.strip_suffix('-').unwrap_or(s).trim_end(); + !name.is_empty() && s.ends_with(name) && !s.ends_with(&format!("\"{name}\"")) +} + pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { let poster = if random_poster(poster) { "" } else { poster }; + let sized = sized_name(subject, name); // The post rule's key takes the [i/M] total; the release's file total any counter's (counterTotal). let post_total = file_counter_total(subject); let stem = random_volume_base(file_stem(run_on(name))); @@ -813,7 +830,8 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { let one_word = image && remainder.len() >= 3 && remainder.bytes().all(|c| c.is_ascii_alphabetic()); if !tag && (names_release(&remainder) || one_word) { // Without a counter an image's set: its name up to its last number, digits kept; none without numbers. - let bound = total.clone().unwrap_or_else(|| if image { image_set(&stem) } else { String::new() }); + // A folder's images posted with their sizes: the subject names the folder, the images no set. + let bound = total.clone().unwrap_or_else(|| if image && !sized { image_set(&stem) } else { String::new() }); let mut parts: Vec<&str> = vec!["subject", poster, &remainder, &bound]; // An upload's random stem on its volumes ("Z3mMTMPsoT4ikE_Di3fKnNF21.part01.rar" under "[ TOWN ] … [ TV ]"). let stripped: String = stem.chars().filter(|c| !matches!(c, '-' | '_')).collect(); @@ -1443,7 +1461,7 @@ mod numbered_tests { #[cfg(test)] mod image_set_tests { - use super::{release_identity, subject_remainder, subject_remainder_re}; + use super::{release_identity, sized_name, subject_remainder, subject_remainder_re}; #[test] fn random_posters_are_no_part_of_the_key() { @@ -1502,6 +1520,22 @@ mod image_set_tests { assert_eq!(mint("JimAndJoey Kindly Husband-to-Be.JPG").key, mint("Oops OnlyOneMore.JPG").key); } + #[test] + fn a_folders_images_posted_with_their_sizes_are_one_release() { + let celeb = |n: &str, size: u32| release_identity("@_CelebMovie_@", &format!(">>>>> Jamie Lee Curtis - Nude Celebrity Pictures <<<<< - {n} {size} bytes"), n); + let keys: std::collections::HashSet = + ["!index0001.jpg", "00-00-02_123_842lo.jpg", "00_00_13.jpg", "bboobs02-curtis-jl.jpg", "0c4_jamie_lee_curtis_15.jpg"].iter().enumerate().map(|(i, n)| celeb(n, 75220 + i as u32).key).collect(); + assert_eq!(keys.len(), 1); + assert_eq!(celeb("00_00_13.jpg", 58748).source, "subject"); + assert_ne!(celeb("00_00_13.jpg", 1).key, release_identity("@_CelebMovie_@", ">>>>> Carmen Electra - Xmas Nudes <<<<< - 00_00_13.jpg 1 bytes", "00_00_13.jpg").key); + let ctp = |n: &str| release_identity("new@update.org (New CTP update)", &format!("*** NEW *** CTP update & adds - {n} - 18441 bytes"), n); + assert_eq!(ctp("cc_CG2008_v10_01_Stephanie_Emma.jpg").key, ctp("cc_CastingCall_LasVegas_01_Kristin_00_Header1.jpg").key); + assert!(sized_name("x - a.jpg 1,234 Bytes", "a.jpg")); + assert!(!sized_name(r#"x - "a.jpg" yEnc 320794 bytes"#, "a.jpg")); + assert!(!sized_name("x - a.jpg 5 megabytes", "a.jpg")); + assert!(!sized_name("x - a.jpg yEnc", "a.jpg")); + } + #[test] fn other_quoted_text_and_text_separators_name_the_release() { let cc = |name: &str, n: &str| release_identity("p", &format!(r#">>> CREATIVE COMMONS NZB <<< "{name} " - File 1 of 9: "creative_commons_nzb_{n}.rar" yEnc"#), &format!("creative_commons_nzb_{n}.rar")); diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 0e17791..14d47a8 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -332,6 +332,23 @@ export function runOn(name: string): string { return name } +/** + * A name posted unquoted with its size in bytes right after it, at the subject's end ("… Nude Celebrity Pictures <<<<< - + * !index0001.jpg 75220 bytes", "*** NEW *** CTP update & adds - cc_CG2008_v10_01_Stephanie_Emma.jpg - 18441 bytes"): + * a tool that posts a folder under the subject typed for it, file by file. Not a quoted name ("The excellent STIRILING + * SILVER - Always popular - "stp250x001.jpg" yEnc 320794 bytes"). + */ +export function sizedName(subject: string, name: string): boolean { + let s = subject.trimEnd() + const unit = /bytes?$/i.exec(s) + if (!unit) return false + s = s.slice(0, unit.index).trimEnd() + const digits = /\d[\d.,]*$/.exec(s) + if (!digits) return false + s = s.slice(0, digits.index).trimEnd().replace(/-$/, '').trimEnd() + return name.length > 0 && s.endsWith(name) && !s.endsWith(`"${name}"`) +} + /** The subject without a number after the file's unquoted name at its end (" - 28": a size in kB). */ export function sizeAfterName(subject: string, name: string): string { const end = subject.trimEnd() @@ -419,7 +436,10 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { // 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 ? imageSet(stem) : '' + // A folder's images posted with their sizes: the subject names the folder, the images no set (a celebrity's + // scraped pictures, "!index0001.jpg", "00_00_13.jpg", "bboobs02-curtis-jl.jpg", were 219 releases of 1,423 images; + // LOG.md 2026-10-16 (63)). + const set = image && !sizedName(input.subject, input.name) ? imageSet(stem) : '' const parts: (string | number)[] = ['subject', poster, remainder, total ?? set] // An upload's random stem on its volumes ("[ TOWN ] [ www town ag ] [ TV ] … "Z3mMTMPsoT4ikE_Di3fKnNF21.part01.rar"", // "….vol4+4.par2"): under a constant tag every upload of a total was one release (2,338 files for 20; LOG.md diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index c37437e..5d3a0dc 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -222,6 +222,22 @@ test.group('release / par2 names', () => { assert.equal(mint('JimAndJoey Kindly Husband-to-Be.JPG').key, mint('Oops OnlyOneMore.JPG').key) }) + test("a folder's images posted with their sizes are one release: the subject names it", async ({ assert }) => { + const celeb = (n: string, size: number) => + releaseIdentity({ poster: '@_CelebMovie_@', subject: `>>>>> Jamie Lee Curtis - Nude Celebrity Pictures <<<<< - ${n} ${size} bytes`, name: n, weak: false })! + const keys = new Set(['!index0001.jpg', '00-00-02_123_842lo.jpg', '00_00_13.jpg', 'bboobs02-curtis-jl.jpg', '0c4_jamie_lee_curtis_15.jpg'].map((n, i) => celeb(n, 75220 + i).key)) + assert.equal(keys.size, 1) + assert.equal(celeb('00_00_13.jpg', 58748).source, 'subject') + assert.notEqual(celeb('00_00_13.jpg', 1).key, releaseIdentity({ poster: '@_CelebMovie_@', subject: '>>>>> Carmen Electra - Xmas Nudes <<<<< - 00_00_13.jpg 1 bytes', name: '00_00_13.jpg', weak: false })!.key) + const ctp = (n: string) => releaseIdentity({ poster: 'new@update.org (New CTP update)', subject: `*** NEW *** CTP update & adds - ${n} - 18441 bytes`, name: n, weak: false })! + assert.equal(ctp('cc_CG2008_v10_01_Stephanie_Emma.jpg').key, ctp('cc_CastingCall_LasVegas_01_Kristin_00_Header1.jpg').key) + const { sizedName } = await import('#lib/release/identity') + assert.isTrue(sizedName('x - a.jpg 1,234 Bytes', 'a.jpg')) + assert.isFalse(sizedName('x - "a.jpg" yEnc 320794 bytes', 'a.jpg')) + assert.isFalse(sizedName('x - a.jpg 5 megabytes', 'a.jpg')) + assert.isFalse(sizedName('x - a.jpg yEnc', 'a.jpg')) + }) + test("an image's set: suffixes that don't count the images go first", async ({ assert }) => { const { imageSet } = await import('#lib/release/identity') for (const [stem, set] of [