A quoted word that isn't a file name; split parts with two digits
Test and deploy / test (push) Successful in 46s
Test and deploy / edge (push) Successful in 1m57s
Test and deploy / deploy (push) Successful in 1m41s

From the re-index's single-file releases:
- A quoted text without an extension ("My \"Official\" remaining set's
  of Inna … 025.jpg [25/74]", "… lr140x16.jpg ( file 046 of 922)
  \"yEnc\"", "\"Dr. Lust\" - … - [12/21] - Ed_DRL_VDR_LM.part09.rar") was
  the file name, the real one stayed in the subject: every file a release.
  It gives way to a file name with an extension beside it
  (nameBesideQuotes); random quoted names with none beside keep theirs.
- Split parts numbered with two digits ("Admiral.EP05.2008.720p.Rus.
  HDCLUB.mkv.21", a.b.movies.divx.russian) share their stem, as .001.
Both implementations; tests; fixtures identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-11 23:46:43 +02:00
1 parent 625ea23171
commit 2814c77225
8 files changed
+77 -9

No files matched your search

+36 -1
View File
@@ -227,7 +227,7 @@ fn describe(subject: &str, name_end: Option<usize>, counter: &Counter, from: &st
_ => None,
};
let name = match quoted {
Some(name) => name,
Some(name) => name_beside_quotes(&name, &subject[..counter.index]).unwrap_or(name),
None => unquoted_name(&subject[..counter.index])?,
};
let name = name.as_str();
@@ -273,6 +273,28 @@ fn first_track(text: &str) -> Option<usize> {
None
}
/// nameBesideQuotes (lib/headers/subject.ts): a file name elsewhere when the quoted text has no extension.
pub fn name_beside_quotes(quoted: &str, before_counter: &str) -> Option<String> {
if has_extension(quoted) {
return None;
}
// .replace(/"[^"]*"/g, ' ')
let mut text = String::with_capacity(before_counter.len());
let mut rest = before_counter;
while let Some(open) = rest.find('"') {
match rest[open + 1..].find('"') {
Some(close) => {
text.push_str(&rest[..open]);
text.push(' ');
rest = &rest[open + 1 + close + 1..];
}
None => break,
}
}
text.push_str(rest);
unquoted_name(&text).filter(|n| has_extension(n))
}
/// unquotedName (lib/headers/subject.ts): the file name of a subject without a quoted one, from the text before its counter.
pub fn unquoted_name(before_counter: &str) -> Option<String> {
let mut text = before_counter.trim_end_matches(js::is_space);
@@ -896,4 +918,17 @@ mod unquoted_tests {
}
}
}
#[cfg(test)]
mod beside_quotes_tests {
use super::name_beside_quotes;
#[test]
fn a_name_beside_a_quoted_word() {
assert_eq!(name_beside_quotes("Official", "My \"Official\" remaining set's of Inna, Please help up to #69 if you can 025.jpg [25/74] - yEnc ").as_deref(), Some("025.jpg"));
assert_eq!(name_beside_quotes("Dr. Lust", "\"Dr. Lust\" - Lily Marlene, Vanessa Del Rio, Scorpio? - yEnc - [12/21] - Ed_DRL_VDR_LM.part09.rar ").as_deref(), Some("Ed_DRL_VDR_LM.part09.rar"));
assert_eq!(name_beside_quotes("1f0554e5fec14b8b05ca", "77cd6d03d19281a775f5bdc5 [2/8] - \"1f0554e5fec14b8b05ca\" yEnc ").as_deref(), None);
assert_eq!(name_beside_quotes("x.rar", "\"x.rar\" other.zip ").as_deref(), None);
}
}
}
+10 -3
View File
@@ -36,7 +36,7 @@ static SUFFIXES: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
(re(r"(?i)\.7z(\.[0-9]{3})?$"), ""),
(re(r"(?i)\.zip(\.[0-9]{3})?$"), ""),
// JS: (?<=\.(7z|…))\.\d{3}$ — no lookbehind here, so the extension is matched and put back.
(re(r"(?i)(\.(?:7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob))\.[0-9]{3}$"), "$1"),
(re(r"(?i)(\.(?:7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob))\.[0-9]{2,3}$"), "$1"),
(re(r"(?i)\.(nfo|sfv|nzb|srr|srs|txt|jpg|png|md5|sha1|srt|sub|idx|ass|ssa|m3u|m3u8|cue|log)$"), ""),
(re(r"(?i)\.tar$"), ""),
(re(r"(?i)\.(zst|gz|xz|bz2)$"), ""),
@@ -114,6 +114,13 @@ fn strip_three_digits(s: &[u8]) -> Option<usize> {
(n >= 4 && s[n - 4] == b'.' && s[n - 3..].iter().all(u8::is_ascii_digit)).then(|| n - 4)
}
/// \.\d{2,3}$: where a split volume's number starts (its dot).
fn strip_volume_digits(s: &[u8]) -> Option<usize> {
let n = s.len();
let d = s.iter().rev().take_while(|c| c.is_ascii_digit()).count();
((2..=3).contains(&d) && n > d && s[n - d - 1] == b'.').then(|| n - d - 1)
}
const SPLIT_EXTS: [&[u8]; 14] = [b"7z", b"zip", b"rar", b"tar", b"mkv", b"mp4", b"avi", b"m4v", b"ts", b"m2ts", b"iso", b"img", b"bin", b"vob"];
const SIDE_EXTS: [&[u8]; 19] = [b"nfo", b"sfv", b"nzb", b"srr", b"srs", b"txt", b"jpg", b"png", b"md5", b"sha1", b"srt", b"sub", b"idx", b"ass", b"ssa", b"m3u", b"m3u8", b"cue", b"log"];
const PACK_EXTS: [&[u8]; 4] = [b"zst", b"gz", b"xz", b"bz2"];
@@ -164,9 +171,9 @@ fn strip_suffix_rule(s: &[u8], rule: usize) -> Option<usize> {
}
ends_ci(s, ext).then(|| n - ext.len())
}
// (?<=\.(7z|…))\.\d{3}$: only the volume number goes.
// (?<=\.(7z|…))\.\d{2,3}$: only the volume number goes.
7 => {
let m = strip_three_digits(s)?;
let m = strip_volume_digits(s)?;
strip_ext(&s[..m], &SPLIT_EXTS).map(|_| m)
}
8 => strip_ext(s, &SIDE_EXTS),
+3 -2
View File
@@ -1,4 +1,4 @@
import { fileKey, unquotedName } from '../headers/index.js'
import { fileKey, nameBesideQuotes, unquotedName } from '../headers/index.js'
import { releaseIdentity, type ReleaseIdentity } from '../release/index.js'
/** A file's details, shared by all of its segments (one object per file, not per article). */
@@ -138,7 +138,8 @@ export class SegmentScanner {
/** A file's details from its first article seen (as parseSubject and segmentFromHeader). */
private describe(subject: string, nameEnd: number, counter: Counter, from: string, xref: string, group: string): FileMeta | null {
const nameStart = nameEnd > 0 ? subject.lastIndexOf('"', nameEnd - 1) : -1
const found = nameStart !== -1 ? subject.slice(nameStart + 1, nameEnd) : unquotedName(subject.slice(0, counter.index))
const quoted = nameStart !== -1 ? subject.slice(nameStart + 1, nameEnd) : null
const found = quoted !== null ? (nameBesideQuotes(quoted, subject.slice(0, counter.index)) ?? quoted) : unquotedName(subject.slice(0, counter.index))
if (!found || counter.total < 1) return null
const name = own(found)
const base = own((subject.slice(0, counter.index) + subject.slice(counter.index + counter.length)).replace(/\s+/g, ' ').trim())
+1 -1
View File
@@ -1,3 +1,3 @@
export { parseSubject, unquotedName, type ParsedSubject } from './subject.js'
export { parseSubject, unquotedName, nameBesideQuotes, type ParsedSubject } from './subject.js'
export { fileKey, weakFileKey, counterFileKey, looksLikeFilename } from './file_key.js'
export { segmentFromHeader, type IngestSegment, type OverviewHeader, type IdentityCache } from './segment.js'
+13 -1
View File
@@ -30,6 +30,17 @@ const EXTENSION = /[^.\s]\.[A-Za-z0-9]{2,4}$/
* (01/24)`). Null when no file name is found: the subject stays obfuscated.
* Words are split on spaces only (both implementations alike).
*/
/**
* The file name of a subject whose quoted text has no extension ("My
* \"Official\" remaining set's of Inna … 025.jpg", "\"Dr. Lust\" - … - Ed_DRL_VDR_LM.part09.rar"):
* a name with one elsewhere, else null (random quoted names keep theirs).
*/
export function nameBesideQuotes(quoted: string, beforeCounter: string): string | null {
if (EXTENSION.test(quoted)) return null
const name = unquotedName(beforeCounter.replace(/"[^"]*"/g, ' '))
return name && EXTENSION.test(name) ? name : null
}
export function unquotedName(beforeCounter: string): string | null {
let text = beforeCounter.trimEnd()
if (/yenc$/i.test(text)) text = text.slice(0, -4).trimEnd()
@@ -78,7 +89,8 @@ export function parseSubject(subject: string): ParsedSubject | null {
if (match.index > nameEnd) counter = match
}
if (!counter) return null
const name = nameStart !== -1 ? subject.slice(nameStart + 1, nameEnd) : unquotedName(subject.slice(0, counter.index))
const quoted = nameStart !== -1 ? subject.slice(nameStart + 1, nameEnd) : null
const name = quoted !== null ? (nameBesideQuotes(quoted, subject.slice(0, counter.index)) ?? quoted) : unquotedName(subject.slice(0, counter.index))
if (!name) return null
const segment = Number.parseInt(counter[1], 10)
+1 -1
View File
@@ -33,7 +33,7 @@ const SUFFIXES = [
/\.7z(\.\d{3})?$/i,
/\.zip(\.\d{3})?$/i,
// Split volumes (x.mkv.001), not codec names (H.264, H.265).
/(?<=\.(7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob))\.\d{3}$/i,
/(?<=\.(7z|zip|rar|tar|mkv|mp4|avi|m4v|ts|m2ts|iso|img|bin|vob))\.\d{2,3}$/i,
/\.(nfo|sfv|nzb|srr|srs|txt|jpg|png|md5|sha1|srt|sub|idx|ass|ssa|m3u|m3u8|cue|log)$/i,
/\.tar$/i,
/\.(zst|gz|xz|bz2)$/i,
+8
View File
@@ -46,6 +46,14 @@ test.group('subject / parseSubject', () => {
assert.isNull(parseSubject('Re: Will somebody answer the question? (1/2)'))
})
test('a quoted text without an extension gives way to a file name beside it (nameBesideQuotes)', ({ assert }) => {
assert.equal(parseSubject("My \"Official\" remaining set's of Inna, Please help up to #69 if you can 025.jpg [25/74] - yEnc (1/1)")!.name, '025.jpg')
assert.equal(parseSubject('"Dr. Lust" - Lily Marlene, Vanessa Del Rio, Scorpio? - yEnc - [12/21] - Ed_DRL_VDR_LM.part09.rar (1/80)')!.name, 'Ed_DRL_VDR_LM.part09.rar')
assert.equal(parseSubject('Unknown Model - Cleaning The Hard Drive ~ lr140x16.jpg ( file 046 of 922) "yEnc" (1/1)')!.name, 'lr140x16.jpg')
// A random quoted name with no file name beside it keeps its name.
assert.equal(parseSubject('77cd6d03d19281a775f5bdc5 [2/8] - "1f0554e5fec14b8b05ca" yEnc (14/567)')!.name, '1f0554e5fec14b8b05ca')
})
test('finds the file name of subjects without a quoted one (unquotedName)', ({ assert }) => {
const cases: [string, string, number, number, number?][] = [
['DVDFab 8.2.0.5.rar (006/332)', 'DVDFab 8.2.0.5.rar', 6, 332],
+5
View File
@@ -214,6 +214,11 @@ test.group('release / par2 names', () => {
assert.isFalse(randomPoster('w.merklein@freenet.de (muffin)'))
})
test('split parts with two-digit numbers share a stem', ({ assert }) => {
assert.equal(fileStem('Admiral.EP05.2008.720p.Rus.HDCLUB.mkv.21'), fileStem('Admiral.EP05.2008.720p.Rus.HDCLUB.mkv.22'))
assert.equal(fileStem('Naslednica_po_prjamoy_DVDRip.avi.05'), 'Naslednica_po_prjamoy_DVDRip')
})
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)