Rebuild: image sets, documents and audio albums as releases
Test and deploy / test (push) Successful in 22s
Test and deploy / deploy (push) Skipped

Picture groups made a release per image: 501k single-file releases in
the first 24 ranges of the bottom 500 groups (a.b.models 385k), ~120M
for all of them. Counters written out ('File 027 of 132', '[12of42]',
'104 of 472', '[2239-6389]', '02/17', '0097-1574'; only when the first
number is at most the second, so '[1948-12]' stays) and per-file sizes
('76.88 MB', '335555 Bytes') kept every subject unique.

- subjectRemainder removes those counters and sizes, and empty brackets.
- A subject-named release is bounded by its counter's total
  (counterTotal: the first form found): uploads a poster names alike
  stay apart, an upload's files (par2, nfo) together.
- Images and audio tracks don't take the poster-tag rule: the subject
  names the set or album. An image set without a counter: the image's
  name up to its last number, without numbers.
- Documents and comics (pdf, epub, mobi, azw3, djvu, cbr, cbz, cb7, chm,
  lit, doc, docx) are a release each: collections post hundreds under
  one subject ('cw-horror&mystery').

releases:regroup --group re-keys a group's indexed releases together:
cheerleaders 55,741 -> 3,165 releases, models 385,298 -> 40,313, German
audiobooks 4,106 -> 1,724, playstation 3,309 -> 2,345; comics and
e-books a release per item; boneless unchanged. Ported to edge-bench-rs;
check.ts: identical on all captures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-09 12:21:00 +02:00
1 parent 59481037fb
commit 14758a5b7a
4 files changed
+272 -15

No files matched your search

+154 -6
View File
@@ -76,10 +76,11 @@ static SEPARATORS: LazyLock<Regex> = LazyLock::new(|| re(r"(?:SP|[-.:|]|\xE2\x80
pub fn subject_remainder_re(subject: &str) -> String {
let s = replace(&QUOTED, subject, " ", true);
let s = replace(&COUNTERS, &s, " ", true);
let s = remove_counters_and_sizes(&s);
let s = replace(&YENC_END, &s, " ", false);
let s = replace(&YENC, &s, " ", true);
let s = replace(&SEPARATORS, &s, " ", true);
js::trim(&s).to_string()
finish_remainder(&s)
}
fn eq_ci(a: &[u8], b: &[u8]) -> bool {
@@ -262,7 +263,7 @@ pub fn subject_remainder(subject: &str) -> String {
i += 1;
}
// Only ASCII was replaced, between whole characters.
let mut s3 = String::from_utf8(s2).unwrap();
let mut s3 = remove_counters_and_sizes(&String::from_utf8(s2).unwrap());
// \byEnc\b(\s+\d+)?\s*$ → ' ' (the first place where it fits)
let b3 = s3.as_bytes();
if let Some(at) = (0..b3.len()).find(|&i| yenc_at(b3, i) && yenc_tail(&s3[i + 4..])) {
@@ -296,7 +297,107 @@ pub fn subject_remainder(subject: &str) -> String {
}
i += c.len_utf8();
}
js::trim(&s4).to_string()
finish_remainder(&s4)
}
static OF_COUNTER: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)[\[(]?\b(?:fileSP+)?([0-9]+)SP*ofSP*([0-9]+)\b[\])]?"));
static DASH_COUNTER: LazyLock<Regex> = LazyLock::new(|| re(r"\[([0-9]+)-([0-9]+)\]"));
static SLASH_COUNTER: LazyLock<Regex> = LazyLock::new(|| re(r"\b([0-9]+)/([0-9]+)\b"));
static ZERO_COUNTER: LazyLock<Regex> = LazyLock::new(|| re(r"\b(0[0-9]*)-([0-9]+)\b"));
static SIZES: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)\b[0-9]+(?:[.,][0-9]+)?SP*(?:[kmgt]i?b|bytes?)\b"));
static EMPTY_BRACKETS: LazyLock<Regex> = LazyLock::new(|| re(r" ?(?:\[ ?\]|\( ?\)) ?"));
fn number(digits: &[u8]) -> f64 {
std::str::from_utf8(digits).unwrap().parse::<f64>().unwrap()
}
/// `s.replace(/…/g, counter)`: the match goes when its first number is at most its second.
fn replace_counters(re: &Regex, s: &str) -> String {
if !re.is_match(s.as_bytes()) {
return s.to_string();
}
let out = re.replace_all(s.as_bytes(), |c: &regex::bytes::Captures| if number(&c[1]) <= number(&c[2]) { b" ".to_vec() } else { c[0].to_vec() });
String::from_utf8(out.into_owned()).unwrap()
}
/// The written-out counters, file sizes and empty brackets subjectRemainder removes after the [i/M] ones.
fn remove_counters_and_sizes(s: &str) -> String {
let s = replace_counters(&OF_COUNTER, s);
let s = replace_counters(&DASH_COUNTER, &s);
let s = replace_counters(&SLASH_COUNTER, &s);
let s = replace_counters(&ZERO_COUNTER, &s);
replace(&SIZES, &s, " ", true)
}
/// The end of subjectRemainder: what's left of brackets, double spaces, trim (after the separators became single spaces).
fn finish_remainder(s: &str) -> String {
let s = if s.contains(['[', '(']) { replace(&EMPTY_BRACKETS, s, " ", true) } else { s.to_string() };
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c == ' ' && out.ends_with(' ') {
continue;
}
out.push(c);
}
js::trim(&out).to_string()
}
/// counterTotal: the subject's file counter total, first form found first.
pub fn counter_total(subject: &str) -> Option<String> {
let s = replace(&QUOTED, subject, " ", true);
if let Some(c) = COUNTERS_CAPTURE.captures(s.as_bytes()) {
let total = c.get(2).or(c.get(4)).unwrap().as_bytes();
return Some(js_number_string(number(total)));
}
for re in [&*OF_COUNTER_OPEN, &*DASH_COUNTER, &*SLASH_COUNTER, &*ZERO_COUNTER] {
for c in re.captures_iter(s.as_bytes()) {
if number(&c[1]) <= number(&c[2]) {
return Some(js_number_string(number(&c[2])));
}
}
}
None
}
static COUNTERS_CAPTURE: LazyLock<Regex> = LazyLock::new(|| re(r"\[([0-9]+)/([0-9]+)\]|\(([0-9]+)/([0-9]+)\)"));
/// The "of" counter without its closing bracket (as counterTotal's pattern).
static OF_COUNTER_OPEN: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)[\[(]?\b(?:fileSP+)?([0-9]+)SP*ofSP*([0-9]+)\b"));
/// `String(n)` of a counter's number (integers as JS prints them).
fn js_number_string(n: f64) -> String {
if n.fract() == 0.0 && n.abs() < 1e21 {
format!("{}", n as i128)
} else {
format!("{n}")
}
}
static IMAGE: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)\.(jpe?g|png|gif|bmp|webp|tiff?)$"));
static AUDIO: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)\.(mp3|flac|m4a|m4b|ogg|opus|wav|aac|wma|ape)(?:$|[^a-z0-9])"));
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.
fn image_prefix(stem: &str) -> String {
let b = stem.as_bytes();
let cut = match b.iter().rposition(u8::is_ascii_digit) {
Some(last) => b[..=last].iter().rposition(|c| !c.is_ascii_digit()).map(|i| i + 1).unwrap_or(0),
None => b.len(),
};
let head = &stem[..cut];
let mut out = String::with_capacity(head.len());
let mut sep = false;
for c in head.chars() {
if c.is_ascii_digit() || js::is_space(c) || matches!(c, '.' | '_' | '-') {
sep = true;
} else {
if sep && !out.is_empty() {
out.push(' ');
}
out.push(c);
sep = false;
}
}
out
}
fn is_random_token(token: &str) -> bool {
@@ -412,16 +513,29 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release {
let file_total = file_counter_total(subject);
let stem = file_stem(name);
let remainder = subject_remainder(subject);
// The poster's constant tag outside the quotes: the file's stem says which upload it is.
let tag = describes(&stem) && !looks_random(&stem) && is_poster_tag(&remainder, &stem);
// A document or comic is a release of its own.
if test(&DOCUMENT, name) {
let shown = replace(&DOCUMENT, &stem, "", false);
let name = if shown.is_empty() { stem.clone() } else { shown };
return Release { key: hash(&["stem", poster, &stem]), name, source: "stem", file_total };
}
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).
let tag = !image && !test(&AUDIO, name) && describes(&stem) && !looks_random(&stem) && is_poster_tag(&remainder, &stem);
let prefix = if image { image_prefix(&stem) } else { String::new() };
if !tag && names_release(&remainder) {
return Release { key: hash(&["subject", poster, &remainder]), name: remainder, source: "subject", file_total };
let bound = counter_total(subject).unwrap_or_else(|| prefix.clone());
return Release { key: hash(&["subject", poster, &remainder, &bound]), name: remainder, source: "subject", file_total };
}
if let Some(total) = file_total.filter(|&n| n != 0 && !tag) {
if is_post_token(&remainder) {
return Release { key: hash(&["post", poster, &remainder, &total.to_string()]), name: remainder, source: "post", file_total };
}
}
if image {
let name = if prefix.is_empty() { stem.clone() } else { prefix.clone() };
return Release { key: hash(&["images", poster, &prefix]), name, source: "stem", file_total };
}
Release { key: hash(&["stem", poster, &stem]), name: stem, source: "stem", file_total }
}
@@ -801,6 +915,40 @@ mod tests {
assert!(!is_poster_tag("Audiofiles DIE ZEIT 35", "35_China_dl"));
}
#[test]
fn sets_documents_and_audio_as_in_the_typescript() {
let id = |subject: &str, name: &str| release_identity("p", subject, name);
let a = id(r#"What about this? - File 027 of 132: "DSC_027.jpg" yEnc"#, "DSC_027.jpg");
let b = id(r#"What about this? - File 028 of 132: "DSC_028.jpg" yEnc"#, "DSC_028.jpg");
let other = id(r#"What about this? - File 001 of 90: "DSC_001.jpg" yEnc"#, "DSC_001.jpg");
assert_eq!(a.key, b.key);
assert_ne!(a.key, other.key);
assert_eq!(a.name, "What about this?");
let k1 = id(r#"Proud to be a poster "Kitchen Hottie 1 set233-76-lg.jpg" yEnc"#, "Kitchen Hottie 1 set233-76-lg.jpg");
let k2 = id(r#"Proud to be a poster "Kitchen Hottie 1 set233-77-lg.jpg" yEnc"#, "Kitchen Hottie 1 set233-77-lg.jpg");
let k3 = id(r#"Proud to be a poster "Office Chair set12-01-lg.jpg" yEnc"#, "Office Chair set12-01-lg.jpg");
assert_eq!(k1.key, k2.key);
assert_ne!(k1.key, k3.key);
for (subject, expected) in [
(r#"Minutemen (001/104) "x.cbr" - 76.88 MB - yEnc"#, "Minutemen"),
(r#"Babes P A R T 3 ("Charlotte 03 16.jpg" 0097-1574 156.87 kB-273.03 MB) yEnc"#, "Babes P A R T 3"),
(r#"*MaryKate* - OfficeChair [23of61] - "OfficeChair023.jpg" yEnc 187924 bytes"#, "*MaryKate* OfficeChair"),
(r#"Adventure Comics [1948-12] - "x.rar""#, "Adventure Comics [1948 12]"),
] {
assert_eq!(subject_remainder(subject), expected, "{subject}");
assert_eq!(subject_remainder_re(subject), expected, "{subject}");
}
let c1 = id(r#"cw-horror&mystery [10/28] "Vampirella Magazine 061.cbr" yEnc"#, "Vampirella Magazine 061.cbr");
let c2 = id(r#"cw-horror&mystery [11/28] "Mystery Tales 12.cbr" yEnc"#, "Mystery Tales 12.cbr");
assert_ne!(c1.key, c2.key);
assert_eq!(c2.name, "Mystery Tales 12");
let t1 = id(r#"(Geisterjäger John Sinclair Hörspiele Vol. 2) [04/59] - "042 - 011 - Der unheimliche Bogenschütze.mp3 - by DeckaR" yEnc"#, "042 - 011 - Der unheimliche Bogenschütze.mp3 - by DeckaR");
let t2 = id(r#"(Geisterjäger John Sinclair Hörspiele Vol. 2) [34/59] - "072 - 027 - Die Teufelsuhr.MP3 - by DeckaR" yEnc"#, "072 - 027 - Die Teufelsuhr.MP3 - by DeckaR");
let par2 = id(r#"(Geisterjäger John Sinclair Hörspiele Vol. 2) [59/59] - "Hörspiele Vol. 2.par2" yEnc"#, "Hörspiele Vol. 2.par2");
assert_eq!(t1.key, t2.key);
assert_eq!(t1.key, par2.key);
}
#[test]
fn random_names() {
assert!(looks_random("prvjr7hapd0i1ny14pz4"));