v1.7.151 Review-Findings nachgepflegt: 3 Edge-Cases entschaerft
Independent code review fand drei echte Probleme an v1.7.151:
(a) File-stability check bei Clock-Skew rueckwaerts:
negative ageMs (mtime in der Zukunft, z.B. NTP-Korrektur, VM-Resume)
wurde von "ageMs < 2000" als "frisch" interpretiert → Datei stuck
bis Clock aufschliesst. Fix: ageMs >= 0 zusaetzlich pruefen — negativ
= "definitiv stabil".
(c) Suffix-Loop koennte Source-File als Resolved-Target waehlen:
wenn Source schon "<base>.2.mkv" heisst und das Original "<base>.mkv"
anderswo existiert, koennte die .2/.3-Loop sich selbst auswaehlen.
Fix: pathKey-Vergleich gegen sourcePath im Loop, springt weiter.
(f) xX-Format matched x264/x265/x266 Codec-Tokens:
"5x265.x265.mkv" wurde als S05E265 interpretiert.
"Movie.x264-GROUP.mkv" konnte phantome Episode triggern.
Fix: zweite Number-Group auf \d{1,2} (max 99) gecapped + negativer
Lookahead [\dx] dahinter. 3-stellige xX-Episoden (sehr selten) gehen
verloren — moderne SxxEnnn deckt das ab. Schutz gegen alle gaengigen
Codecs (x264/265/266, h264/265) und Aspect-Ratios (1920x1080).
Tests: neue assertions fuer x264/x265/aspect-ratio + 10x99 vs 10x100.
591/591 gruen.
This commit is contained in:
parent
c9d4e69bea
commit
7f7bcf8ab2
@ -1017,8 +1017,10 @@ function hasSceneGroupSuffix(fileName: string): boolean {
|
|||||||
return isValidSceneGroupSuffix(suffix);
|
return isValidSceneGroupSuffix(suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Older scene releases used "1x01" / "01x100" instead of "S01E01". */
|
/** Older scene releases used "1x01" instead of "S01E01". The episode group
|
||||||
const SCENE_EPISODE_X_RE = /(?:^|[._\-\s])(\d{1,2})x(\d{1,3})(?:x(\d{1,3}))?(?!\d)/i;
|
* is capped at 2 digits so the regex does NOT falsely match codec tokens
|
||||||
|
* like "x264" / "x265" / "x266" or aspect ratios like "1920x1080". */
|
||||||
|
const SCENE_EPISODE_X_RE = /(?:^|[._\-\s])(\d{1,2})x(\d{1,2})(?:x(\d{1,2}))?(?![\dx])/i;
|
||||||
|
|
||||||
export function extractEpisodeToken(fileName: string): string | null {
|
export function extractEpisodeToken(fileName: string): string | null {
|
||||||
const text = String(fileName || "");
|
const text = String(fileName || "");
|
||||||
@ -3979,7 +3981,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const ageMs = now - sourceStat.mtimeMs;
|
const ageMs = now - sourceStat.mtimeMs;
|
||||||
if (ageMs < FILE_STABILIZE_MIN_AGE_MS) {
|
// Negative age = mtime in the future (clock skew, NTP correction,
|
||||||
|
// VM resume after suspension). Treat as "definitely stable" so the
|
||||||
|
// file doesn't get stuck waiting for the wall clock to catch up.
|
||||||
|
if (ageMs >= 0 && ageMs < FILE_STABILIZE_MIN_AGE_MS) {
|
||||||
logger.info(`Auto-Rename: ${sourceName} uebersprungen — Datei noch frisch (${Math.floor(ageMs)}ms), wird beim naechsten Scan behandelt`);
|
logger.info(`Auto-Rename: ${sourceName} uebersprungen — Datei noch frisch (${Math.floor(ageMs)}ms), wird beim naechsten Scan behandelt`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -4290,6 +4295,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
let resolvedTarget: string | null = null;
|
let resolvedTarget: string | null = null;
|
||||||
for (let suffixN = 2; suffixN <= 99; suffixN += 1) {
|
for (let suffixN = 2; suffixN <= 99; suffixN += 1) {
|
||||||
const candidate = path.join(targetDir, `${targetBase}.${suffixN}${targetExt}`);
|
const candidate = path.join(targetDir, `${targetBase}.${suffixN}${targetExt}`);
|
||||||
|
// Defensive: never pick the source file as our resolved target.
|
||||||
|
// If sourceName is already e.g. "<base>.2.mkv", existsAsync would
|
||||||
|
// see it as "existing" and the loop would otherwise pick "<base>.3"
|
||||||
|
// — but if pathKey matches (case-insensitive), bail to next idx
|
||||||
|
// so we don't accidentally rename source-onto-itself with a
|
||||||
|
// surprising suffix.
|
||||||
|
if (pathKey(candidate) === pathKey(sourcePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!(await this.existsAsync(candidate))) {
|
if (!(await this.existsAsync(candidate))) {
|
||||||
resolvedTarget = candidate;
|
resolvedTarget = candidate;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -59,10 +59,15 @@ describe("looksLikeObfuscatedSceneFileName", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("extractEpisodeToken (extended formats)", () => {
|
describe("extractEpisodeToken (extended formats)", () => {
|
||||||
it("recognizes the older xX format", () => {
|
it("recognizes the older xX format (capped at 2 episode digits)", () => {
|
||||||
expect(extractEpisodeToken("show.1x01.720p.mkv")).toBe("S01E01");
|
expect(extractEpisodeToken("show.1x01.720p.mkv")).toBe("S01E01");
|
||||||
expect(extractEpisodeToken("Show.Name.10x100.mkv")).toBe("S10E100");
|
|
||||||
expect(extractEpisodeToken("show-2x05-hdtv.mkv")).toBe("S02E05");
|
expect(extractEpisodeToken("show-2x05-hdtv.mkv")).toBe("S02E05");
|
||||||
|
expect(extractEpisodeToken("Show.Name.10x99.mkv")).toBe("S10E99");
|
||||||
|
// 3-digit episode in xX format is intentionally NOT supported — would
|
||||||
|
// collide with codec tokens (x264/x265/x266). 3-digit episodes still
|
||||||
|
// work in the modern SxxEnnn format which has explicit S/E delimiters.
|
||||||
|
expect(extractEpisodeToken("Show.Name.10x100.mkv")).toBeNull();
|
||||||
|
expect(extractEpisodeToken("Show.Name.S10E100.mkv")).toBe("S10E100");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not falsely match resolution tokens like 1080x720", () => {
|
it("does not falsely match resolution tokens like 1080x720", () => {
|
||||||
@ -71,6 +76,20 @@ describe("extractEpisodeToken (extended formats)", () => {
|
|||||||
expect(extractEpisodeToken("show.1080p.mkv")).toBeNull();
|
expect(extractEpisodeToken("show.1080p.mkv")).toBeNull();
|
||||||
expect(extractEpisodeToken("show.S01E01.1080p.mkv")).toBe("S01E01");
|
expect(extractEpisodeToken("show.S01E01.1080p.mkv")).toBe("S01E01");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not falsely match codec tokens like x264 / x265 (caps episode digits)", () => {
|
||||||
|
// First number 5, second number capped to 2 digits → "5x265" CANNOT
|
||||||
|
// match because 265 has 3 digits. Same for x264, x266, h264, h265.
|
||||||
|
expect(extractEpisodeToken("Movie.x264-GROUP.mkv")).toBeNull();
|
||||||
|
expect(extractEpisodeToken("Movie.5x265.x265.mkv")).toBeNull();
|
||||||
|
// SxxExx still wins ahead of phantom xX matches.
|
||||||
|
expect(extractEpisodeToken("Show.S01E01.x265-GROUP.mkv")).toBe("S01E01");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not falsely match common aspect ratios like 1920x1080", () => {
|
||||||
|
// 1920 has 4 digits, first group capped at 2 → no match.
|
||||||
|
expect(extractEpisodeToken("Movie.1920x1080.mkv")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extractEpisodeToken", () => {
|
describe("extractEpisodeToken", () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user