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",
"version": "1.6.25",
"version": "1.6.26",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -751,6 +751,36 @@ function resolveArchiveItemsFromList(archiveName: string, items: DownloadItem[])
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) => {
const name = path.basename(item.targetPath || item.fileName || "").toLowerCase();
return name === entryLower;
@ -1680,6 +1710,9 @@ export class DownloadManager extends EventEmitter {
item.lastError = "Datei nicht gefunden auf Rapidgator";
item.onlineStatus = "offline";
item.updatedAt = nowMs();
if (this.runItemIds.has(itemId)) {
this.recordRunOutcome(itemId, "failed");
}
// Refresh package status since item was set to failed
const pkg = this.session.packages[item.packageId];
if (pkg) this.refreshPackageStatus(pkg);
@ -2460,7 +2493,10 @@ export class DownloadManager extends EventEmitter {
if (!item) {
continue;
}
// 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);
if (active) {
active.abortReason = "cancel";
@ -2687,6 +2723,25 @@ export class DownloadManager extends EventEmitter {
const pkg = this.session.packages[pkgId];
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.emitState();
}
@ -3196,6 +3251,7 @@ export class DownloadManager extends EventEmitter {
item.fullStatus = "Wartet";
item.lastError = "";
item.speedBps = 0;
item.updatedAt = nowMs();
continue;
}
if (item.status === "extracting" || item.status === "integrity_check") {
@ -3204,6 +3260,7 @@ export class DownloadManager extends EventEmitter {
item.status = "completed";
item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`;
item.speedBps = 0;
item.updatedAt = nowMs();
} else if (item.status === "downloading"
|| item.status === "validating"
|| item.status === "paused"
@ -3211,6 +3268,7 @@ export class DownloadManager extends EventEmitter {
item.status = "queued";
item.fullStatus = "Wartet";
item.speedBps = 0;
item.updatedAt = nowMs();
}
// Clear stale transient status texts from previous session
if (item.status === "queued") {
@ -3292,6 +3350,7 @@ export class DownloadManager extends EventEmitter {
if (!pkg) {
continue;
}
const completedItemIds: string[] = [];
pkg.itemIds = pkg.itemIds.filter((itemId) => {
const item = this.session.items[itemId];
if (!item) {
@ -3302,14 +3361,18 @@ export class DownloadManager extends EventEmitter {
if (this.settings.autoExtract && !isExtractedLabel(item.fullStatus || "")) {
return true;
}
delete this.session.items[itemId];
this.itemCount = Math.max(0, this.itemCount - 1);
completedItemIds.push(itemId);
return false;
}
return true;
});
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") {
const allCompleted = pkg.itemIds.every((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 (this.settings.autoExtract) {
@ -4624,6 +4687,7 @@ export class DownloadManager extends EventEmitter {
if (item.attempts < maxAttempts) {
item.status = "integrity_check";
item.progressPercent = 0;
this.dropItemContribution(item.id);
item.downloadedBytes = 0;
item.totalBytes = unrestricted.fileSize;
this.emitState();
@ -4992,6 +5056,15 @@ export class DownloadManager extends EventEmitter {
} catch {
// 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> = {};
if (existingBytes > 0) {
headers.Range = `bytes=${existingBytes}-`;
@ -6071,6 +6144,12 @@ export class DownloadManager extends EventEmitter {
const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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;
}
@ -6755,6 +6834,7 @@ export class DownloadManager extends EventEmitter {
delete this.session.items[itemId];
this.itemCount = Math.max(0, this.itemCount - 1);
this.retryAfterByItem.delete(itemId);
this.retryStateByItem.delete(itemId);
if (pkg.itemIds.length === 0) {
this.removePackageFromSession(packageId, []);
}

View File

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