Fix: Hybrid-Rename-Race — 1-2 Dateien pro Staffel blieben unbenannt
User-Report (verifiziert via Support-Bundle): pl3x-24hours.s01e07, tmsf-burnnotice-s05e11-repack, -s05e15 landeten mit Original-Scene-Namen in der Library statt umbenannt. Andere Episoden derselben Pakete (formatidentisch) wurden korrekt umbenannt → kein Format-Problem, sondern Timing-Race. Root Cause (aus Log-Timeline): 1. autoRenameExtractedVideoFilesImpl erfasste `now` EINMAL am Scan-Start. Bei Hybrid-Extraktion werden weitere Dateien WÄHREND des Scans geschrieben → deren mtime > now → negatives ageMs → der "Clock-Skew = stabil"-Zweig wertete sie faelschlich als stabil → Rename mitten im Extractor-Write → EBUSY → 200ms- Retry deferred. 2. Der MKV-Collect hatte KEINEN Frische-Skip und moved die Datei im Retry-Fenster mit Original-Namen, bevor der Rename-Retry feuerte. 3. Rename + Collect liefen als zwei separate chainPackageFileOp-Ketten → ueberlappende Hybrid-Runden konnten einen Collect zwischen Rename und Collect einer anderen Runde einschieben. Fix (3 Teile, scoped auf extractDir des Pakets — kein Shared-Library-Scan, nicht das v1.7.107-Antipattern): 1. `now` wird PRO DATEI erfasst → frisch-geschriebene Dateien korrekt als "frisch" erkannt und deferred (statt EBUSY-Rename mitten im Write). 2. collectMkvFilesToLibrary bekommt deferFreshFiles-Param: im Hybrid-Pfad werden frische Dateien (juenger als fileStabilizeMinAgeMs) uebersprungen statt unbenannt gemoved. Der finale Deferred-Pass (deferFreshFiles=false) sammelt sie nach Stabilisierung ein (Safety-Net). 3. Hybrid-Pfad: Rename (Impl-Variante, kein Self-Chain) + Collect in EINER chainPackageFileOp-Kette → atomar, kein Interleaving ueberlappender Runden. Deferred-Pfad unangetastet (dort keine concurrent Extraktion). Regressionstest: frische Datei wird im Hybrid-Collect deferred, vom finalen Pass gesammelt. 622 Tests gruen, tsc-Fehlerzahl unveraendert (9 pre-existing). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e061997ed2
commit
18eada963f
@@ -9358,6 +9358,77 @@ describe("download manager", () => {
|
||||
expect(fs.existsSync(originalExtractedPath)).toBe(false);
|
||||
}, 20000);
|
||||
|
||||
it("hybrid collect defers fresh files instead of moving them unrenamed; final pass collects them", async () => {
|
||||
// Regression: User-Report — bei Hybrid-Extraktion blieben 1-2 Dateien pro
|
||||
// Staffel unbenannt (mit Original-Scene-Namen in der Library). Ursache: eine
|
||||
// frisch extrahierte Datei wird vom Auto-Rename absichtlich deferred (noch nicht
|
||||
// stabil), aber der Collect moved sie vorher mit Original-Namen. Fix: der
|
||||
// Hybrid-Collect (deferFreshFiles=true) ueberspringt frische Dateien; der finale
|
||||
// Deferred-Pass (deferFreshFiles=false) sammelt sie nach Stabilisierung ein.
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const packageName = "Fresh.Defer.Test.S01.German.720p.BluRay.x264-GRP";
|
||||
const outputDir = path.join(root, "downloads", packageName);
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
|
||||
const mkvName = "grp-freshshow.s01e07-720p.mkv";
|
||||
const mkvPath = path.join(extractDir, mkvName);
|
||||
fs.writeFileSync(mkvPath, Buffer.alloc(4096, 7)); // mtime = jetzt → "frisch"
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: true,
|
||||
autoRename4sf4sj: false,
|
||||
collectMkvToLibrary: true,
|
||||
mkvLibraryDir,
|
||||
enableIntegrityCheck: false,
|
||||
cleanupMode: "none"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
// In Tests ist fileStabilizeMinAgeMs=0 (Frische-Erkennung aus) — fuer diesen
|
||||
// Test aktivieren, damit die gerade erstellte Datei als "frisch" gilt.
|
||||
(manager as any).fileStabilizeMinAgeMs = 30_000;
|
||||
|
||||
const libPath = path.join(mkvLibraryDir, mkvName);
|
||||
|
||||
// Hybrid-Collect (deferFreshFiles=true): frische Datei darf NICHT gemoved werden.
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId], undefined, true);
|
||||
expect(fs.existsSync(libPath)).toBe(false);
|
||||
expect(fs.existsSync(mkvPath)).toBe(true);
|
||||
|
||||
// Finaler Deferred-Pass (deferFreshFiles=false): sammelt die Datei trotzdem ein.
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId], undefined, false);
|
||||
expect(fs.existsSync(libPath)).toBe(true);
|
||||
expect(fs.existsSync(mkvPath)).toBe(false);
|
||||
|
||||
void manager;
|
||||
}, 20000);
|
||||
|
||||
it("moves direct MKV download from outputDir to library when no archive present (Mega-Debrid flow)", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Reference in New Issue
Block a user