Release v1.4.7 with ENOENT extraction recovery and lag optimizations
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
dbf1c34282
commit
3b9c4a4e88
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.7",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -113,6 +113,10 @@ function isFinishedStatus(status: DownloadStatus): boolean {
|
||||
return status === "completed" || status === "failed" || status === "cancelled";
|
||||
}
|
||||
|
||||
function isExtractedLabel(statusText: string): boolean {
|
||||
return /^entpackt\b/i.test(String(statusText || "").trim());
|
||||
}
|
||||
|
||||
function providerLabel(provider: DownloadItem["provider"]): string {
|
||||
if (provider === "realdebrid") {
|
||||
return "Real-Debrid";
|
||||
@ -169,6 +173,8 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
private speedBytesLastWindow = 0;
|
||||
|
||||
private lastPersistAt = 0;
|
||||
|
||||
private cleanupQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
private packagePostProcessQueue: Promise<void> = Promise.resolve();
|
||||
@ -1203,13 +1209,28 @@ export class DownloadManager extends EventEmitter {
|
||||
if (this.persistTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemCount = Object.keys(this.session.items).length;
|
||||
const minGapMs = this.session.running
|
||||
? itemCount >= 1500
|
||||
? 1300
|
||||
: itemCount >= 700
|
||||
? 950
|
||||
: itemCount >= 250
|
||||
? 700
|
||||
: 450
|
||||
: 250;
|
||||
const sinceLastPersist = nowMs() - this.lastPersistAt;
|
||||
const delay = Math.max(120, minGapMs - sinceLastPersist);
|
||||
|
||||
this.persistTimer = setTimeout(() => {
|
||||
this.persistTimer = null;
|
||||
this.persistNow();
|
||||
}, 250);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private persistNow(): void {
|
||||
this.lastPersistAt = nowMs();
|
||||
saveSession(this.storagePaths, this.session);
|
||||
}
|
||||
|
||||
@ -1397,7 +1418,7 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
if (this.settings.autoExtract && failed === 0 && success > 0) {
|
||||
const needsPostProcess = pkg.status !== "completed"
|
||||
|| items.some((item) => item.status === "completed" && item.fullStatus !== "Entpackt");
|
||||
|| items.some((item) => item.status === "completed" && !isExtractedLabel(item.fullStatus));
|
||||
if (needsPostProcess) {
|
||||
void this.runPackagePostProcessing(packageId);
|
||||
} else if (pkg.status !== "completed") {
|
||||
@ -1475,7 +1496,9 @@ export class DownloadManager extends EventEmitter {
|
||||
break;
|
||||
}
|
||||
|
||||
await sleep(120);
|
||||
const maxParallel = Math.max(1, this.settings.maxParallel);
|
||||
const schedulerSleepMs = this.activeTasks.size >= maxParallel ? 170 : 120;
|
||||
await sleep(schedulerSleepMs);
|
||||
}
|
||||
} finally {
|
||||
this.scheduleRunning = false;
|
||||
@ -2381,7 +2404,7 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
|
||||
const completedItems = items.filter((item) => item.status === "completed");
|
||||
const alreadyMarkedExtracted = completedItems.length > 0 && completedItems.every((item) => item.fullStatus === "Entpackt");
|
||||
const alreadyMarkedExtracted = completedItems.length > 0 && completedItems.every((item) => isExtractedLabel(item.fullStatus));
|
||||
|
||||
if (this.settings.autoExtract && failed === 0 && success > 0 && !alreadyMarkedExtracted) {
|
||||
pkg.status = "extracting";
|
||||
@ -2436,11 +2459,22 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
pkg.status = "failed";
|
||||
} else {
|
||||
if (result.extracted > 0) {
|
||||
for (const entry of completedItems) {
|
||||
entry.fullStatus = "Entpackt";
|
||||
entry.updatedAt = nowMs();
|
||||
}
|
||||
const hasExtractedOutput = this.directoryHasAnyFiles(pkg.extractDir);
|
||||
const sourceExists = fs.existsSync(pkg.outputDir);
|
||||
let finalStatusText = "";
|
||||
|
||||
if (result.extracted > 0 || hasExtractedOutput) {
|
||||
finalStatusText = "Entpackt";
|
||||
} else if (!sourceExists) {
|
||||
finalStatusText = "Entpackt (Quelle fehlt)";
|
||||
logger.warn(`Post-Processing ohne Quellordner: pkg=${pkg.name}, outputDir fehlt`);
|
||||
} else {
|
||||
finalStatusText = "Entpackt (keine Archive)";
|
||||
}
|
||||
|
||||
for (const entry of completedItems) {
|
||||
entry.fullStatus = finalStatusText;
|
||||
entry.updatedAt = nowMs();
|
||||
}
|
||||
pkg.status = "completed";
|
||||
}
|
||||
|
||||
@ -42,9 +42,18 @@ type ExtractResumeState = {
|
||||
};
|
||||
|
||||
function findArchiveCandidates(packageDir: string): string[] {
|
||||
const files = fs.readdirSync(packageDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => path.join(packageDir, entry.name));
|
||||
if (!packageDir || !fs.existsSync(packageDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let files: string[] = [];
|
||||
try {
|
||||
files = fs.readdirSync(packageDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => path.join(packageDir, entry.name));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const preferred = files.filter((file) => /\.part0*1\.rar$/i.test(file));
|
||||
const zip = files.filter((file) => /\.zip$/i.test(file));
|
||||
@ -408,56 +417,6 @@ function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function captureDirFingerprint(rootDir: string): Map<string, string> {
|
||||
const fingerprint = new Map<string, string>();
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
const stack = [rootDir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop() as string;
|
||||
let entries: fs.Dirent[] = [];
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const stat = fs.statSync(full);
|
||||
const relative = path.relative(rootDir, full).toLowerCase();
|
||||
fingerprint.set(relative, `${stat.size}:${stat.mtimeMs}`);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
function hasDirChanges(before: Map<string, string>, after: Map<string, string>): boolean {
|
||||
if (after.size > before.size) {
|
||||
return true;
|
||||
}
|
||||
for (const [relative, meta] of after.entries()) {
|
||||
if (before.get(relative) !== meta) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function collectArchiveCleanupTargets(sourceArchivePath: string): string[] {
|
||||
const targets = new Set<string>([sourceArchivePath]);
|
||||
const dir = path.dirname(sourceArchivePath);
|
||||
@ -619,7 +578,6 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
|
||||
const conflictMode = effectiveConflictMode(options.conflictMode);
|
||||
const passwordCandidates = archivePasswords(options.passwordList || "");
|
||||
const beforeFingerprint = captureDirFingerprint(options.targetDir);
|
||||
const resumeCompleted = readExtractResumeState(options.packageDir);
|
||||
const resumeCompletedAtStart = resumeCompleted.size;
|
||||
const candidateNames = new Set(candidates.map((archivePath) => path.basename(archivePath)));
|
||||
@ -751,10 +709,9 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
}
|
||||
|
||||
if (extracted > 0) {
|
||||
const afterFingerprint = captureDirFingerprint(options.targetDir);
|
||||
const changedOutput = hasDirChanges(beforeFingerprint, afterFingerprint);
|
||||
const hasOutputAfter = hasAnyFilesRecursive(options.targetDir);
|
||||
const hadResumeProgress = resumeCompletedAtStart > 0;
|
||||
if (!changedOutput && conflictMode !== "skip" && !hadResumeProgress) {
|
||||
if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) {
|
||||
lastError = "Keine entpackten Dateien erkannt";
|
||||
failed += extracted;
|
||||
extracted = 0;
|
||||
|
||||
@ -3,6 +3,14 @@ import path from "node:path";
|
||||
|
||||
let logFilePath = path.resolve(process.cwd(), "rd_downloader.log");
|
||||
let fallbackLogFilePath: string | null = null;
|
||||
const LOG_FLUSH_INTERVAL_MS = 120;
|
||||
const LOG_BUFFER_LIMIT_CHARS = 1_000_000;
|
||||
|
||||
let pendingLines: string[] = [];
|
||||
let pendingChars = 0;
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
let flushInFlight = false;
|
||||
let exitHookAttached = false;
|
||||
|
||||
export function configureLogger(baseDir: string): void {
|
||||
logFilePath = path.join(baseDir, "rd_downloader.log");
|
||||
@ -20,31 +28,126 @@ function appendLine(filePath: string, line: string): { ok: boolean; errorText: s
|
||||
}
|
||||
}
|
||||
|
||||
function write(level: "INFO" | "WARN" | "ERROR", message: string): void {
|
||||
const line = `${new Date().toISOString()} [${level}] ${message}\n`;
|
||||
const primary = appendLine(logFilePath, line);
|
||||
async function appendChunk(filePath: string, chunk: string): Promise<{ ok: boolean; errorText: string }> {
|
||||
try {
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.promises.appendFile(filePath, chunk, "utf8");
|
||||
return { ok: true, errorText: "" };
|
||||
} catch (error) {
|
||||
return { ok: false, errorText: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
function writeStderr(text: string): void {
|
||||
try {
|
||||
process.stderr.write(text);
|
||||
} catch {
|
||||
// ignore stderr failures
|
||||
}
|
||||
}
|
||||
|
||||
function flushSyncPending(): void {
|
||||
if (pendingLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunk = pendingLines.join("");
|
||||
pendingLines = [];
|
||||
pendingChars = 0;
|
||||
|
||||
const primary = appendLine(logFilePath, chunk);
|
||||
if (fallbackLogFilePath) {
|
||||
const fallback = appendLine(fallbackLogFilePath, line);
|
||||
const fallback = appendLine(fallbackLogFilePath, chunk);
|
||||
if (!primary.ok && !fallback.ok) {
|
||||
try {
|
||||
process.stderr.write(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
|
||||
} catch {
|
||||
// ignore stderr failures
|
||||
}
|
||||
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!primary.ok) {
|
||||
try {
|
||||
process.stderr.write(`LOGGER write failed: ${primary.errorText}\n`);
|
||||
} catch {
|
||||
// ignore stderr failures
|
||||
writeStderr(`LOGGER write failed: ${primary.errorText}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFlush(immediate = false): void {
|
||||
if (flushInFlight) {
|
||||
return;
|
||||
}
|
||||
if (immediate) {
|
||||
if (flushTimer) {
|
||||
clearTimeout(flushTimer);
|
||||
flushTimer = null;
|
||||
}
|
||||
void flushAsync();
|
||||
return;
|
||||
}
|
||||
if (flushTimer) {
|
||||
return;
|
||||
}
|
||||
flushTimer = setTimeout(() => {
|
||||
flushTimer = null;
|
||||
void flushAsync();
|
||||
}, LOG_FLUSH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async function flushAsync(): Promise<void> {
|
||||
if (flushInFlight || pendingLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
flushInFlight = true;
|
||||
const chunk = pendingLines.join("");
|
||||
pendingLines = [];
|
||||
pendingChars = 0;
|
||||
|
||||
try {
|
||||
const primary = await appendChunk(logFilePath, chunk);
|
||||
if (fallbackLogFilePath) {
|
||||
const fallback = await appendChunk(fallbackLogFilePath, chunk);
|
||||
if (!primary.ok && !fallback.ok) {
|
||||
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
|
||||
}
|
||||
} else if (!primary.ok) {
|
||||
writeStderr(`LOGGER write failed: ${primary.errorText}\n`);
|
||||
}
|
||||
} finally {
|
||||
flushInFlight = false;
|
||||
if (pendingLines.length > 0) {
|
||||
scheduleFlush(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureExitHook(): void {
|
||||
if (exitHookAttached) {
|
||||
return;
|
||||
}
|
||||
exitHookAttached = true;
|
||||
process.once("beforeExit", flushSyncPending);
|
||||
process.once("exit", flushSyncPending);
|
||||
}
|
||||
|
||||
function write(level: "INFO" | "WARN" | "ERROR", message: string): void {
|
||||
ensureExitHook();
|
||||
const line = `${new Date().toISOString()} [${level}] ${message}\n`;
|
||||
pendingLines.push(line);
|
||||
pendingChars += line.length;
|
||||
|
||||
while (pendingChars > LOG_BUFFER_LIMIT_CHARS && pendingLines.length > 1) {
|
||||
const removed = pendingLines.shift();
|
||||
if (!removed) {
|
||||
break;
|
||||
}
|
||||
pendingChars = Math.max(0, pendingChars - removed.length);
|
||||
}
|
||||
|
||||
if (level === "ERROR") {
|
||||
scheduleFlush(true);
|
||||
return;
|
||||
}
|
||||
scheduleFlush();
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
info: (msg: string): void => write("INFO", msg),
|
||||
warn: (msg: string): void => write("WARN", msg),
|
||||
|
||||
@ -205,7 +205,7 @@ export function loadSession(paths: StoragePaths): SessionState {
|
||||
|
||||
export function saveSession(paths: StoragePaths, session: SessionState): void {
|
||||
ensureBaseDir(paths.baseDir);
|
||||
const payload = JSON.stringify({ ...session, updatedAt: Date.now() }, null, 2);
|
||||
const payload = JSON.stringify({ ...session, updatedAt: Date.now() });
|
||||
const tempPath = `${paths.sessionFile}.tmp`;
|
||||
fs.writeFileSync(tempPath, payload, "utf8");
|
||||
fs.renameSync(tempPath, paths.sessionFile);
|
||||
|
||||
@ -201,21 +201,39 @@ export function App(): ReactElement {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const packages = useMemo(() => snapshot.session.packageOrder
|
||||
.map((id: string) => snapshot.session.packages[id])
|
||||
.filter(Boolean), [snapshot.session.packageOrder, snapshot.session.packages]);
|
||||
const downloadsTabActive = tab === "downloads";
|
||||
|
||||
const packageOrderKey = useMemo(() => snapshot.session.packageOrder.join("|"), [snapshot.session.packageOrder]);
|
||||
const packages = useMemo(() => {
|
||||
if (!downloadsTabActive) {
|
||||
return [] as PackageEntry[];
|
||||
}
|
||||
return snapshot.session.packageOrder
|
||||
.map((id: string) => snapshot.session.packages[id])
|
||||
.filter(Boolean);
|
||||
}, [downloadsTabActive, snapshot.session.packageOrder, snapshot.session.packages]);
|
||||
|
||||
const packageOrderKey = useMemo(() => {
|
||||
if (!downloadsTabActive) {
|
||||
return "";
|
||||
}
|
||||
return snapshot.session.packageOrder.join("|");
|
||||
}, [downloadsTabActive, snapshot.session.packageOrder]);
|
||||
|
||||
const packagePosition = useMemo(() => {
|
||||
if (!downloadsTabActive) {
|
||||
return new Map<string, number>();
|
||||
}
|
||||
const map = new Map<string, number>();
|
||||
snapshot.session.packageOrder.forEach((id, index) => {
|
||||
map.set(id, index);
|
||||
});
|
||||
return map;
|
||||
}, [packageOrderKey]);
|
||||
}, [downloadsTabActive, packageOrderKey, snapshot.session.packageOrder]);
|
||||
|
||||
const itemsByPackage = useMemo(() => {
|
||||
if (!downloadsTabActive) {
|
||||
return new Map<string, DownloadItem[]>();
|
||||
}
|
||||
const map = new Map<string, DownloadItem[]>();
|
||||
for (const packageId of snapshot.session.packageOrder) {
|
||||
const pkg = snapshot.session.packages[packageId];
|
||||
@ -228,9 +246,12 @@ export function App(): ReactElement {
|
||||
map.set(packageId, items);
|
||||
}
|
||||
return map;
|
||||
}, [packageOrderKey, snapshot.session.items, snapshot.session.packages]);
|
||||
}, [downloadsTabActive, packageOrderKey, snapshot.session.items, snapshot.session.packages, snapshot.session.packageOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadsTabActive) {
|
||||
return;
|
||||
}
|
||||
setCollapsedPackages((prev) => {
|
||||
const next: Record<string, boolean> = {};
|
||||
const defaultCollapsed = snapshot.session.packageOrder.length >= 24;
|
||||
@ -239,7 +260,7 @@ export function App(): ReactElement {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [packageOrderKey, snapshot.session.packageOrder.length]);
|
||||
}, [downloadsTabActive, packageOrderKey, snapshot.session.packageOrder.length]);
|
||||
|
||||
const deferredDownloadSearch = useDeferredValue(downloadSearch);
|
||||
|
||||
|
||||
@ -2941,4 +2941,136 @@ describe("download manager", () => {
|
||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt");
|
||||
});
|
||||
|
||||
it("does not fail startup post-processing when source package dir is missing but extract output exists", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const outputDir = path.join(root, "downloads", "missing-source-ok");
|
||||
const extractDir = path.join(root, "extract", "missing-source-ok");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(extractDir, "episode.mkv"), "ok", "utf8");
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "missing-source-ok-pkg";
|
||||
const itemId = "missing-source-ok-item";
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: "missing-source-ok",
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [itemId],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
session.items[itemId] = {
|
||||
id: itemId,
|
||||
packageId,
|
||||
url: "https://dummy/missing-source-ok",
|
||||
provider: "megadebrid",
|
||||
status: "completed",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: 123,
|
||||
totalBytes: 123,
|
||||
progressPercent: 100,
|
||||
fileName: "missing-source-ok.part01.rar",
|
||||
targetPath: path.join(outputDir, "missing-source-ok.part01.rar"),
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig (123 B)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: true,
|
||||
enableIntegrityCheck: false,
|
||||
cleanupMode: "none"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
await waitFor(() => manager.getSnapshot().session.items[itemId]?.fullStatus.startsWith("Entpackt"), 12000);
|
||||
const snapshot = manager.getSnapshot();
|
||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt");
|
||||
});
|
||||
|
||||
it("marks missing source package dir as extracted instead of failed", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const outputDir = path.join(root, "downloads", "missing-source-empty");
|
||||
const extractDir = path.join(root, "extract", "missing-source-empty");
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "missing-source-empty-pkg";
|
||||
const itemId = "missing-source-empty-item";
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: "missing-source-empty",
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [itemId],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
session.items[itemId] = {
|
||||
id: itemId,
|
||||
packageId,
|
||||
url: "https://dummy/missing-source-empty",
|
||||
provider: "megadebrid",
|
||||
status: "completed",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: 123,
|
||||
totalBytes: 123,
|
||||
progressPercent: 100,
|
||||
fileName: "missing-source-empty.part01.rar",
|
||||
targetPath: path.join(outputDir, "missing-source-empty.part01.rar"),
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig (123 B)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: true,
|
||||
enableIntegrityCheck: false,
|
||||
cleanupMode: "none"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
await waitFor(() => manager.getSnapshot().session.items[itemId]?.fullStatus.startsWith("Entpackt"), 12000);
|
||||
const snapshot = manager.getSnapshot();
|
||||
expect(snapshot.session.packages[packageId]?.status).toBe("completed");
|
||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt (Quelle fehlt)");
|
||||
});
|
||||
});
|
||||
|
||||
@ -331,4 +331,25 @@ describe("extractor", () => {
|
||||
signal: controller.signal
|
||||
})).rejects.toThrow("aborted:extract");
|
||||
});
|
||||
|
||||
it("handles missing package source directory without throwing", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
|
||||
tempDirs.push(root);
|
||||
const packageDir = path.join(root, "pkg-missing");
|
||||
const targetDir = path.join(root, "out");
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(targetDir, "video.mkv"), "ok", "utf8");
|
||||
|
||||
const result = await extractPackageArchives({
|
||||
packageDir,
|
||||
targetDir,
|
||||
cleanupMode: "none",
|
||||
conflictMode: "overwrite",
|
||||
removeLinks: false,
|
||||
removeSamples: false
|
||||
});
|
||||
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.extracted).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user