From 7ad4b70775e9a20341614d3f503ff5e6c1e202af Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Tue, 13 Oct 2026 06:56:56 +0200 Subject: [PATCH] A scene music post's par2 under the release name; .par/.part before .par2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "[07/14] - "blaise_mc-in_hellstill_3-ep-web-2022-known.par2"": the tracks and the 00- files are keyed by artist and total, the par2 by its stem, so ~1,900 Meow and Cat posts were two releases. Under a counter a stem that reads as a scene music release (artist-…-web|cd|ep|single|vinyl|…-…YYYY(-group)) takes the artist key. And "goh1Phoz5ahXu.par.vol006+08.par2", "X.part.par2" beside the volumes: .par/.part before a par2 suffix goes too. Co-Authored-By: Claude Opus 5.5 --- edge/src/names.rs | 35 ++++++++++++++++++++++++++++- lib/release/identity.ts | 10 ++++++++- tests/unit/release/identity.spec.ts | 9 ++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/edge/src/names.rs b/edge/src/names.rs index 2f8cb60..c2093b2 100644 --- a/edge/src/names.rs +++ b/edge/src/names.rs @@ -30,6 +30,7 @@ static SUFFIXES: LazyLock> = LazyLock::new(|| { vec![ // A part's par2 files ("X.part1.par2", "X.part1.vol00+34.par2"). (re(r"(?i)\.part[0-9]+(?:\.vol[0-9]+[+-][0-9]+)?\.par2$"), ""), + (re(r"(?i)\.(?:part|par)(?:\.vol[0-9]+[+-][0-9]+)?\.par2$"), ""), (re(r"(?i)\.vol[0-9]+[+-][0-9]+\.par2$"), ""), (re(r"(?i)\.par2$"), ""), (re(r"(?i)\.part[0-9]+\.(?:rar|rev)$"), ""), @@ -216,6 +217,20 @@ fn strip_suffix_rule(s: &[u8], rule: usize) -> Option { let p = m.checked_sub(d + 5).filter(|_| d > 0)?; eq_ci(&s[p..p + 5], b".part").then_some(p) } + // \.(?:part|par)(?:\.vol\d+[+-]\d+)?\.par2$ + 14 => { + if !ends_ci(s, b".par2") { + return None; + } + let m = strip_suffix_rule(s, 0).unwrap_or(n - 5); + if m >= 5 && eq_ci(&s[m - 5..m], b".part") { + Some(m - 5) + } else if m >= 4 && eq_ci(&s[m - 4..m], b".par") { + Some(m - 4) + } else { + None + } + } // (?:\.\d{1,4})?\.bc$ 13 => { if !ends_ci(s, b".bc") { @@ -243,7 +258,7 @@ pub fn file_stem(name: &str) -> String { while changed { changed = false; // In the order of SUFFIXES: a part's par2 first, BCArchive volumes after the rar rules. - for rule in [12, 0, 1, 2, 3, 4, 13, 5, 6, 7, 8, 9, 10, 11] { + for rule in [12, 14, 0, 1, 2, 3, 4, 13, 5, 6, 7, 8, 9, 10, 11] { if let Some(next) = strip_suffix_rule(&b[..len], rule) { if next > 0 && next < len { len = next; @@ -815,6 +830,13 @@ pub fn release_identity(poster: &str, subject: &str, name: &str) -> Release { return Release { key: hash(&["stem", poster, &artist, &total.to_string()]), name: artist, source: "stem", file_total }; } } + // And its par2 and sfv under the release's own name ("blaise_mc-in_hellstill_3-ep-web-2022-known.par2"). + if !test(&AUDIO, name) { + if let Some(c) = SCENE_RELEASE.captures(stem.as_bytes()) { + let artist = String::from_utf8(c[1].to_vec()).unwrap(); + return Release { key: hash(&["stem", poster, &artist, &total.to_string()]), name: artist, source: "stem", file_total }; + } + } } if image { // Keyed by its set when its name has one ("diana-040-001.jpg"). @@ -870,6 +892,10 @@ static BOILERPLATE: LazyLock = /// Also one long hex run ("6c57319fbae505679737bd46a76193a1-u4e.part6.rar"). static HASHED: LazyLock = LazyLock::new(|| re(r"(?i)^(?:[0-9a-f]{8,}(?:_[0-9a-f]{6,}){2,}|[0-9a-f]{24,})(?:-[0-9a-z]+)?$")); static SCENE_TRACK: LazyLock = LazyLock::new(|| re(r"^[0-9]{1,3}[-_ .]+(.+?)(?:_-_| - |-)")); +/// SCENE_RELEASE (lib/release/identity.ts): a scene music release's name. +static SCENE_RELEASE: LazyLock = LazyLock::new(|| { + re(r"(?i)^(.+?)(?:_-_| - |-).*-(?:web|cd[a-z]?|vinyl|vls|sat|cable|fm|dab|single|ep|lp|promo|bootleg)-.*(?:19|20)[0-9]{2}(?:-[A-Za-z0-9_]+)?$") +}); static VOLUME_NUMBER: LazyLock = LazyLock::new(|| re(r"^(.*[^0-9. ])[. ]([0-9]{1,5})\.?$")); static GENERIC: LazyLock = LazyLock::new(|| { @@ -1476,6 +1502,9 @@ mod image_set_tests { ("Licence 700790900887990.z18", "Licence 700790900887990"), ("Licence 077887994400668.x12", "Licence 077887994400668"), ("Reply 987986177631730.x004", "Reply 987986177631730"), + ("goh1Phoz5ahXu.par.vol006+08.par2", "goh1Phoz5ahXu"), + ("brSWc0Q2KWblQpogOmgbn9.part.par2", "brSWc0Q2KWblQpogOmgbn9"), + ("x.parts.par2", "x.parts"), ("Some.Movie.2010.x264", "Some.Movie.2010.x264"), ("chappelle1.part02.rev", "chappelle1"), ("chappelle1.rev", "chappelle1"), @@ -1574,6 +1603,10 @@ mod image_set_tests { assert_eq!(meow("01", n).key, tracks, "{n}"); } assert_ne!(meow("01", "00-frank_van_etten-wie_heeft_jouw_lach_gestolen-single-web-nl-2022-cover.jpg").key, tracks); + let blaise = |i: &str, n: &str| release_identity("p", &format!(r#"[{i}/14] - "{n}" yEnc"#), n); + assert_eq!(blaise("07", "blaise_mc-in_hellstill_3-ep-web-2022-known.par2").key, blaise("03", "00-blaise_mc-in_hellstill_3-ep-web-2022.nfo").key); + assert_eq!(blaise("09", "blaise_mc-in_hellstill_3-ep-web-2022-known.vol000+01.par2").key, blaise("05", "01-blaise_mc-intro.mp3").key); + assert_eq!(blaise("01", "Some.Movie.2010.1080p.BluRay.x264-GRP.par2").name, "Some.Movie.2010.1080p.BluRay.x264-GRP"); } #[test] diff --git a/lib/release/identity.ts b/lib/release/identity.ts index 04662b6..487d1fa 100644 --- a/lib/release/identity.ts +++ b/lib/release/identity.ts @@ -25,8 +25,10 @@ export interface ReleaseInput { // Suffixes that differ between the files of one release. Stripped repeatedly, // so "x.mkv.vol03+04.par2" and "x.part07.rar" both reduce to "x". const SUFFIXES = [ - // A part's par2 files ("X.part1.par2", "X.part1.vol00+34.par2" beside "X.part1.rar": 683 single files in a.b.test). + // A part's par2 files ("X.part1.par2", "X.part1.vol00+34.par2" beside "X.part1.rar": 683 single files in a.b.test), and + // a par2 named after "X.part" or "X.par" ("goh1Phoz5ahXu.par.vol006+08.par2" beside "goh1Phoz5ahXu.r00", NewsUP). /\.part\d+(?:\.vol\d+[+-]\d+)?\.par2$/i, + /\.(?:part|par)(?:\.vol\d+[+-]\d+)?\.par2$/i, /\.vol\d+[+-]\d+\.par2$/i, /\.par2$/i, // Recovery volumes too ("chappelle1.part02.rev"). @@ -146,6 +148,8 @@ const BOILERPLATE = /\(?\bwww(?: [A-Za-z0-9]+)*? (?:org|com|net|info|nl|de|eu|to // 11 uploads were one release of 14,450 files; LOG.md 2026-10-12 (24)). const HASHED = /^(?:[0-9a-f]{8,}(?:_[0-9a-f]{6,}){2,}|[0-9a-f]{24,})(?:-[0-9a-z]+)?$/i const SCENE_TRACK = /^\d{1,3}[-_ .]+(.+?)(?:_-_| - |-)/ +/** A scene music release's name: the artist, then its source (web, cd, vinyl, …) and the year, last the group. */ +const SCENE_RELEASE = /^(.+?)(?:_-_| - |-).*-(?:web|cd[a-z]?|vinyl|vls|sat|cable|fm|dab|single|ep|lp|promo|bootleg)-.*(?:19|20)\d{2}(?:-[A-Za-z0-9_]+)?$/i const VOLUME_NUMBER = /^(.*[^\d. ])[. ](\d{1,5})\.?$/ /** Image files: a set's images are named one by one ("IMG_3086.jpg"); its subject names the set. */ @@ -418,6 +422,10 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null { // cover, nfo, sfv and m3u of 2,600 Meow posts were releases of their own; LOG.md 2026-10-12 (28)). const sceneIndex = postTotal && /^00[-_ .]/.test(stem) ? SCENE_TRACK.exec(stem) : null if (sceneIndex) return { key: hash('stem', poster, sceneIndex[1], postTotal), name: sceneIndex[1], fileTotal, source: 'stem' } + // And its par2 and sfv under the release's own name ("[07/14] - "blaise_mc-in_hellstill_3-ep-web-2022-known.par2"": the + // par2 of 1,900 Meow and Cat posts was a release apart; LOG.md 2026-10-13 (40)), when the name reads as a scene music release. + const sceneRelease = postTotal && !AUDIO.test(input.name) ? SCENE_RELEASE.exec(stem) : null + if (sceneRelease) return { key: hash('stem', poster, sceneRelease[1], postTotal), name: sceneRelease[1], fileTotal, source: 'stem' } // 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)). diff --git a/tests/unit/release/identity.spec.ts b/tests/unit/release/identity.spec.ts index bdd099d..8fbfa19 100644 --- a/tests/unit/release/identity.spec.ts +++ b/tests/unit/release/identity.spec.ts @@ -309,6 +309,9 @@ test.group('release / par2 names', () => { assert.equal(fileStem('Licence 700790900887990.z18'), 'Licence 700790900887990') assert.equal(fileStem('Licence 077887994400668.x12'), 'Licence 077887994400668') assert.equal(fileStem('Reply 987986177631730.x004'), 'Reply 987986177631730') + assert.equal(fileStem('goh1Phoz5ahXu.par.vol006+08.par2'), 'goh1Phoz5ahXu') + assert.equal(fileStem('brSWc0Q2KWblQpogOmgbn9.part.par2'), 'brSWc0Q2KWblQpogOmgbn9') + assert.equal(fileStem('x.parts.par2'), 'x.parts') assert.equal(fileStem('Some.Movie.2010.x264'), 'Some.Movie.2010.x264') assert.equal(runOn('lois 1258 Billy Joel - Extremes.part01.rar lois 1258 Billy Joel - Extremes '), 'lois 1258 Billy Joel - Extremes.part01.rar') assert.equal(runOn('GrooveCoverage.part26.rarGrooveCoveragebestof'), 'GrooveCoverage.part26.rar') @@ -384,6 +387,12 @@ test.group('release / par2 names', () => { assert.equal(meow('01', n).key, tracks, n) } assert.notEqual(meow('01', '00-frank_van_etten-wie_heeft_jouw_lach_gestolen-single-web-nl-2022-cover.jpg').key, tracks) + const blaise = (i: string, n: string) => id(`[${i}/14] - "${n}" yEnc`, n) + assert.equal(blaise('07', 'blaise_mc-in_hellstill_3-ep-web-2022-known.par2').key, blaise('03', '00-blaise_mc-in_hellstill_3-ep-web-2022.nfo').key) + assert.equal(blaise('09', 'blaise_mc-in_hellstill_3-ep-web-2022-known.vol000+01.par2').key, blaise('05', '01-blaise_mc-intro.mp3').key) + // A film isn't a scene music release. + assert.equal(blaise('01', 'Some.Movie.2010.1080p.BluRay.x264-GRP.par2').source, 'stem') + assert.equal(blaise('01', 'Some.Movie.2010.1080p.BluRay.x264-GRP.par2').name, 'Some.Movie.2010.1080p.BluRay.x264-GRP') }) test('a host and a sponsor without a counter name no upload', ({ assert }) => {