Rebuild: image sets, documents and audio albums as releases
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:
1 parent
59481037fb
commit
14758a5b7a
4 files changed
+272
-15
No files matched your search
@@ -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("ED, 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: ®ex::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("ED, 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"));
|
||||
|
||||
Reference in new issue
Block a user