Release v1.4.11 with stability hardening and full-function regression pass
This commit is contained in:
parent
6e72c63268
commit
8b5c936177
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.10",
|
"version": "1.4.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.10",
|
"version": "1.4.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.10",
|
"version": "1.4.11",
|
||||||
"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",
|
||||||
|
|||||||
@ -36,6 +36,9 @@ export interface ExtractProgressUpdate {
|
|||||||
|
|
||||||
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
|
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
|
||||||
const EXTRACT_PROGRESS_FILE = ".rd_extract_progress.json";
|
const EXTRACT_PROGRESS_FILE = ".rd_extract_progress.json";
|
||||||
|
const EXTRACT_BASE_TIMEOUT_MS = 6 * 60 * 1000;
|
||||||
|
const EXTRACT_PER_GIB_TIMEOUT_MS = 7 * 60 * 1000;
|
||||||
|
const EXTRACT_MAX_TIMEOUT_MS = 40 * 60 * 1000;
|
||||||
|
|
||||||
type ExtractResumeState = {
|
type ExtractResumeState = {
|
||||||
completedArchives: string[];
|
completedArchives: string[];
|
||||||
@ -110,6 +113,17 @@ function shouldPreferExternalZip(archivePath: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeExtractTimeoutMs(archivePath: string): number {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(archivePath);
|
||||||
|
const gib = stat.size / (1024 * 1024 * 1024);
|
||||||
|
const dynamicMs = EXTRACT_BASE_TIMEOUT_MS + Math.floor(gib * EXTRACT_PER_GIB_TIMEOUT_MS);
|
||||||
|
return Math.max(EXTRACT_BASE_TIMEOUT_MS, Math.min(EXTRACT_MAX_TIMEOUT_MS, dynamicMs));
|
||||||
|
} catch {
|
||||||
|
return EXTRACT_BASE_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function extractProgressFilePath(packageDir: string): string {
|
function extractProgressFilePath(packageDir: string): string {
|
||||||
return path.join(packageDir, EXTRACT_PROGRESS_FILE);
|
return path.join(packageDir, EXTRACT_PROGRESS_FILE);
|
||||||
}
|
}
|
||||||
@ -215,6 +229,7 @@ type ExtractSpawnResult = {
|
|||||||
ok: boolean;
|
ok: boolean;
|
||||||
missingCommand: boolean;
|
missingCommand: boolean;
|
||||||
aborted: boolean;
|
aborted: boolean;
|
||||||
|
timedOut: boolean;
|
||||||
errorText: string;
|
errorText: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -222,28 +237,51 @@ function runExtractCommand(
|
|||||||
command: string,
|
command: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
onChunk?: (chunk: string) => void,
|
onChunk?: (chunk: string) => void,
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal,
|
||||||
|
timeoutMs?: number
|
||||||
): Promise<ExtractSpawnResult> {
|
): Promise<ExtractSpawnResult> {
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
return Promise.resolve({ ok: false, missingCommand: false, aborted: true, errorText: "aborted:extract" });
|
return Promise.resolve({ ok: false, missingCommand: false, aborted: true, timedOut: false, errorText: "aborted:extract" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
let output = "";
|
let output = "";
|
||||||
const child = spawn(command, args, { windowsHide: true });
|
const child = spawn(command, args, { windowsHide: true });
|
||||||
|
let timeoutId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
const finish = (result: ExtractSpawnResult): void => {
|
const finish = (result: ExtractSpawnResult): void => {
|
||||||
if (settled) {
|
if (settled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settled = true;
|
settled = true;
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
if (signal && onAbort) {
|
if (signal && onAbort) {
|
||||||
signal.removeEventListener("abort", onAbort);
|
signal.removeEventListener("abort", onAbort);
|
||||||
}
|
}
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (timeoutMs && timeoutMs > 0) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
child.kill();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
finish({
|
||||||
|
ok: false,
|
||||||
|
missingCommand: false,
|
||||||
|
aborted: false,
|
||||||
|
timedOut: true,
|
||||||
|
errorText: `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s`
|
||||||
|
});
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
const onAbort = signal
|
const onAbort = signal
|
||||||
? (): void => {
|
? (): void => {
|
||||||
try {
|
try {
|
||||||
@ -251,7 +289,7 @@ function runExtractCommand(
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
finish({ ok: false, missingCommand: false, aborted: true, errorText: "aborted:extract" });
|
finish({ ok: false, missingCommand: false, aborted: true, timedOut: false, errorText: "aborted:extract" });
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
if (signal && onAbort) {
|
if (signal && onAbort) {
|
||||||
@ -275,13 +313,14 @@ function runExtractCommand(
|
|||||||
ok: false,
|
ok: false,
|
||||||
missingCommand: text.toLowerCase().includes("enoent"),
|
missingCommand: text.toLowerCase().includes("enoent"),
|
||||||
aborted: false,
|
aborted: false,
|
||||||
|
timedOut: false,
|
||||||
errorText: text
|
errorText: text
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
if (code === 0 || code === 1) {
|
if (code === 0 || code === 1) {
|
||||||
finish({ ok: true, missingCommand: false, aborted: false, errorText: "" });
|
finish({ ok: true, missingCommand: false, aborted: false, timedOut: false, errorText: "" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cleaned = cleanErrorText(output);
|
const cleaned = cleanErrorText(output);
|
||||||
@ -289,6 +328,7 @@ function runExtractCommand(
|
|||||||
ok: false,
|
ok: false,
|
||||||
missingCommand: false,
|
missingCommand: false,
|
||||||
aborted: false,
|
aborted: false,
|
||||||
|
timedOut: false,
|
||||||
errorText: cleaned || `Exit Code ${String(code ?? "?")}`
|
errorText: cleaned || `Exit Code ${String(code ?? "?")}`
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -353,6 +393,7 @@ async function runExternalExtract(
|
|||||||
const command = await resolveExtractorCommand();
|
const command = await resolveExtractorCommand();
|
||||||
const passwords = passwordCandidates;
|
const passwords = passwordCandidates;
|
||||||
let lastError = "";
|
let lastError = "";
|
||||||
|
const timeoutMs = computeExtractTimeoutMs(archivePath);
|
||||||
|
|
||||||
fs.mkdirSync(targetDir, { recursive: true });
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
|
||||||
@ -375,7 +416,7 @@ async function runExternalExtract(
|
|||||||
}
|
}
|
||||||
bestPercent = parsed;
|
bestPercent = parsed;
|
||||||
onArchiveProgress?.(bestPercent);
|
onArchiveProgress?.(bestPercent);
|
||||||
}, signal);
|
}, signal, timeoutMs);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
onArchiveProgress?.(100);
|
onArchiveProgress?.(100);
|
||||||
return password;
|
return password;
|
||||||
@ -385,6 +426,11 @@ async function runExternalExtract(
|
|||||||
throw new Error("aborted:extract");
|
throw new Error("aborted:extract");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.timedOut) {
|
||||||
|
lastError = result.errorText;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.missingCommand) {
|
if (result.missingCommand) {
|
||||||
resolvedExtractorCommand = null;
|
resolvedExtractorCommand = null;
|
||||||
resolveFailureReason = NO_EXTRACTOR_MESSAGE;
|
resolveFailureReason = NO_EXTRACTOR_MESSAGE;
|
||||||
|
|||||||
@ -205,22 +205,51 @@ export function App(): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const downloadsTabActive = tab === "downloads";
|
const downloadsTabActive = tab === "downloads";
|
||||||
|
const deferredDownloadSearch = useDeferredValue(downloadSearch);
|
||||||
|
const downloadSearchQuery = deferredDownloadSearch.trim().toLowerCase();
|
||||||
|
const downloadSearchActive = downloadSearchQuery.length > 0;
|
||||||
|
const totalPackageCount = snapshot.session.packageOrder.length;
|
||||||
|
const shouldLimitPackageRendering = downloadsTabActive
|
||||||
|
&& snapshot.session.running
|
||||||
|
&& !downloadSearchActive
|
||||||
|
&& totalPackageCount > AUTO_RENDER_PACKAGE_LIMIT
|
||||||
|
&& !showAllPackages;
|
||||||
|
|
||||||
const packages = useMemo(() => {
|
const packageIdsForView = useMemo(() => {
|
||||||
if (!downloadsTabActive) {
|
if (!downloadsTabActive) {
|
||||||
return [] as PackageEntry[];
|
return [] as string[];
|
||||||
}
|
}
|
||||||
return snapshot.session.packageOrder
|
if (downloadSearchActive) {
|
||||||
.map((id: string) => snapshot.session.packages[id])
|
return snapshot.session.packageOrder;
|
||||||
.filter(Boolean);
|
}
|
||||||
}, [downloadsTabActive, snapshot.session.packageOrder, snapshot.session.packages]);
|
if (shouldLimitPackageRendering) {
|
||||||
|
return snapshot.session.packageOrder.slice(0, AUTO_RENDER_PACKAGE_LIMIT);
|
||||||
|
}
|
||||||
|
return snapshot.session.packageOrder;
|
||||||
|
}, [downloadsTabActive, downloadSearchActive, shouldLimitPackageRendering, snapshot.session.packageOrder]);
|
||||||
|
|
||||||
const packageOrderKey = useMemo(() => {
|
const packageOrderKey = useMemo(() => {
|
||||||
if (!downloadsTabActive) {
|
if (!downloadsTabActive) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return snapshot.session.packageOrder.join("|");
|
return packageIdsForView.join("|");
|
||||||
}, [downloadsTabActive, snapshot.session.packageOrder]);
|
}, [downloadsTabActive, packageIdsForView]);
|
||||||
|
|
||||||
|
const packages = useMemo(() => {
|
||||||
|
if (!downloadsTabActive) {
|
||||||
|
return [] as PackageEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadSearchActive) {
|
||||||
|
return snapshot.session.packageOrder
|
||||||
|
.map((id: string) => snapshot.session.packages[id])
|
||||||
|
.filter((pkg): pkg is PackageEntry => Boolean(pkg) && pkg.name.toLowerCase().includes(downloadSearchQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageIdsForView
|
||||||
|
.map((id) => snapshot.session.packages[id])
|
||||||
|
.filter((pkg): pkg is PackageEntry => Boolean(pkg));
|
||||||
|
}, [downloadsTabActive, downloadSearchActive, downloadSearchQuery, packageIdsForView, snapshot.session.packageOrder, snapshot.session.packages]);
|
||||||
|
|
||||||
const packagePosition = useMemo(() => {
|
const packagePosition = useMemo(() => {
|
||||||
if (!downloadsTabActive) {
|
if (!downloadsTabActive) {
|
||||||
@ -238,18 +267,14 @@ export function App(): ReactElement {
|
|||||||
return new Map<string, DownloadItem[]>();
|
return new Map<string, DownloadItem[]>();
|
||||||
}
|
}
|
||||||
const map = new Map<string, DownloadItem[]>();
|
const map = new Map<string, DownloadItem[]>();
|
||||||
for (const packageId of snapshot.session.packageOrder) {
|
for (const pkg of packages) {
|
||||||
const pkg = snapshot.session.packages[packageId];
|
|
||||||
if (!pkg) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const items = pkg.itemIds
|
const items = pkg.itemIds
|
||||||
.map((id) => snapshot.session.items[id])
|
.map((id) => snapshot.session.items[id])
|
||||||
.filter(Boolean) as DownloadItem[];
|
.filter(Boolean) as DownloadItem[];
|
||||||
map.set(packageId, items);
|
map.set(pkg.id, items);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [downloadsTabActive, packageOrderKey, snapshot.session.items, snapshot.session.packages, snapshot.session.packageOrder]);
|
}, [downloadsTabActive, packageOrderKey, packages, snapshot.session.items]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!downloadsTabActive) {
|
if (!downloadsTabActive) {
|
||||||
@ -257,38 +282,18 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
setCollapsedPackages((prev) => {
|
setCollapsedPackages((prev) => {
|
||||||
const next: Record<string, boolean> = {};
|
const next: Record<string, boolean> = {};
|
||||||
const defaultCollapsed = snapshot.session.packageOrder.length >= 24;
|
const defaultCollapsed = totalPackageCount >= 24;
|
||||||
for (const packageId of snapshot.session.packageOrder) {
|
for (const packageId of packageIdsForView) {
|
||||||
next[packageId] = prev[packageId] ?? defaultCollapsed;
|
next[packageId] = prev[packageId] ?? defaultCollapsed;
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, [downloadsTabActive, packageOrderKey, snapshot.session.packageOrder.length]);
|
}, [downloadsTabActive, packageOrderKey, totalPackageCount, packageIdsForView]);
|
||||||
|
|
||||||
const deferredDownloadSearch = useDeferredValue(downloadSearch);
|
const hiddenPackageCount = shouldLimitPackageRendering
|
||||||
|
? Math.max(0, totalPackageCount - packages.length)
|
||||||
const filteredPackages = useMemo(() => {
|
: 0;
|
||||||
const query = deferredDownloadSearch.trim().toLowerCase();
|
const visiblePackages = packages;
|
||||||
if (!query) {
|
|
||||||
return packages;
|
|
||||||
}
|
|
||||||
return packages.filter((pkg) => pkg.name.toLowerCase().includes(query));
|
|
||||||
}, [packages, deferredDownloadSearch]);
|
|
||||||
|
|
||||||
const downloadSearchActive = deferredDownloadSearch.trim().length > 0;
|
|
||||||
const shouldLimitPackageRendering = snapshot.session.running
|
|
||||||
&& !downloadSearchActive
|
|
||||||
&& filteredPackages.length > AUTO_RENDER_PACKAGE_LIMIT
|
|
||||||
&& !showAllPackages;
|
|
||||||
|
|
||||||
const visiblePackages = useMemo(() => {
|
|
||||||
if (!shouldLimitPackageRendering) {
|
|
||||||
return filteredPackages;
|
|
||||||
}
|
|
||||||
return filteredPackages.slice(0, AUTO_RENDER_PACKAGE_LIMIT);
|
|
||||||
}, [filteredPackages, shouldLimitPackageRendering]);
|
|
||||||
|
|
||||||
const hiddenPackageCount = filteredPackages.length - visiblePackages.length;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!snapshot.session.running) {
|
if (!snapshot.session.running) {
|
||||||
@ -855,8 +860,8 @@ export function App(): ReactElement {
|
|||||||
<span>Dateien: {snapshot.stats.totalFiles} fertig</span>
|
<span>Dateien: {snapshot.stats.totalFiles} fertig</span>
|
||||||
<span>Gesamt: {humanSize(snapshot.stats.totalDownloaded)}</span>
|
<span>Gesamt: {humanSize(snapshot.stats.totalDownloaded)}</span>
|
||||||
</div>
|
</div>
|
||||||
{packages.length === 0 && <div className="empty">Noch keine Pakete in der Queue.</div>}
|
{totalPackageCount === 0 && <div className="empty">Noch keine Pakete in der Queue.</div>}
|
||||||
{packages.length > 0 && filteredPackages.length === 0 && <div className="empty">Keine Pakete passend zur Suche.</div>}
|
{totalPackageCount > 0 && packages.length === 0 && <div className="empty">Keine Pakete passend zur Suche.</div>}
|
||||||
{hiddenPackageCount > 0 && (
|
{hiddenPackageCount > 0 && (
|
||||||
<div className="reconnect-banner">
|
<div className="reconnect-banner">
|
||||||
Performance-Modus aktiv: {hiddenPackageCount} Paket(e) sind temporar ausgeblendet.
|
Performance-Modus aktiv: {hiddenPackageCount} Paket(e) sind temporar ausgeblendet.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user