Release v1.4.9 with extraction resume fix and faster update downloads
- Fix extraction status display after restart (shows "Entpacken ausstehend" instead of stale status) - Fix Start button to trigger pending extractions for already-downloaded packages - Fix extraction resume when archives already cleaned (recognizes completed state from resume file) - Reduce update download connection timeout from 8min to 30s per candidate for faster fallback - Add logging for update download candidates and failures - Show manual download URL on update failure - Sequential extraction preserved (one package at a time via queue) - Extraction properly cancelled on shutdown, resumes on restart Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
333a912d67
commit
e1286e02af
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.8",
|
||||
"version": "1.4.9",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -969,6 +969,8 @@ export class DownloadManager extends EventEmitter {
|
||||
this.emitState(true);
|
||||
}
|
||||
|
||||
this.triggerPendingExtractions();
|
||||
|
||||
const runItems = Object.values(this.session.items)
|
||||
.filter((item) => {
|
||||
if (item.status !== "queued" && item.status !== "reconnect_wait") {
|
||||
@ -1420,6 +1422,15 @@ export class DownloadManager extends EventEmitter {
|
||||
const needsPostProcess = pkg.status !== "completed"
|
||||
|| items.some((item) => item.status === "completed" && !isExtractedLabel(item.fullStatus));
|
||||
if (needsPostProcess) {
|
||||
pkg.status = "queued";
|
||||
pkg.updatedAt = nowMs();
|
||||
for (const item of items) {
|
||||
if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) {
|
||||
item.fullStatus = "Entpacken ausstehend";
|
||||
item.updatedAt = nowMs();
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
void this.runPackagePostProcessing(packageId);
|
||||
} else if (pkg.status !== "completed") {
|
||||
pkg.status = "completed";
|
||||
@ -1443,6 +1454,47 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private triggerPendingExtractions(): void {
|
||||
if (!this.settings.autoExtract) {
|
||||
return;
|
||||
}
|
||||
for (const packageId of this.session.packageOrder) {
|
||||
const pkg = this.session.packages[packageId];
|
||||
if (!pkg || pkg.cancelled || !pkg.enabled) {
|
||||
continue;
|
||||
}
|
||||
if (this.packagePostProcessTasks.has(packageId)) {
|
||||
continue;
|
||||
}
|
||||
const items = pkg.itemIds.map((id) => this.session.items[id]).filter(Boolean) as DownloadItem[];
|
||||
if (items.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const success = items.filter((item) => item.status === "completed").length;
|
||||
const failed = items.filter((item) => item.status === "failed").length;
|
||||
const cancelled = items.filter((item) => item.status === "cancelled").length;
|
||||
if (success + failed + cancelled < items.length || failed > 0 || success === 0) {
|
||||
continue;
|
||||
}
|
||||
const needsExtraction = items.some((item) =>
|
||||
item.status === "completed" && !isExtractedLabel(item.fullStatus)
|
||||
);
|
||||
if (!needsExtraction) {
|
||||
continue;
|
||||
}
|
||||
pkg.status = "queued";
|
||||
pkg.updatedAt = nowMs();
|
||||
for (const item of items) {
|
||||
if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) {
|
||||
item.fullStatus = "Entpacken ausstehend";
|
||||
item.updatedAt = nowMs();
|
||||
}
|
||||
}
|
||||
logger.info(`Entpacken via Start ausgelöst: pkg=${pkg.name}`);
|
||||
void this.runPackagePostProcessing(packageId);
|
||||
}
|
||||
}
|
||||
|
||||
private removePackageFromSession(packageId: string, itemIds: string[]): void {
|
||||
for (const itemId of itemIds) {
|
||||
delete this.session.items[itemId];
|
||||
|
||||
@ -571,6 +571,19 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
const candidates = findArchiveCandidates(options.packageDir);
|
||||
logger.info(`Entpacken gestartet: packageDir=${options.packageDir}, targetDir=${options.targetDir}, archives=${candidates.length}, cleanupMode=${options.cleanupMode}, conflictMode=${options.conflictMode}`);
|
||||
if (candidates.length === 0) {
|
||||
const existingResume = readExtractResumeState(options.packageDir);
|
||||
if (existingResume.size > 0 && hasAnyFilesRecursive(options.targetDir)) {
|
||||
clearExtractResumeState(options.packageDir);
|
||||
logger.info(`Entpacken übersprungen (Archive bereinigt, Ziel hat Dateien): ${options.packageDir}`);
|
||||
options.onProgress?.({
|
||||
current: existingResume.size,
|
||||
total: existingResume.size,
|
||||
percent: 100,
|
||||
archiveName: "",
|
||||
phase: "done"
|
||||
});
|
||||
return { extracted: existingResume.size, failed: 0, lastError: "" };
|
||||
}
|
||||
clearExtractResumeState(options.packageDir);
|
||||
logger.info(`Entpacken übersprungen (keine Archive gefunden): ${options.packageDir}`);
|
||||
return { extracted: 0, failed: 0, lastError: "" };
|
||||
|
||||
@ -8,9 +8,10 @@ import { ReadableStream as NodeReadableStream } from "node:stream/web";
|
||||
import { APP_VERSION, DEFAULT_UPDATE_REPO } from "./constants";
|
||||
import { UpdateCheckResult, UpdateInstallResult } from "../shared/types";
|
||||
import { compactErrorText } from "./utils";
|
||||
import { logger } from "./logger";
|
||||
|
||||
const RELEASE_FETCH_TIMEOUT_MS = 12000;
|
||||
const DOWNLOAD_TIMEOUT_MS = 8 * 60 * 1000;
|
||||
const CONNECT_TIMEOUT_MS = 30000;
|
||||
const UPDATE_USER_AGENT = `RD-Node-Downloader/${APP_VERSION}`;
|
||||
|
||||
type ReleaseAsset = {
|
||||
@ -274,13 +275,15 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, targetPath: string): Promise<void> {
|
||||
const timeout = timeoutController(DOWNLOAD_TIMEOUT_MS);
|
||||
logger.info(`Update-Download versucht: ${url}`);
|
||||
const timeout = timeoutController(CONNECT_TIMEOUT_MS);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": UPDATE_USER_AGENT
|
||||
},
|
||||
redirect: "follow",
|
||||
signal: timeout.signal
|
||||
});
|
||||
} finally {
|
||||
@ -294,11 +297,13 @@ async function downloadFile(url: string, targetPath: string): Promise<void> {
|
||||
const source = Readable.fromWeb(response.body as unknown as NodeReadableStream<Uint8Array>);
|
||||
const target = fs.createWriteStream(targetPath);
|
||||
await pipeline(source, target);
|
||||
logger.info(`Update-Download abgeschlossen: ${targetPath}`);
|
||||
}
|
||||
|
||||
async function downloadFromCandidates(candidates: string[], targetPath: string): Promise<void> {
|
||||
let lastError: unknown = new Error("Update Download fehlgeschlagen");
|
||||
|
||||
logger.info(`Update-Download: ${candidates.length} Kandidat(en)`);
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const candidate = candidates[index];
|
||||
try {
|
||||
@ -306,6 +311,7 @@ async function downloadFromCandidates(candidates: string[], targetPath: string):
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
logger.warn(`Update-Download Kandidat ${index + 1}/${candidates.length} fehlgeschlagen: ${compactErrorText(error)}`);
|
||||
try {
|
||||
await fs.promises.rm(targetPath, { force: true });
|
||||
} catch {
|
||||
@ -373,6 +379,8 @@ export async function installLatestUpdate(repo: string, prechecked?: UpdateCheck
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return { started: false, message: compactErrorText(error) };
|
||||
const releaseUrl = String(effectiveCheck.releaseUrl || "").trim();
|
||||
const hint = releaseUrl ? ` – Manuell: ${releaseUrl}` : "";
|
||||
return { started: false, message: `${compactErrorText(error)}${hint}` };
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user