Auto-Rename Hardening: 9 weitere Bugs aus 10-Agent-Audit gefixt
B) Symlink-Following + Library-Cross-Risk verhindert
- collectFilesByExtensions skippt jetzt Symbolic Links / Junctions
(entry.isSymbolicLink) — der v1.7.107-Korruptions-Vektor kann nicht
mehr ueber Reparse-Points zurueckkehren
- autoRenameExtractedVideoFiles bricht ab wenn extractDir mit
mkvLibraryDir ueberlappt (in beide Richtungen) → keine Cross-
Package-Korruption durch fehlerhafte User-Konfig
- collectMkvFilesToLibrary mit gleichem Schutz fuer sourceDir<->targetDir
C) Long-Path Silent Skip behoben
- buildSafeAutoRenameTargetPath prueft jetzt zusaetzlich Gesamtpfad-
Laenge (247 chars conservative Windows-Limit), nicht nur Datei-
Namen-Laenge. Fallback zu kuerzerem Pfad greift jetzt zuverlaessig
D) Hybrid-Extract Partial-Write Race entschaerft
- Files mit mtime juenger als 2s werden uebersprungen (im naechsten
Scan re-evaluiert). Verhindert Rename auf gerade-noch-gschriebene
MKVs waehrend Hybrid-Extract parallel arbeitet
- Konfigurierbar via fileStabilizeMinAgeMs (Tests: VITEST=true => 0)
E) Retry-Logik fuer transiente Rename-Fehler
- renamePathWithExdevFallback retried jetzt EBUSY/EACCES/EPERM/EEXIST
mit 200/500/1000ms Backoff. Antivirus, Indexer, OneDrive, offene
Player-Locks → automatisch geheilt statt permanent geskippt
F) Subtitle/.nfo Companion-Files werden mit-umbenannt UND mit-verschoben
- Neue Helper renameCompanionFiles + moveCompanionFiles erkennen Subs
(.srt/.ass/.ssa/.sub/.idx/.vtt/.smi) und Metadaten (.nfo) am Basis-
Namen-Match. Auch Sprach-Tags wie .de.srt bleiben erhalten
- Mediaplayer kann Subs nach Library-Move wieder automatisch laden
G) Sample-Token False-Positive entschaerft
- Dateien die sampleTokenRe matchen bekommen Size-Check: nur als Sample
behandelt wenn ≤150 MB. Series mit "Sample" im Titel (z.B.
"Sample.Squad.S01E01.mkv") werden jetzt korrekt umbenannt
- Sample-Subfolder-Detection bleibt unveraendert (eindeutig)
H) UNC + Casing-only Rename: jetzt via renamePathWithExdevFallback
- Casing-Rename benutzt jetzt den gleichen Helper, bekommt automatisch
toWindowsLongPathIfNeeded und Retry-Logik
I) Multi-MKV in selbem Folder: numerischer Suffix statt Skip
- Wenn Ziel existiert: probiert .2, .3, ... bis .99 bevor aufgegeben.
A/B-Parts oder alternate-Audio-Files in selbem Folder werden jetzt
korrekt mit Suffix differenziert statt 2./3. File silent zu droppen
J) Episode-Token Coverage: xX-Format hinzugefuegt
- Neuer SCENE_EPISODE_X_RE erkennt 1x01, 10x100, etc. (aeltere
Scene-Releases). Quality-Tokens wie 1080p werden NICHT falsch
als 1080xX matched (kein zweiter Number-Group)
Tests:
- Symlink-Guard: extractDir==mkvLibraryDir → 0 renamed, File unangetastet
- Companion: .srt/.de.srt/.nfo bei Rename mitbenannt
- Multi-MKV-Collision: 2 Files → suffix .2 statt skip
- Episode-Token: 1x01/10x100 erkannt, 1080p nicht falsch matched
589/589 Tests gruen.
This commit is contained in:
@@ -58,6 +58,21 @@ describe("looksLikeObfuscatedSceneFileName", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractEpisodeToken (extended formats)", () => {
|
||||
it("recognizes the older xX format", () => {
|
||||
expect(extractEpisodeToken("show.1x01.720p.mkv")).toBe("S01E01");
|
||||
expect(extractEpisodeToken("Show.Name.10x100.mkv")).toBe("S10E100");
|
||||
expect(extractEpisodeToken("show-2x05-hdtv.mkv")).toBe("S02E05");
|
||||
});
|
||||
|
||||
it("does not falsely match resolution tokens like 1080x720", () => {
|
||||
// The xX regex is bounded; 1080p shouldn't match as "1080x???" because
|
||||
// there's no second number group in 1080p / 720p / etc.
|
||||
expect(extractEpisodeToken("show.1080p.mkv")).toBeNull();
|
||||
expect(extractEpisodeToken("show.S01E01.1080p.mkv")).toBe("S01E01");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractEpisodeToken", () => {
|
||||
it("extracts S01E01 from standard scene format", () => {
|
||||
expect(extractEpisodeToken("show.name.s01e01.720p")).toBe("S01E01");
|
||||
|
||||
@@ -10499,6 +10499,122 @@ describe("download manager", () => {
|
||||
expect((manager as any).packageFileOpChain.has(pkgId)).toBe(false);
|
||||
});
|
||||
|
||||
it("auto-rename refuses to scan when extractDir overlaps with mkvLibraryDir", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-overlap-"));
|
||||
tempDirs.push(root);
|
||||
const sharedDir = path.join(root, "library");
|
||||
fs.mkdirSync(path.join(sharedDir, "EpisodeFolder"), { recursive: true });
|
||||
fs.writeFileSync(path.join(sharedDir, "EpisodeFolder", "obfus.mkv"), Buffer.alloc(1024, 0));
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "out"),
|
||||
extractDir: sharedDir,
|
||||
mkvLibraryDir: sharedDir,
|
||||
autoRename4sf4sj: true
|
||||
},
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
const pkg: any = {
|
||||
id: "overlap-pkg",
|
||||
name: "Overlap.Test.S01.GERMAN.x264-aWake",
|
||||
outputDir: path.join(root, "out", "Overlap.Test"),
|
||||
extractDir: sharedDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
priority: "normal",
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
downloadStartedAt: 0,
|
||||
downloadCompletedAt: 0
|
||||
};
|
||||
|
||||
const renamed = await (manager as any).autoRenameExtractedVideoFiles(sharedDir, pkg);
|
||||
expect(renamed).toBe(0);
|
||||
// File must remain untouched — no rename performed.
|
||||
expect(fs.existsSync(path.join(sharedDir, "EpisodeFolder", "obfus.mkv"))).toBe(true);
|
||||
});
|
||||
|
||||
it("auto-rename also renames matching subtitle / .nfo companion files", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-companion-"));
|
||||
tempDirs.push(root);
|
||||
const extractDir = path.join(root, "extract");
|
||||
const epFolder = path.join(extractDir, "Test.Show.S02E05.Title.GERMAN.WS.720p.HDTV.x264-aWake");
|
||||
fs.mkdirSync(epFolder, { recursive: true });
|
||||
fs.writeFileSync(path.join(epFolder, "awa-testshow02e05hd.mkv"), Buffer.alloc(1024, 0));
|
||||
fs.writeFileSync(path.join(epFolder, "awa-testshow02e05hd.srt"), "subtitle");
|
||||
fs.writeFileSync(path.join(epFolder, "awa-testshow02e05hd.de.srt"), "german subtitle");
|
||||
fs.writeFileSync(path.join(epFolder, "awa-testshow02e05hd.nfo"), "info");
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "out"), extractDir, autoRename4sf4sj: true },
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
const pkg: any = {
|
||||
id: "companion-pkg",
|
||||
name: "Test.Show.S02.GERMAN.WS.720p.HDTV.x264-aWake",
|
||||
outputDir: path.join(root, "out", "Test.Show.S02.GERMAN.WS.720p.HDTV.x264-aWake"),
|
||||
extractDir,
|
||||
status: "completed", itemIds: [], cancelled: false, enabled: true, priority: "normal",
|
||||
createdAt: 0, updatedAt: 0, downloadStartedAt: 0, downloadCompletedAt: 0
|
||||
};
|
||||
|
||||
const renamed = await (manager as any).autoRenameExtractedVideoFiles(extractDir, pkg);
|
||||
expect(renamed).toBe(1);
|
||||
const expectedBase = "Test.Show.S02E05.Title.GERMAN.WS.720p.HDTV.x264-aWake";
|
||||
const files = fs.readdirSync(epFolder);
|
||||
// Video renamed.
|
||||
expect(files).toContain(`${expectedBase}.mkv`);
|
||||
expect(files).not.toContain("awa-testshow02e05hd.mkv");
|
||||
// Companions renamed alongside.
|
||||
expect(files).toContain(`${expectedBase}.srt`);
|
||||
expect(files).toContain(`${expectedBase}.de.srt`);
|
||||
expect(files).toContain(`${expectedBase}.nfo`);
|
||||
expect(files).not.toContain("awa-testshow02e05hd.srt");
|
||||
expect(files).not.toContain("awa-testshow02e05hd.de.srt");
|
||||
expect(files).not.toContain("awa-testshow02e05hd.nfo");
|
||||
});
|
||||
|
||||
it("auto-rename appends a numeric suffix when target already exists (no silent skip)", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-collision-"));
|
||||
tempDirs.push(root);
|
||||
const extractDir = path.join(root, "extract");
|
||||
const epFolder = path.join(extractDir, "Test.Show.S02E05.Title.GERMAN.WS.720p.HDTV.x264-aWake");
|
||||
fs.mkdirSync(epFolder, { recursive: true });
|
||||
fs.writeFileSync(path.join(epFolder, "awa-testshow02e05hd.mkv"), Buffer.alloc(1024, 0));
|
||||
fs.writeFileSync(path.join(epFolder, "awa-testshow02e05hd.alt.mkv"), Buffer.alloc(2048, 0));
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "out"), extractDir, autoRename4sf4sj: true },
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
const pkg: any = {
|
||||
id: "collision-pkg",
|
||||
name: "Test.Show.S02.GERMAN.WS.720p.HDTV.x264-aWake",
|
||||
outputDir: path.join(root, "out", "Test.Show.S02.GERMAN.WS.720p.HDTV.x264-aWake"),
|
||||
extractDir,
|
||||
status: "completed", itemIds: [], cancelled: false, enabled: true, priority: "normal",
|
||||
createdAt: 0, updatedAt: 0, downloadStartedAt: 0, downloadCompletedAt: 0
|
||||
};
|
||||
|
||||
const renamed = await (manager as any).autoRenameExtractedVideoFiles(extractDir, pkg);
|
||||
expect(renamed).toBe(2);
|
||||
const expectedBase = "Test.Show.S02E05.Title.GERMAN.WS.720p.HDTV.x264-aWake";
|
||||
const files = fs.readdirSync(epFolder).sort();
|
||||
// First file got the canonical name; second got a numeric suffix.
|
||||
expect(files).toContain(`${expectedBase}.mkv`);
|
||||
expect(files).toContain(`${expectedBase}.2.mkv`);
|
||||
expect(files).not.toContain("awa-testshow02e05hd.mkv");
|
||||
expect(files).not.toContain("awa-testshow02e05hd.alt.mkv");
|
||||
});
|
||||
|
||||
it("chainPackageFileOp recovers from a failed op so subsequent ops still run", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-chain-recover-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Reference in New Issue
Block a user