diff --git a/edge/src/names.rs b/edge/src/names.rs index 83f84ba..f832e3c 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -657,10 +657,17 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { let named = replace(&SIZE_AFTER_COUNTER, &named, "$1", true); // A per-file hex id in brackets ("… [1f1793f68d5321] [newzNZB] [01/22]"). let named = replace(&HEX_ID, &named, " ", true); - let remainder = subject_remainder(&named); // Counted without the file name: "shyla-shy-09-0195.jpg hardcore …" is no "9 of 195". let total = counter_total(&named); let file_total = total.as_ref().and_then(|t| t.parse::().ok()); + // Without a counter a host and a sponsor name no upload ("(www.Thunder-News.org) >XXX< "). + let remainder = subject_remainder(&named); + let remainder = if total.is_none() { + let bare = replace(&BOILERPLATE, &remainder, " ", true); + js::trim(&js::collapse_spaces(&bare)).to_string() + } else { + remainder + }; // A document or comic is a release of its own. if test(&DOCUMENT, name) { let shown = replace(&DOCUMENT, &stem, "", false); @@ -749,6 +756,9 @@ static HEX_ID: LazyLock = LazyLock::new(|| re(r"(?i)\[[0-9a-f]{8,}\]")); static VIDEO: LazyLock = LazyLock::new(|| re(r"(?i)\.(mkv|mp4|avi|m4v|ts|wmv|mov)$")); static EPISODE: LazyLock = LazyLock::new(|| re(r"(?i)^(.+?)[ ._-]+S([0-9]{1,2})E[0-9]{1,3}")); static SIZE_AFTER_COUNTER: LazyLock = LazyLock::new(|| re(r"(\[[0-9]+/[0-9]+\])SP*-SP*\[[0-9]+\]")); +/// A host and a sponsor in a subject's remainder (BOILERPLATE in lib/release/identity.ts). +static BOILERPLATE: LazyLock = + LazyLock::new(|| re(r"(?i)\(?\bwww(?: [A-Za-z0-9]+)*? (?:org|com|net|info|nl|de|eu|to|biz|us)\b\)?|?")); static HASHED: LazyLock = LazyLock::new(|| re(r"(?i)^[0-9a-f]{8,}(?:_[0-9a-f]{6,}){2,}(?:-[0-9a-z]+)?$")); static SCENE_TRACK: LazyLock = LazyLock::new(|| re(r"^[0-9]{1,3}[-_ .]+(.+?)(?:_-_| - |-)")); static VOLUME_NUMBER: LazyLock = LazyLock::new(|| re(r"^(.*[^0-9. ])[. ]([0-9]{1,5})\.?$")); @@ -1369,6 +1379,20 @@ mod image_set_tests { assert_eq!(wl("01 Time Machine.mp3").key, wl("02 Jimmy Dean Loved Marilyn.mp3").key); } + #[test] + fn host_and_sponsor_without_counter() { + let tn = |tag: &str, n: &str| release_identity("p", &format!(r#"(www.Thunder-News.org) >{tag}< - "{n}" yEnc"#), n); + assert_eq!(tn("XXX", "ps-itb10b.r00").key, tn("XXX", "ps-itb10b.par2").key); + assert_ne!(tn("XXX", "ps-itb10b.r00").key, tn("XXX", "swe6-legsa.r00").key); + assert_eq!(tn("XXX", "ps-itb10b.r00").source, "stem"); + let named = tn("For.Rent.XXX.720p.Bluray.x264-DPXXXHD", "dp-for.rent.720p.x264.r00"); + assert_eq!(named.source, "subject"); + assert_eq!(named.key, tn("For.Rent.XXX.720p.Bluray.x264-DPXXXHD", "dp-for.rent.720p.x264.par2").key); + assert_eq!(named.name, ">For Rent XXX 720p Bluray x264 DPXXXHD<"); + let counted = release_identity("p", r#"(www.Thunder-News.org) >XXX< [01/20] - "ps-itb10b.r00" yEnc"#, "ps-itb10b.r00"); + assert_eq!(counted.source, "subject"); + } + #[test] fn hashed_stems_volume_numbers_and_scene_tracks() { let u4e = |n: &str| release_identity("p", &format!(r#"Seit 1753 Tagen - usenet4ever.info - "{n}" yEnc"#), n); diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 4fc60fa..c32f016 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -131,6 +131,8 @@ function hash(...parts: (string | number | undefined)[]) { const DATE_LED = /^\d{1,2}[-._]\d{1,2}[-._](?:19|20)\d{2}(?!\d)/ const VIDEO = /\.(mkv|mp4|avi|m4v|ts|wmv|mov)$/i const EPISODE = /^(.+?)[ ._-]+S(\d{1,2})E\d{1,3}/i +/** A host and a sponsor in a subject's remainder (its dots and dashes spaces): "(www Thunder News org)", "". */ +const BOILERPLATE = /\(?\bwww(?: [A-Za-z0-9]+)*? (?:org|com|net|info|nl|de|eu|to|biz|us)\b\)?|?/gi const HASHED = /^[0-9a-f]{8,}(?:_[0-9a-f]{6,}){2,}(?:-[0-9a-z]+)?$/i const SCENE_TRACK = /^\d{1,3}[-_ .]+(.+?)(?:_-_| - |-)/ const VOLUME_NUMBER = /^(.*[^\d. ])[. ](\d{1,5})\.?$/ @@ -307,10 +309,15 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { .replace(/(\[\d+\/\d+\])\s*-\s*\[\d+\]/g, '$1') // A per-file hex id in brackets ("[PRiVATE] Rosewood.S02E08… [1f1793f68d5321] [newzNZB] [01/22]": 32,101 single files). .replace(/\[[0-9a-f]{8,}\]/gi, ' ') - const remainder = subjectRemainder(named) // Counted without the file name, as quoted names are: "shyla-shy-09-0195.jpg hardcore …" is no "9 of 195" // (each of 866 images was a release of its own; LOG.md 2026-10-12 (5)). const fileTotal = counterTotal(named) + // Without a counter a host and a sponsor name no upload ("(www.Thunder-News.org) >XXX< - + // "ps-itb10b.r00"": with no total to bound them, a poster's uploads were one release of 1,135 files; LOG.md 2026-10-12 (23)). + const remainder = + fileTotal === undefined + ? subjectRemainder(named).replace(BOILERPLATE, ' ').replace(/ {2,}/g, ' ').trim() + : subjectRemainder(named) // A document or comic is a release of its own (collections post hundreds under one subject). if (DOCUMENT.test(input.name)) return { key: hash('stem', poster, stem), name: stem.replace(DOCUMENT, '') || stem, fileTotal, source: 'stem' } const image = IMAGE.test(input.name) diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index eb01ece..f603a5a 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -339,6 +339,20 @@ test.group('release / par2 names', () => { assert.equal(track('01', '01-2bm_mq-real_rap_(intro).mp3').name, '2bm_mq') }) + test('a host and a sponsor without a counter name no upload', ({ assert }) => { + const id = (subject: string, name: string) => releaseIdentity({ poster: 'p', subject, name, weak: false })! + const tn = (tag: string, n: string) => id(`(www.Thunder-News.org) >${tag}< - "${n}" yEnc`, n) + assert.equal(tn('XXX', 'ps-itb10b.r00').key, tn('XXX', 'ps-itb10b.par2').key) + assert.notEqual(tn('XXX', 'ps-itb10b.r00').key, tn('XXX', 'swe6-legsa.r00').key) + assert.equal(tn('XXX', 'ps-itb10b.r00').source, 'stem') + const named = tn('For.Rent.XXX.720p.Bluray.x264-DPXXXHD', 'dp-for.rent.720p.x264.r00') + assert.equal(named.source, 'subject') + assert.equal(named.key, tn('For.Rent.XXX.720p.Bluray.x264-DPXXXHD', 'dp-for.rent.720p.x264.par2').key) + assert.equal(named.name, '>For Rent XXX 720p Bluray x264 DPXXXHD<') + // With a counter the subject stays as it was: the total bounds it. + assert.equal(id('(www.Thunder-News.org) >XXX< [01/20] - "ps-itb10b.r00" yEnc', 'ps-itb10b.r00').source, 'subject') + }) + 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)