From 3f0b5b8be15c1b04f60d9b8d8790342619394aa0 Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Thu, 29 Oct 2026 13:04:06 +0100 Subject: [PATCH] Subject names lose the brackets the counters left unmatched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A poster's counter without its "[" ("… Facialized 002/477] - 0177.jpg") left "… Facialized ]" as the release name (11286967). displayName (TS) and display_name (Rust) drop unmatched brackets from subject and post names only; the identity key keeps the remainder as it was, so no release splits. Co-Authored-By: Claude Opus 5.5 --- edge/src/names.rs | 53 +++++++++++++++++++++++++++-- lib/release/identity.ts | 27 +++++++++++++-- lib/release/index.ts | 2 +- tests/unit/release/identity.spec.ts | 14 +++++++- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/edge/src/names.rs b/edge/src/names.rs index 8dfe8d2..8879f6a 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -449,6 +449,43 @@ fn finish_remainder(s: &str) -> String { js::trim(&out).to_string() } +/// displayName: a subject name as shown, brackets the counters left unmatched removed ("… Facialized 002/477] - …" +/// gave "… Facialized ]"). Only the name: the key keeps the remainder. +pub fn display_name(remainder: &str) -> String { + let b = remainder.as_bytes(); + if !b.iter().any(|c| matches!(c, b'[' | b']' | b'(' | b')')) { + return remainder.to_string(); + } + let mut drop = vec![false; b.len()]; + for (open, close) in [(b'[', b']'), (b'(', b')')] { + let mut opens: Vec = Vec::new(); + for (i, &c) in b.iter().enumerate() { + if c == open { + opens.push(i); + } else if c == close && opens.pop().is_none() { + drop[i] = true; + } + } + for i in opens { + drop[i] = true; + } + } + if !drop.contains(&true) { + return remainder.to_string(); + } + let kept: Vec = b.iter().zip(&drop).filter(|(_, &d)| !d).map(|(&c, _)| c).collect(); + let kept = String::from_utf8(kept).expect("only ASCII brackets removed"); + let mut out = String::with_capacity(kept.len()); + for c in kept.chars() { + if c == ' ' && out.ends_with(' ') { + continue; + } + out.push(c); + } + let name = js::trim(&out); + if name.is_empty() { remainder.to_string() } else { name.to_string() } +} + /// counterTotal: the subject's file counter total, first form found first. pub fn counter_total(subject: &str) -> Option { let s = replace("ED, subject, " ", true); @@ -841,11 +878,11 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { if !image && test(&RANDOM_VOLUME, name) && stem.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_' || c == b'-') && looks_random(&stripped) { parts.push(&stem); } - return Release { key: hash(&parts), name: remainder, source: "subject", file_total }; + return Release { key: hash(&parts), name: display_name(&remainder), source: "subject", file_total }; } if let Some(total) = post_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 }; + return Release { key: hash(&["post", poster, &remainder, &total.to_string()]), name: display_name(&remainder), source: "post", file_total }; } } // A scene music post's index files ("00-ammar-turk_uit_de_kroeg-single-web-nl-2022-cover.jpg") under a bare counter go @@ -1339,6 +1376,18 @@ mod tests { assert_eq!(file_stem_re("Morning Lovers.wmv.058"), "Morning Lovers"); } + #[test] + fn display_names() { + let porn4you = "webmaster@hdbangvidz.com (Porn4You)"; + let a = release_identity(porn4you, "Babe with Plump Bubble Butt Fucked and Facialized 002/477] - 0178.jpg", "0178.jpg"); + let b = release_identity(porn4you, "Babe with Plump Bubble Butt Fucked and Facialized 003/477] - 0179.jpg", "0179.jpg"); + assert_eq!(a.name, "Babe with Plump Bubble Butt Fucked and Facialized"); + assert_eq!(a.key, b.key); + assert_eq!(display_name("[Group] Show (2020)"), "[Group] Show (2020)"); + assert_eq!(display_name("( Show [x] name"), "Show [x] name"); + assert_eq!(display_name("]"), "]"); + } + #[test] fn remainders() { assert_eq!(subject_remainder(r#"Some.Release [01/45] - "x.rar" yEnc (1/50) 7168"#), "Some Release"); diff --git a/lib/release/identity.ts b/lib/release/identity.ts index c7d914d..dbd82cf 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -105,6 +105,29 @@ export function subjectRemainder(subject: string): string { .trim() } +/** + * A subject name as shown: brackets the counters left unmatched removed ("… Facialized 002/477] - 0177.jpg", the + * counter's "[" missing, gave "… Facialized ]"; LOG.md 2026-10-29 (121)). Only the name: the key keeps the remainder. + */ +export function displayName(remainder: string): string { + if (!/[[\]()]/.test(remainder)) return remainder + const drop = new Set() + for (const [open, close] of [['[', ']'], ['(', ')']]) { + const opens: number[] = [] + for (let i = 0; i < remainder.length; i++) { + if (remainder[i] === open) opens.push(i) + else if (remainder[i] === close) { + if (opens.length > 0) opens.pop() + else drop.add(i) + } + } + for (const i of opens) drop.add(i) + } + if (drop.size === 0) return remainder + const name = remainder.split('').filter((_, i) => !drop.has(i)).join('').replace(/ {2,}/g, ' ').trim() + return name || remainder +} + /** * Posting tools put random per-file tokens and poster IDs in subjects (e.g. * `elys-de81bcabef5faf6a - [Nyh5Q07Y]`); only text that still reads like a @@ -448,7 +471,7 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { if (!image && RANDOM_VOLUME.test(input.name) && /^[A-Za-z0-9_-]+$/.test(stem) && looksRandom(stem.replace(/[-_]/g, ''))) parts.push(stem) return { key: hash(...parts), - name: remainder, + name: displayName(remainder), fileTotal, source: 'subject', } @@ -456,7 +479,7 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { // Not a name, but a post id (e.g. `83110629-n [10/18] - "random"`): with the // [i/M] counter it groups the files of one post whose names are random. if (!tag && postTotal && isPostToken(remainder)) { - return { key: hash('post', poster, remainder, postTotal), name: remainder, fileTotal, source: 'post' } + return { key: hash('post', poster, remainder, postTotal), name: displayName(remainder), fileTotal, source: 'post' } } // A scene music post's index files under a bare counter go with its tracks: the artist and the total ("[01/13] - // "00-ammar-turk_uit_de_kroeg-single-web-nl-2022-cover.jpg"" with "[05/13] - "01-ammar-turk_uit_de_kroeg.mp3"": the diff --git a/lib/release/index.ts b/lib/release/index.ts index a6bdae8..f6eaf78 100644 --- a/lib/release/index.ts +++ b/lib/release/index.ts @@ -1,5 +1,5 @@ /** * Grouping of files into releases (downloads), independent of AdonisJS. */ -export { releaseIdentity, par2ReleaseIdentity, par2ReleaseName, rarReleaseIdentity, isGenericName, fileStem, sizeAfterName, runOn, subjectRemainder, namesRelease, isPostToken, type ReleaseIdentity, type ReleaseInput } from './identity.js' +export { releaseIdentity, displayName, par2ReleaseIdentity, par2ReleaseName, rarReleaseIdentity, isGenericName, fileStem, sizeAfterName, runOn, subjectRemainder, namesRelease, isPostToken, type ReleaseIdentity, type ReleaseInput } from './identity.js' export { looksRandom } from './random.js' diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index e7807e2..acf9a9b 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -1,5 +1,5 @@ import { test } from '@japa/runner' -import { fileStem, releaseIdentity, runOn, sizeAfterName, subjectRemainder } from '#lib/release/index' +import { displayName, fileStem, releaseIdentity, runOn, sizeAfterName, subjectRemainder } from '#lib/release/index' import { looksLikeFilename, weakFileKey } from '#lib/headers/index' test.group('release / fileStem', () => { @@ -604,3 +604,15 @@ test.group('release / par2 names', () => { assert.equal(id('[2/6] - "x.jpg" yEnc', 'x.jpg').key, id('[1/6] - "x.part01.rar" yEnc', 'x.part01.rar').key) }) }) + +test.group('release / displayName', () => { + test('brackets the counters left unmatched leave the name, the key keeps them', ({ assert }) => { + const subject = (i: number) => `Babe with Plump Bubble Butt Fucked and Facialized ${String(i).padStart(3, '0')}/477] - 0${176 + i}.jpg` + const id = (i: number) => releaseIdentity({ poster: 'webmaster@hdbangvidz.com (Porn4You)', weak: false, subject: subject(i), name: `0${176 + i}.jpg` })! + assert.equal(id(2).name, 'Babe with Plump Bubble Butt Fucked and Facialized') + assert.equal(id(2).key, id(3).key) + assert.equal(displayName('[Group] Show (2020)'), '[Group] Show (2020)') + assert.equal(displayName('( Show [x] name'), 'Show [x] name') + assert.equal(displayName(']'), ']') + }) +})