Auto-Rename Race-Fix: parallele Scans pro Package serialisieren
Im Production-Log: 21:30:33.957Z Auto-Rename Scan gestartet | videoFiles=25 21:30:33.992Z Auto-Rename Scan gestartet | videoFiles=25 (35ms spaeter, gleiches pkg!) 21:30:33.994Z Auto-Rename durchgefuehrt | E24.B (Scan 1) 21:30:34.009Z Auto-Rename uebersprungen: Ziel existiert (Scan 2 sieht renamed file) 21:30:34.029Z Auto-Rename durchgefuehrt | E24.A (Scan 1) 21:30:34.056Z Auto-Rename fehlgeschlagen | ENOENT (Scan 2 versucht renamed file) Ursache: hybrid-extract feuert nach JEDEM erfolgreichen Archive einen fire-and-forget autoRename (Z.10915), und der deferred Post-Process-Pfad ruft am Ende nochmal autoRename auf (Z.11630). Bei einem Multi-Archive- Package (25 Episoden) ueberlappen sich 2+ Scans auf demselben Fileset. Ergebnis: "Ziel existiert"-Warnungen + ENOENT-Fehler beim Rename. Manchmal blieben einzelne Files unbenannt durchrutschen (Scan 2 sieht File X, will renamen, aber Scan 1 hat es schon weg-renamed). Fix: pro Package via Promise-Chaining serialisieren. Neue Map autoRenameInFlight haelt das laufende Scan-Promise pro packageId. Der neue Wrapper kettet jeden weiteren Aufruf an das vorherige Promise an — so laeuft maximal ein Scan zur Zeit pro Package, der naechste startet erst wenn der vorherige fertig ist (und sieht damit den korrekten Disk-State). Test: zwei parallele autoRenameExtractedVideoFiles-Aufrufe fuer dasselbe Package mit 3 obfuskierten Files. Beide resolven sauber, Summe der Renames == 3, alle 3 Folders enthalten am Ende den korrekten Folder- Namen statt Hoster-Obfuskation. 582/582 Tests gruen.
This commit is contained in:
@@ -10375,4 +10375,84 @@ describe("download manager", () => {
|
||||
const pkgLogFiles = fs.readdirSync(packageLogsDir).filter((f) => f.startsWith("package_") && f.endsWith(".txt"));
|
||||
expect(pkgLogFiles.length).toBe(60);
|
||||
});
|
||||
|
||||
it("serializes parallel auto-rename invocations for the same package (no Ziel existiert / ENOENT race)", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-rename-race-"));
|
||||
tempDirs.push(root);
|
||||
const stateDir = path.join(root, "state");
|
||||
fs.mkdirSync(stateDir, { recursive: true });
|
||||
initPackageLogs(stateDir);
|
||||
initItemLogs(stateDir);
|
||||
initRenameLog(stateDir);
|
||||
|
||||
// Build extract tree with 3 episode-folders, each containing 1 obfuscated MKV
|
||||
// mirroring the scene release pattern from the production log.
|
||||
const extractDir = path.join(root, "extracted");
|
||||
const episodes = [
|
||||
{ folder: "Test.Show.S02E01.Pilot.GERMAN.WS.720p.HDTV.x264-aWake", file: "awa-testshow02e01hd.mkv" },
|
||||
{ folder: "Test.Show.S02E02.Second.GERMAN.WS.720p.HDTV.x264-aWake", file: "awa-testshow02e02hd.mkv" },
|
||||
{ folder: "Test.Show.S02E03.Third.GERMAN.WS.720p.HDTV.x264-aWake", file: "awa-testshow02e03hd.mkv" }
|
||||
];
|
||||
for (const ep of episodes) {
|
||||
const dir = path.join(extractDir, ep.folder);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, ep.file), Buffer.alloc(1024, 0));
|
||||
}
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir,
|
||||
autoExtract: false,
|
||||
autoReconnect: false,
|
||||
autoRename4sf4sj: true
|
||||
},
|
||||
emptySession(),
|
||||
createStoragePaths(stateDir)
|
||||
);
|
||||
|
||||
const pkg: any = {
|
||||
id: "race-pkg-1",
|
||||
name: "Test.Show.S02.GERMAN.WS.720p.HDTV.x264-aWake",
|
||||
outputDir: path.join(root, "downloads", "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
|
||||
};
|
||||
|
||||
// Fire two scans simultaneously for the SAME package — without
|
||||
// serialization, both would race on the same fileset.
|
||||
const [n1, n2] = await Promise.all([
|
||||
(manager as any).autoRenameExtractedVideoFiles(extractDir, pkg),
|
||||
(manager as any).autoRenameExtractedVideoFiles(extractDir, pkg)
|
||||
]);
|
||||
|
||||
// First scan should rename all 3 files. Second scan, having waited for
|
||||
// the first via the in-flight promise, should find them already
|
||||
// renamed (== 0 fresh renames). What matters is that BOTH calls
|
||||
// resolved cleanly (no thrown ENOENT) and the disk state is correct.
|
||||
expect(typeof n1).toBe("number");
|
||||
expect(typeof n2).toBe("number");
|
||||
expect(n1 + n2).toBe(3);
|
||||
|
||||
// All three episodes should now have the folder-derived name (the
|
||||
// obfuscated source name was overridden via the v1.7.148 logic AND
|
||||
// the rename actually succeeded for ALL of them, not just some).
|
||||
for (const ep of episodes) {
|
||||
const dir = path.join(extractDir, ep.folder);
|
||||
const files = fs.readdirSync(dir);
|
||||
const renamedFile = `${ep.folder}.mkv`;
|
||||
expect(files).toContain(renamedFile);
|
||||
expect(files).not.toContain(ep.file);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user