Release v1.6.26

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 20:18:47 +01:00
parent e85f12977f
commit 55b00bf884
3 changed files with 91 additions and 11 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.25", "version": "1.6.26",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -751,6 +751,36 @@ function resolveArchiveItemsFromList(archiveName: string, items: DownloadItem[])
return pattern.test(name); return pattern.test(name);
}); });
} }
// Split ZIP (e.g., movie.zip.001, movie.zip.002)
const zipSplitMatch = entryLower.match(/^(.*)\.zip\.001$/);
if (zipSplitMatch) {
const stem = zipSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`^${stem}\\.zip(\\.\\d+)?$`, "i");
return items.filter((item) => {
const name = path.basename(item.targetPath || item.fileName || "");
return pattern.test(name);
});
}
// Split 7z (e.g., movie.7z.001, movie.7z.002)
const sevenSplitMatch = entryLower.match(/^(.*)\.7z\.001$/);
if (sevenSplitMatch) {
const stem = sevenSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`^${stem}\\.7z(\\.\\d+)?$`, "i");
return items.filter((item) => {
const name = path.basename(item.targetPath || item.fileName || "");
return pattern.test(name);
});
}
// Generic .NNN splits (e.g., movie.001, movie.002)
const genericSplitMatch = entryLower.match(/^(.*)\.001$/);
if (genericSplitMatch && !/\.(zip|7z)\.001$/.test(entryLower)) {
const stem = genericSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`^${stem}\\.\\d{3}$`, "i");
return items.filter((item) => {
const name = path.basename(item.targetPath || item.fileName || "");
return pattern.test(name);
});
}
return items.filter((item) => { return items.filter((item) => {
const name = path.basename(item.targetPath || item.fileName || "").toLowerCase(); const name = path.basename(item.targetPath || item.fileName || "").toLowerCase();
return name === entryLower; return name === entryLower;
@ -1680,6 +1710,9 @@ export class DownloadManager extends EventEmitter {
item.lastError = "Datei nicht gefunden auf Rapidgator"; item.lastError = "Datei nicht gefunden auf Rapidgator";
item.onlineStatus = "offline"; item.onlineStatus = "offline";
item.updatedAt = nowMs(); item.updatedAt = nowMs();
if (this.runItemIds.has(itemId)) {
this.recordRunOutcome(itemId, "failed");
}
// Refresh package status since item was set to failed // Refresh package status since item was set to failed
const pkg = this.session.packages[item.packageId]; const pkg = this.session.packages[item.packageId];
if (pkg) this.refreshPackageStatus(pkg); if (pkg) this.refreshPackageStatus(pkg);
@ -2460,7 +2493,10 @@ export class DownloadManager extends EventEmitter {
if (!item) { if (!item) {
continue; continue;
} }
this.recordRunOutcome(itemId, "cancelled"); // Only overwrite outcome for non-completed items to preserve correct summary stats
if (item.status !== "completed") {
this.recordRunOutcome(itemId, "cancelled");
}
const active = this.activeTasks.get(itemId); const active = this.activeTasks.get(itemId);
if (active) { if (active) {
active.abortReason = "cancel"; active.abortReason = "cancel";
@ -2687,6 +2723,25 @@ export class DownloadManager extends EventEmitter {
const pkg = this.session.packages[pkgId]; const pkg = this.session.packages[pkgId];
if (pkg) this.refreshPackageStatus(pkg); if (pkg) this.refreshPackageStatus(pkg);
} }
// Trigger extraction if all items are now in a terminal state and some completed
if (this.settings.autoExtract) {
for (const pkgId of affectedPackageIds) {
const pkg = this.session.packages[pkgId];
if (!pkg || pkg.cancelled || this.packagePostProcessTasks.has(pkgId)) continue;
const pkgItems = pkg.itemIds.map((id) => this.session.items[id]).filter(Boolean) as DownloadItem[];
const hasPending = pkgItems.some((i) => i.status !== "completed" && i.status !== "failed" && i.status !== "cancelled");
const hasUnextracted = pkgItems.some((i) => i.status === "completed" && !isExtractedLabel(i.fullStatus || ""));
if (!hasPending && hasUnextracted) {
for (const it of pkgItems) {
if (it.status === "completed" && !isExtractedLabel(it.fullStatus || "")) {
it.fullStatus = "Entpacken - Ausstehend";
it.updatedAt = nowMs();
}
}
void this.runPackagePostProcessing(pkgId).catch((err) => logger.warn(`Post-processing nach Skip: ${compactErrorText(err)}`));
}
}
}
this.persistSoon(); this.persistSoon();
this.emitState(); this.emitState();
} }
@ -3196,6 +3251,7 @@ export class DownloadManager extends EventEmitter {
item.fullStatus = "Wartet"; item.fullStatus = "Wartet";
item.lastError = ""; item.lastError = "";
item.speedBps = 0; item.speedBps = 0;
item.updatedAt = nowMs();
continue; continue;
} }
if (item.status === "extracting" || item.status === "integrity_check") { if (item.status === "extracting" || item.status === "integrity_check") {
@ -3204,6 +3260,7 @@ export class DownloadManager extends EventEmitter {
item.status = "completed"; item.status = "completed";
item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`; item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`;
item.speedBps = 0; item.speedBps = 0;
item.updatedAt = nowMs();
} else if (item.status === "downloading" } else if (item.status === "downloading"
|| item.status === "validating" || item.status === "validating"
|| item.status === "paused" || item.status === "paused"
@ -3211,6 +3268,7 @@ export class DownloadManager extends EventEmitter {
item.status = "queued"; item.status = "queued";
item.fullStatus = "Wartet"; item.fullStatus = "Wartet";
item.speedBps = 0; item.speedBps = 0;
item.updatedAt = nowMs();
} }
// Clear stale transient status texts from previous session // Clear stale transient status texts from previous session
if (item.status === "queued") { if (item.status === "queued") {
@ -3292,6 +3350,7 @@ export class DownloadManager extends EventEmitter {
if (!pkg) { if (!pkg) {
continue; continue;
} }
const completedItemIds: string[] = [];
pkg.itemIds = pkg.itemIds.filter((itemId) => { pkg.itemIds = pkg.itemIds.filter((itemId) => {
const item = this.session.items[itemId]; const item = this.session.items[itemId];
if (!item) { if (!item) {
@ -3302,14 +3361,18 @@ export class DownloadManager extends EventEmitter {
if (this.settings.autoExtract && !isExtractedLabel(item.fullStatus || "")) { if (this.settings.autoExtract && !isExtractedLabel(item.fullStatus || "")) {
return true; return true;
} }
delete this.session.items[itemId]; completedItemIds.push(itemId);
this.itemCount = Math.max(0, this.itemCount - 1);
return false; return false;
} }
return true; return true;
}); });
if (pkg.itemIds.length === 0) { if (pkg.itemIds.length === 0) {
this.removePackageFromSession(pkgId, []); this.removePackageFromSession(pkgId, completedItemIds);
} else {
for (const itemId of completedItemIds) {
delete this.session.items[itemId];
this.itemCount = Math.max(0, this.itemCount - 1);
}
} }
} }
} }
@ -3349,7 +3412,7 @@ export class DownloadManager extends EventEmitter {
} else if (policy === "package_done" || policy === "on_start") { } else if (policy === "package_done" || policy === "on_start") {
const allCompleted = pkg.itemIds.every((id) => { const allCompleted = pkg.itemIds.every((id) => {
const item = this.session.items[id]; const item = this.session.items[id];
return !item || item.status === "completed"; return !item || item.status === "completed" || item.status === "failed" || item.status === "cancelled";
}); });
if (!allCompleted) continue; if (!allCompleted) continue;
if (this.settings.autoExtract) { if (this.settings.autoExtract) {
@ -4624,6 +4687,7 @@ export class DownloadManager extends EventEmitter {
if (item.attempts < maxAttempts) { if (item.attempts < maxAttempts) {
item.status = "integrity_check"; item.status = "integrity_check";
item.progressPercent = 0; item.progressPercent = 0;
this.dropItemContribution(item.id);
item.downloadedBytes = 0; item.downloadedBytes = 0;
item.totalBytes = unrestricted.fileSize; item.totalBytes = unrestricted.fileSize;
this.emitState(); this.emitState();
@ -4992,6 +5056,15 @@ export class DownloadManager extends EventEmitter {
} catch { } catch {
// file does not exist // file does not exist
} }
// Guard against pre-allocated sparse files from a crashed session:
// if file size exceeds persisted downloadedBytes by >1MB, the file was
// likely pre-allocated but only partially written before a hard crash.
if (existingBytes > 0 && item.downloadedBytes > 0 && existingBytes > item.downloadedBytes + 1048576) {
try {
await fs.promises.truncate(effectiveTargetPath, item.downloadedBytes);
existingBytes = item.downloadedBytes;
} catch { /* best-effort */ }
}
const headers: Record<string, string> = {}; const headers: Record<string, string> = {};
if (existingBytes > 0) { if (existingBytes > 0) {
headers.Range = `bytes=${existingBytes}-`; headers.Range = `bytes=${existingBytes}-`;
@ -6071,6 +6144,12 @@ export class DownloadManager extends EventEmitter {
const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`^${escaped}\\.7z(\\.\\d+)?$`, "i").test(fileName); return new RegExp(`^${escaped}\\.7z(\\.\\d+)?$`, "i").test(fileName);
} }
// Generic .NNN splits (e.g., movie.001, movie.002)
if (/\.001$/i.test(entryPointName) && !/\.(zip|7z)\.001$/i.test(entryPointName)) {
const stem = entryPointName.replace(/\.001$/i, "").toLowerCase();
const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`^${escaped}\\.\\d{3}$`, "i").test(fileName);
}
return false; return false;
} }
@ -6755,6 +6834,7 @@ export class DownloadManager extends EventEmitter {
delete this.session.items[itemId]; delete this.session.items[itemId];
this.itemCount = Math.max(0, this.itemCount - 1); this.itemCount = Math.max(0, this.itemCount - 1);
this.retryAfterByItem.delete(itemId); this.retryAfterByItem.delete(itemId);
this.retryStateByItem.delete(itemId);
if (pkg.itemIds.length === 0) { if (pkg.itemIds.length === 0) {
this.removePackageFromSession(packageId, []); this.removePackageFromSession(packageId, []);
} }

View File

@ -209,7 +209,7 @@ function archiveSortKey(filePath: string): string {
.replace(/\.zip\.\d{3}$/i, "") .replace(/\.zip\.\d{3}$/i, "")
.replace(/\.7z\.\d{3}$/i, "") .replace(/\.7z\.\d{3}$/i, "")
.replace(/\.\d{3}$/i, "") .replace(/\.\d{3}$/i, "")
.replace(/\.tar\.(gz|bz2|xz)$/i, "") .replace(/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i, "")
.replace(/\.rar$/i, "") .replace(/\.rar$/i, "")
.replace(/\.zip$/i, "") .replace(/\.zip$/i, "")
.replace(/\.7z$/i, "") .replace(/\.7z$/i, "")
@ -230,7 +230,7 @@ function archiveTypeRank(filePath: string): number {
if (/\.7z(?:\.\d{3})?$/i.test(fileName)) { if (/\.7z(?:\.\d{3})?$/i.test(fileName)) {
return 3; return 3;
} }
if (/\.tar\.(gz|bz2|xz)$/i.test(fileName)) { if (/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i.test(fileName)) {
return 4; return 4;
} }
if (/\.\d{3}$/i.test(fileName)) { if (/\.\d{3}$/i.test(fileName)) {
@ -281,7 +281,7 @@ export async function findArchiveCandidates(packageDir: string): Promise<string[
} }
return !fileNamesLower.has(`${fileName}.001`.toLowerCase()); return !fileNamesLower.has(`${fileName}.001`.toLowerCase());
}); });
const tarCompressed = files.filter((filePath) => /\.tar\.(gz|bz2|xz)$/i.test(filePath)); const tarCompressed = files.filter((filePath) => /\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i.test(filePath));
// Generic .001 splits (HJSplit etc.) — exclude already-recognized .zip.001 and .7z.001 // Generic .001 splits (HJSplit etc.) — exclude already-recognized .zip.001 and .7z.001
const genericSplit = files.filter((filePath) => { const genericSplit = files.filter((filePath) => {
const fileName = path.basename(filePath).toLowerCase(); const fileName = path.basename(filePath).toLowerCase();
@ -477,7 +477,7 @@ export function archiveFilenamePasswords(archiveName: string): string[] {
.replace(/\.zip\.\d{3}$/i, "") .replace(/\.zip\.\d{3}$/i, "")
.replace(/\.7z\.\d{3}$/i, "") .replace(/\.7z\.\d{3}$/i, "")
.replace(/\.\d{3}$/i, "") .replace(/\.\d{3}$/i, "")
.replace(/\.tar\.(gz|bz2|xz)$/i, "") .replace(/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i, "")
.replace(/\.(rar|zip|7z|tar|gz|bz2|xz)$/i, ""); .replace(/\.(rar|zip|7z|tar|gz|bz2|xz)$/i, "");
if (!stem) return []; if (!stem) return [];
const candidates = [stem]; const candidates = [stem];
@ -1345,7 +1345,7 @@ async function runExternalExtract(
// subst only needed for legacy UnRAR/7z (MAX_PATH limit) // subst only needed for legacy UnRAR/7z (MAX_PATH limit)
subst = createSubstMapping(targetDir); subst = createSubstMapping(targetDir);
const effectiveTargetDir = subst ? `${subst.drive}:` : targetDir; const effectiveTargetDir = subst ? `${subst.drive}:\\` : targetDir;
const command = await resolveExtractorCommand(); const command = await resolveExtractorCommand();
const password = await runExternalExtractInner( const password = await runExternalExtractInner(