Image sets by their numbered name, digits kept
Test and deploy / test (push) Successful in 44s
Test and deploy / edge (push) Successful in 1m52s
Test and deploy / deploy (push) Successful in 2m1s

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 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-12 00:45:52 +02:00
1 parent 0577b16e5e
commit c4f48a6d4c
3 files changed
+36 -2

No files matched your search

+23 -1
View File
@@ -414,6 +414,18 @@ static AUDIO: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)\.(mp3|flac|m4a|m4b|og
static DOCUMENT: LazyLock<Regex> = 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;
+5 -1
View File
@@ -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',
+8
View File
@@ -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'))