From 229189f35cbf9c39df2f43a0fa465180882bdbea Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Tue, 13 Oct 2026 09:03:23 +0200 Subject: [PATCH] A date alone isn't an unquoted name; an image under a post token keys by the post - "Ballers.2014.S01E01.HDTV.x264-KILLERS[ettv] 15_07_2015.rar", "Computer Bild - Ausgabe 152015 - 15_07_2015.rar": the cut after "] " or " - " left "15_07_2015.rar" for every item of a Razor1911 dump (1,335 files for 12); a date alone after the cut means the whole text is the name. - "MDG005b - (01/90) "IMG_2307.JPG"": a counter in parentheses makes no post key, so every "IMG" set of the poster was one (609 for 90); an image under a post token is keyed by it too. Co-Authored-By: Claude Opus 5.5 --- edge/src/indexer.rs | 13 ++++++++++++- edge/src/names.rs | 7 ++++++- lib/headers/subject.ts | 6 +++++- lib/release/identity.ts | 7 ++++++- tests/unit/headers/subject.spec.ts | 3 +++ tests/unit/release/identity.spec.ts | 7 +++++++ 6 files changed, 39 insertions(+), 4 deletions(-) diff --git a/edge/src/indexer.rs b/edge/src/indexer.rs index 9819d3e..e83eb1e 100644 --- a/edge/src/indexer.rs +++ b/edge/src/indexer.rs @@ -320,6 +320,14 @@ fn is_image_name(name: &str) -> bool { [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tif", ".tiff"].iter().any(|e| lower.ends_with(e)) } +/// DATE_ONLY (lib/headers/subject.ts): a file name that is a date alone ("15_07_2015.rar"). +fn date_only(name: &str) -> bool { + static DATE_ONLY: std::sync::LazyLock = std::sync::LazyLock::new(|| { + regex::Regex::new(r"^(?:[0-9]{1,2}[-_.][0-9]{1,2}[-_.](?:19|20)[0-9]{2}|(?:19|20)[0-9]{2}[-_.][0-9]{1,2}[-_.][0-9]{1,2})\.[A-Za-z0-9]{2,4}$").unwrap() + }); + DATE_ONLY.is_match(name) +} + pub fn unquoted_name(before_counter: &str) -> Option { let mut text = before_counter.trim_end_matches(js::is_space); if text.len() >= 4 && text.is_char_boundary(text.len() - 4) && text[text.len() - 4..].eq_ignore_ascii_case("yenc") { @@ -374,7 +382,8 @@ pub fn unquoted_name(before_counter: &str) -> Option { // Or after an arrow ("… -> Exclusive-tgirls_000000-lg.jpg"). let arrow = text.rfind("-> ").map_or(0, |i| i + 3); let cut = bracket.max(dash).max(arrow); - let name = js::trim(&text[cut..]); + // A date alone after the cut ("… [ettv] 15_07_2015.rar"): the whole text is the name. + let name = if date_only(js::trim(&text[cut..])) { js::trim(text) } else { js::trim(&text[cut..]) }; // An image after numbered text without a separator: its last word (" … AB: part2 0573-1002 043.jpg"). // Not an index alone ("Teen Tease Self Shooter sad1 (10).jpg"). if cut == 0 && is_image_name(name) && name.contains(' ') { @@ -978,6 +987,8 @@ mod unquoted_tests { ("pal! [Mivina high(1).wmv\" yEnc] ", Some("Mivina high(1).wmv")), ("[03/21 Geroellheimer - S03E16.part01.rar\" yEnc ", Some("Geroellheimer - S03E16.part01.rar")), (" FP -> {EXL01~P~004} -> Alexia (3) -> VF -> [0001 of 0076] -> Exclusive-tgirls_000000-lg.jpg ", Some("Exclusive-tgirls_000000-lg.jpg")), + ("Ballers.2014.S01E01.HDTV.x264-KILLERS[ettv] 15_07_2015.rar ", Some("Ballers.2014.S01E01.HDTV.x264-KILLERS[ettv] 15_07_2015.rar")), + ("Computer Bild - Ausgabe 152015 - 15_07_2015.rar ", Some("Computer Bild - Ausgabe 152015 - 15_07_2015.rar")), (" here's a little thanksgiving present: AB: part2 0573-1002 043.jpg ", Some("043.jpg")), ("xlady post You, Me And Dupree www.x2l.nl [001/106] 'You, Me And Dupree.nfo'", Some("You, Me And Dupree.nfo")), ("[01/77][LIBRATEAM_M2219009.par2] Onbeperkt downloaden met een snelheid van 1,22 MB p.s. en dat voor maar 7,95 p.m. www.libranews.nl", Some("LIBRATEAM_M2219009.par2")), diff --git a/edge/src/names.rs b/edge/src/names.rs index f8d6058..c70b21a 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -843,7 +843,9 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { // Keyed by its set when its name has one ("diana-040-001.jpg"). let name = if prefix.is_empty() { stem.clone() } else { prefix.clone() }; let set = image_set(&stem); - let key = if set.is_empty() { hash(&["stem", poster, &name]) } else { hash(&["stem", poster, &set]) }; + let set = if set.is_empty() { name.clone() } else { set }; + // Under a post token ("MDG005b - (01/90) "IMG_2307.JPG""): the post's too. + let key = if is_post_token(&remainder) { hash(&["stem", poster, &set, &remainder]) } else { hash(&["stem", poster, &set]) }; return Release { key, name, source: "stem", file_total }; } // An audiobook's or album's tracks without a name around them: by their name without its numbers. @@ -1537,6 +1539,9 @@ mod image_set_tests { lois("02", "lois 1258 Billy Joel - Extremes.part01.rar lois 1258 Billy Joel - Extremes ").key, lois("01", "lois 1258 Billy Joel - Extremes.par2 lois 1258 Billy Joel - Extremes ").key ); + let mdg = |post: &str, i: &str, n: &str| super::release_identity("p", &format!(r#"{post} - ({i}/90) "{n}" - 16,14 MB - yEnc"#), n); + assert_eq!(mdg("MDG005b", "01", "IMG_2307.JPG").key, mdg("MDG005b", "02", "IMG_2308.jpg").key); + assert_ne!(mdg("MDG005b", "01", "IMG_2307.JPG").key, mdg("MDG006a", "01", "IMG_4410.JPG").key); let milf = |i: &str, n: &str, kb: &str| super::release_identity("p", &format!("(Hot MILF getting banged hard)[{i}/70] - {n} - {kb}"), n); assert_eq!(milf("27", "2407.jpg", "28").key, milf("28", "2408.jpg", "31").key); assert_eq!(super::file_stem_re("rld-gtre.r13.002"), "rld-gtre"); diff --git a/lib/headers/subject.ts b/lib/headers/subject.ts index b0f9673..45d1faa 100644 --- a/lib/headers/subject.ts +++ b/lib/headers/subject.ts @@ -45,6 +45,8 @@ export function nameBesideQuotes(quoted: string, beforeCounter: string): string return name && EXTENSION.test(name) ? name : null } +/** A file name that is a date alone ("15_07_2015.rar"). */ +const DATE_ONLY = /^(?:\d{1,2}[-_.]\d{1,2}[-_.](?:19|20)\d{2}|(?:19|20)\d{2}[-_.]\d{1,2}[-_.]\d{1,2})\.[A-Za-z0-9]{2,4}$/ const IMAGE_NAME = /\.(jpe?g|png|gif|bmp|webp|tiff?)$/i /** A host ("www.libranews.nl", "www.x2l.nl"): its ".nl" is no extension. */ const HOST = /www\./i @@ -77,7 +79,9 @@ export function unquotedName(beforeCounter: string): string | null { // Or after an arrow ("… -> [0001 of 0076] -> Exclusive-tgirls_000000-lg.jpg"). const arrow = text.lastIndexOf('-> ') const cut = Math.max(bracket === -1 ? 0 : bracket + 2, dash === -1 ? 0 : dash + 3, arrow === -1 ? 0 : arrow + 3) - const name = text.slice(cut).trim() + // A date alone after the cut ("Ballers.2014.S01E01.HDTV.x264-KILLERS[ettv] 15_07_2015.rar", "Computer Bild - Ausgabe + // 152015 - 15_07_2015.rar": a dump's every item was "15_07_2015.rar"; LOG.md 2026-10-13 (42)): the whole text is the name. + const name = DATE_ONLY.test(text.slice(cut).trim()) ? text.trim() : text.slice(cut).trim() // An image after numbered text without a separator: its last word (" … AB: part2 0573-1002 043.jpg"; the whole // subject was the name, so each image a release). Not an index alone ("Teen Tease Self Shooter sad1 (10).jpg"). if (cut === 0 && IMAGE_NAME.test(name) && name.includes(' ')) { diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 85a27b0..73f9c9c 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -431,7 +431,12 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { // An image without words around it: its set by its name (keyed as a stem, so "x.jpg" joins "x.part01.rar"). // Keyed by its set when its name has one ("##[ "diana-040-001.jpg" (01/68)": all of a poster's 263 diana sets were // one release of 17,808 images; LOG.md 2026-10-12 (7)). - if (image) return { key: hash('stem', poster, imageSet(stem) || prefix || stem), name: prefix || stem, fileTotal, source: 'stem' } + // Under a post token ("MDG005b - (01/90) "IMG_2307.JPG"": a counter in parentheses, no post key): the post's too, else + // every set named "IMG" was one (609 images for 90; LOG.md 2026-10-13 (42)). + if (image) { + const set = imageSet(stem) || prefix || stem + return { key: isPostToken(remainder) ? hash('stem', poster, set, remainder) : hash('stem', poster, set), name: prefix || stem, fileTotal, source: 'stem' } + } // An audiobook's or album's tracks without a name around them: by their name without its numbers, as an // image set ("… Seeing A Large Cat - D11.16-22.mp3", "Candy Dulfer - Candy Store - 11 - everytime.mp3"). if (AUDIO.test(input.name)) { diff --git a/tests/unit/headers/subject.spec.ts b/tests/unit/headers/subject.spec.ts index 2258afc..1608152 100644 --- a/tests/unit/headers/subject.spec.ts +++ b/tests/unit/headers/subject.spec.ts @@ -65,6 +65,9 @@ test.group('subject / parseSubject', () => { ["pal! [Mivina high(1).wmv\" yEnc] (323/349)", 'Mivina high(1).wmv', 323, 349], ['[03/21 Geroellheimer - S03E16.part01.rar" yEnc (1/50)', 'Geroellheimer - S03E16.part01.rar', 1, 50], [' FP -> {EXL01~P~004} -> Alexia (3) -> VF -> [0001 of 0076] -> Exclusive-tgirls_000000-lg.jpg (1/1)', 'Exclusive-tgirls_000000-lg.jpg', 1, 1], + // A date alone after the cut: the whole text. + ['Ballers.2014.S01E01.HDTV.x264-KILLERS[ettv] 15_07_2015.rar (1/12)', 'Ballers.2014.S01E01.HDTV.x264-KILLERS[ettv] 15_07_2015.rar', 1, 12], + ['Computer Bild - Ausgabe 152015 - 15_07_2015.rar (1/12)', 'Computer Bild - Ausgabe 152015 - 15_07_2015.rar', 1, 12], [" here's a little thanksgiving present: AB: part2 0573-1002 043.jpg (1/1)", '043.jpg', 1, 1], ["xlady post You, Me And Dupree www.x2l.nl [001/106] 'You, Me And Dupree.nfo' (1/1)", 'You, Me And Dupree.nfo', 1, 1], ['[01/77][LIBRATEAM_M2219009.par2] Onbeperkt downloaden met een snelheid van 1,22 MB p.s. en dat voor maar 7,95 p.m. www.libranews.nl (1/1)', 'LIBRATEAM_M2219009.par2', 1, 1], diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index ca47cde..d062f9e 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -414,6 +414,13 @@ test.group('release / par2 names', () => { assert.equal(id('(www.Thunder-News.org) >XXX< [01/20] - "ps-itb10b.r00" yEnc', 'ps-itb10b.r00').source, 'subject') }) + test('an image under a post token is keyed by the post too', ({ assert }) => { + const id = (subject: string, name: string) => releaseIdentity({ poster: 'p', subject, name, weak: false })! + const mdg = (post: string, i: string, n: string) => id(`${post} - (${i}/90) "${n}" - 16,14 MB - yEnc`, n) + assert.equal(mdg('MDG005b', '01', 'IMG_2307.JPG').key, mdg('MDG005b', '02', 'IMG_2308.jpg').key) + assert.notEqual(mdg('MDG005b', '01', 'IMG_2307.JPG').key, mdg('MDG006a', '01', 'IMG_4410.JPG').key) + }) + test('an image without words around it is keyed by its set', ({ assert }) => { const id = (name: string, counter: string) => releaseIdentity({ poster: 'p', subject: `##[ "${name}" (${counter})`, name, weak: false })! assert.equal(id('diana-040-001.jpg', '01/68').key, id('diana-040-002.jpg', '02/68').key)