diff --git a/edge/src/names.rs b/edge/src/names.rs index b92dd20..44b1112 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -304,7 +304,8 @@ static OF_COUNTER: LazyLock = LazyLock::new(|| re(r"(?i)[\[(]?\b(?:fileSP static DASH_COUNTER: LazyLock = LazyLock::new(|| re(r"\[([0-9]+)-([0-9]+)\]")); static SLASH_COUNTER: LazyLock = LazyLock::new(|| re(r"\b([0-9]+)/([0-9]+)\b")); static ZERO_COUNTER: LazyLock = LazyLock::new(|| re(r"\b(0[0-9]*)-([0-9]+)\b")); -static SIZES: LazyLock = LazyLock::new(|| re(r"(?i)\b[0-9]+(?:[.,][0-9]+)?SP*(?:[kmgt]i?b|bytes?)\b")); +static SIZES: LazyLock = LazyLock::new(|| re(r"(?i)\b[0-9]+(?:[.,][0-9]+)?SP*(?:[kmgt]i?b|bytes?|byt)\b")); +static BRACKET_SIZES: LazyLock = LazyLock::new(|| re(r"(?i)\[SP*[0-9]+(?:[.,][0-9]+)?SP*[kmgt]SP*\]")); static EMPTY_BRACKETS: LazyLock = LazyLock::new(|| re(r" ?(?:\[ ?\]|\( ?\)) ?")); fn number(digits: &[u8]) -> f64 { @@ -326,7 +327,8 @@ fn remove_counters_and_sizes(s: &str) -> String { 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) + let s = replace(&SIZES, &s, " ", true); + replace(&BRACKET_SIZES, &s, " ", true) } /// The end of subjectRemainder: what's left of brackets, double spaces, trim (after the separators became single spaces). @@ -532,7 +534,9 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { }; let tag = !image && !numbered && !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) { + // 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()); return Release { key: hash(&["subject", poster, &remainder, &bound]), name: remainder, source: "subject", file_total }; } @@ -1024,4 +1028,21 @@ mod numbered_tests { assert_eq!(a.name, "ai engineer complete"); } } + +#[cfg(test)] +mod image_set_tests { + use super::{release_identity, subject_remainder, subject_remainder_re}; + + #[test] + fn sizes_in_brackets_and_one_word_sets() { + for s in [r#"Aletta Ocean - "100628-035.jpg" yEnc [347K]"#, r#"Corie - "2877-041.jpg" yEnc [290K]"#, "x - !!!blob2010-102katie009.jpg 336573 byt"] { + assert_eq!(subject_remainder(s), subject_remainder_re(s), "{s}"); + } + assert_eq!(subject_remainder(r#"Aletta Ocean - "100628-035.jpg" yEnc [347K]"#), "Aletta Ocean"); + let a = release_identity("p", r#"Corie - "2877-041.jpg" yEnc [290K]"#, "2877-041.jpg"); + let b = release_identity("p", r#"Corie - "2877-042.jpg" yEnc [301K]"#, "2877-042.jpg"); + assert_eq!(a.key, b.key); + assert_eq!(a.name, "Corie"); + } +} } diff --git a/lib/release/identity.ts b/lib/release/identity.ts index a3d8b69..74eb774 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -74,8 +74,9 @@ export function subjectRemainder(subject: string): string { .replace(/\[(\d+)-(\d+)\]/g, counter) .replace(/\b(\d+)\/(\d+)\b/g, counter) .replace(/\b(0\d*)-(\d+)\b/g, counter) - // File sizes ("76.88 MB", "335555 Bytes") differ per file. - .replace(/\b\d+(?:[.,]\d+)?\s*(?:[kmgt]i?b|bytes?)\b/gi, ' ') + // File sizes ("76.88 MB", "335555 Bytes", "336573 byt" cut off, "[347K]") differ per file. + .replace(/\b\d+(?:[.,]\d+)?\s*(?:[kmgt]i?b|bytes?|byt)\b/gi, ' ') + .replace(/\[\s*\d+(?:[.,]\d+)?\s*[kmgt]\s*\]/gi, ' ') // File sizes follow "yEnc" at the end; other numbers (years, post ids) stay. .replace(/\byEnc\b(\s+\d+)?\s*$/i, ' ') .replace(/\byEnc\b/gi, ' ') @@ -217,7 +218,8 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { // An image set without a counter: the image's name up to its last number, without numbers // ("Kitchen Hottie 1 set233-76-lg" → "Kitchen Hottie set", "bm_jessica_blb_h087" → "bm jessica blb h"). const prefix = image ? stem.replace(/\d+\D*$/, '').replace(/\d+/g, ' ').replace(/[\s._-]+/g, ' ').trim() : '' - if (!tag && namesRelease(remainder)) { + // An image set's subject may be one word, the model's name ("Corie - "2877-041.jpg" yEnc [290K]"). + if (!tag && (namesRelease(remainder) || (image && /^[A-Za-z]{3,}$/.test(remainder)))) { // 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) diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index 3613535..aa59d7a 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -175,6 +175,14 @@ test.group('release / par2 names', () => { assert.equal(par2ReleaseName(['Album.Name-2024-GRP.part01.rar', 'Album.Name-2024-GRP.part02.rar']), 'Album.Name-2024-GRP') assert.equal(par2ReleaseName(['only.one.file.2024.mkv']), 'only.one.file.2024') }) + test('image sets: sizes in brackets and one-word set names', ({ assert }) => { + const id = (subject: string, name: string) => releaseIdentity({ poster: 'p', subject, name, weak: false })! + assert.equal(id('Aletta Ocean - "100628-035.jpg" yEnc [347K]', '100628-035.jpg').key, id('Aletta Ocean - "100628-036.jpg" yEnc [298K]', '100628-036.jpg').key) + const corie = id('Corie - "2877-041.jpg" yEnc [290K]', '2877-041.jpg') + assert.equal(corie.key, id('Corie - "2877-042.jpg" yEnc [301K]', '2877-042.jpg').key) + assert.equal(corie.name, 'Corie') + }) + test('numbered items of a set stay one release under the set name (a course, not a poster tag)', ({ assert }) => { const id = (subject: string, name: string) => releaseIdentity({ poster: 'Poster49118 ', subject, name, weak: false })! const a = id('ai-engineer-complete [401/465] - "003 Defining Tools with JSON Schema.mp4" yEnc', '003 Defining Tools with JSON Schema.mp4')