Image sets: suffixes that don't count the images go first
The set bound cut at an image name's last number, but in many threads
that's a suffix, so every image was a set ("It does my heart good to see
Red Dog dancing again": 4,683 releases of 4,730 files): a repeat of the
index (katelg010_10), a year (hgrscans1097_2009), a size
(pj02kristina_24of40_1600), an "of" total (24of40). imageSet drops
those, then cuts at the last number (cutie-071-032 stays cutie-071-).
Both implementations; tests; fixtures identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c4f48a6d4c
commit
392ff09910
3 files changed
+93
-7
No files matched your search
+55
-6
@@ -414,18 +414,48 @@ static AUDIO: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)\.(mp3|flac|m4a|m4b|og
|
||||
static DOCUMENT: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)\.(pdf|epub|mobi|azw3?|djvu|cbr|cbz|cb7|chm|lit|docx?)$"));
|
||||
|
||||
/// An image set's name without a counter: up to the last number, without numbers.
|
||||
/// `/\d/.test(stem) ? stem.replace(/\d+\D*$/, '') : ''`: an image's set, digits kept.
|
||||
fn image_set(stem: &str) -> String {
|
||||
let b = stem.as_bytes();
|
||||
match b.iter().rposition(u8::is_ascii_digit) {
|
||||
/// imageSet (lib/release/identity.ts): the name up to the number that counts the images, digits kept.
|
||||
pub fn image_set(stem: &str) -> String {
|
||||
// .replace(/(\d+)\s*of\s*\d+/gi, '$1')
|
||||
let mut s = OF_TOTAL.replace_all(stem.as_bytes(), &b"$1"[..]).into_owned();
|
||||
loop {
|
||||
// /[_-](\d+)$/
|
||||
let digits = s.iter().rev().take_while(|c| c.is_ascii_digit()).count();
|
||||
if digits == 0 || s.len() <= digits || !matches!(s[s.len() - digits - 1], b'_' | b'-') {
|
||||
break;
|
||||
}
|
||||
let n = &s[s.len() - digits..];
|
||||
let before = &s[..s.len() - digits - 1];
|
||||
// /(\d+)\D*$/: the last digit run before
|
||||
let Some(last) = before.iter().rposition(u8::is_ascii_digit) else { break };
|
||||
let start = before[..=last].iter().rposition(|c| !c.is_ascii_digit()).map_or(0, |i| i + 1);
|
||||
let previous = &before[start..=last];
|
||||
let same = trim_zeros(n) == trim_zeros(previous);
|
||||
let year = n.len() == 4 && (n.starts_with(b"19") || n.starts_with(b"20"));
|
||||
if same || year || n.len() >= 4 {
|
||||
let cut = before.len();
|
||||
s.truncate(cut);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match s.iter().rposition(u8::is_ascii_digit) {
|
||||
Some(last) => {
|
||||
let start = b[..=last].iter().rposition(|c| !c.is_ascii_digit()).map_or(0, |i| i + 1);
|
||||
stem[..start].to_string()
|
||||
let start = s[..=last].iter().rposition(|c| !c.is_ascii_digit()).map_or(0, |i| i + 1);
|
||||
String::from_utf8_lossy(&s[..start]).into_owned()
|
||||
}
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Number(digits) equality for digit strings: without leading zeros (all zeros: "0").
|
||||
fn trim_zeros(d: &[u8]) -> &[u8] {
|
||||
let t = d.iter().position(|&c| c != b'0').map_or(&d[d.len().saturating_sub(1)..], |i| &d[i..]);
|
||||
t
|
||||
}
|
||||
|
||||
static OF_TOTAL: LazyLock<Regex> = LazyLock::new(|| re(r"(?i)([0-9]+)SP*ofSP*[0-9]+"));
|
||||
|
||||
fn image_prefix(stem: &str) -> String {
|
||||
let b = stem.as_bytes();
|
||||
let cut = match b.iter().rposition(u8::is_ascii_digit) {
|
||||
@@ -1130,6 +1160,25 @@ mod image_set_tests {
|
||||
assert!(!random_poster("w.merklein@freenet.de (muffin)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_sets_without_suffixes() {
|
||||
use super::image_set;
|
||||
for (stem, want) in [
|
||||
("stp250x001", "stp250x"),
|
||||
("cutie-071-032", "cutie-071-"),
|
||||
("katelg010_10", "katelg"),
|
||||
("kate112lg035_35", "kate112lg"),
|
||||
("!!hgrscans1097_2009", "!!hgrscans"),
|
||||
("pj02kristina_24of40_1600", "pj02kristina_"),
|
||||
("keira_s04_l keira_s04_0040_l", "keira_s04_l keira_s04_"),
|
||||
("IMG_2676", "IMG_"),
|
||||
("2877-041", "2877-"),
|
||||
("JimAndJoey Kindly Husband-to-Be", ""),
|
||||
] {
|
||||
assert_eq!(image_set(stem), want, "{stem}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_sets_by_their_numbered_name() {
|
||||
let silver = |n: &str| release_identity("p", &format!(r#"The excellent STIRILING SILVER - Always popular - "{n}" yEnc 320794 bytes"#), n);
|
||||
|
||||
+22
-1
@@ -141,6 +141,27 @@ const DOCUMENT = /\.(pdf|epub|mobi|azw3?|djvu|cbr|cbz|cb7|chm|lit|docx?)$/i
|
||||
/** Dates in names: 2011-08-29, 08-29-2011 or 29.08.2011, 02-2011. */
|
||||
const DATES = /\b(?:(?:19|20)\d{2}[-._ ](?:0?[1-9]|1[0-2])[-._ ]\d{1,2}|\d{1,2}[-._ ]\d{1,2}[-._ ](?:19|20)\d{2}|\d{1,2}[-._](?:19|20)\d{2})\b/g
|
||||
|
||||
/**
|
||||
* An image's set: its name up to the number that counts the images, digits before it kept ("stp250x001" →
|
||||
* "stp250x", "cutie-071-032" → "cutie-071-"); empty for a name without numbers. Suffixes after it that don't
|
||||
* count the images go first: a repeat of the number before (katelg010_10), a year (hgrscans1097_2009), a
|
||||
* size of 4+ digits (…_24of40_1600), and an "of" total (24of40) (LOG.md 2026-10-12 (3)).
|
||||
*/
|
||||
export function imageSet(stem: string): string {
|
||||
let s = stem.replace(/(\d+)\s*of\s*\d+/gi, '$1')
|
||||
while (true) {
|
||||
const m = /[_-](\d+)$/.exec(s)
|
||||
if (!m) break
|
||||
const before = s.slice(0, m.index)
|
||||
const previous = /(\d+)\D*$/.exec(before)
|
||||
if (!previous) break
|
||||
const n = m[1]
|
||||
if (Number(n) === Number(previous[1]) || /^(?:19|20)\d\d$/.test(n) || n.length >= 4) s = before
|
||||
else break
|
||||
}
|
||||
return /\d/.test(s) ? s.replace(/\d+\D*$/, '') : ''
|
||||
}
|
||||
|
||||
export function counterTotal(subject: string): number | undefined {
|
||||
const s = subject.replace(/"[^"]*"/g, ' ')
|
||||
const plain = /\[(\d+)\/(\d+)\]|\((\d+)\/(\d+)\)/.exec(s)
|
||||
@@ -257,7 +278,7 @@ export function releaseIdentity(input: ReleaseInput): ReleaseIdentity | null {
|
||||
// Without a counter an image's set bounds it: its name up to its last number, digits kept ("stp250x001.jpg"
|
||||
// → "stp250x": STIRILING SILVER's 300 sets were one release of 44k images); a name without numbers names no
|
||||
// set ("Kindly Husband-to-Be.JPG": a thread of 1,177 releases), so the subject alone (LOG.md 2026-10-12 (2)).
|
||||
const set = image && /\d/.test(stem) ? stem.replace(/\d+\D*$/, '') : ''
|
||||
const set = image ? imageSet(stem) : ''
|
||||
return {
|
||||
key: hash('subject', poster, remainder, total ?? set),
|
||||
name: remainder,
|
||||
|
||||
@@ -222,6 +222,22 @@ test.group('release / par2 names', () => {
|
||||
assert.equal(mint('JimAndJoey Kindly Husband-to-Be.JPG').key, mint('Oops OnlyOneMore.JPG').key)
|
||||
})
|
||||
|
||||
test("an image's set: suffixes that don't count the images go first", async ({ assert }) => {
|
||||
const { imageSet } = await import('#lib/release/identity')
|
||||
for (const [stem, set] of [
|
||||
['stp250x001', 'stp250x'],
|
||||
['cutie-071-032', 'cutie-071-'],
|
||||
['katelg010_10', 'katelg'],
|
||||
['kate112lg035_35', 'kate112lg'],
|
||||
['!!hgrscans1097_2009', '!!hgrscans'],
|
||||
['pj02kristina_24of40_1600', 'pj02kristina_'],
|
||||
['keira_s04_l keira_s04_0040_l', 'keira_s04_l keira_s04_'],
|
||||
['IMG_2676', 'IMG_'],
|
||||
['2877-041', '2877-'],
|
||||
['JimAndJoey Kindly Husband-to-Be', ''],
|
||||
]) assert.equal(imageSet(stem), set, stem)
|
||||
})
|
||||
|
||||
test('dates are no file counters', async ({ assert }) => {
|
||||
const { counterTotal } = await import('#lib/release/identity')
|
||||
assert.isUndefined(counterTotal('Avira AntiVir Rescue System 08-29-2011.rar'))
|
||||
|
||||
Reference in new issue
Block a user