Release v1.6.26
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e85f12977f
commit
55b00bf884
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
// Only overwrite outcome for non-completed items to preserve correct summary stats
|
||||||
|
if (item.status !== "completed") {
|
||||||
this.recordRunOutcome(itemId, "cancelled");
|
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, []);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user