Release v1.4.10 with freeze mitigation and extraction throughput fixes
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
306826ecb9
commit
6e72c63268
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.8",
|
"version": "1.4.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.8",
|
"version": "1.4.10",
|
||||||
"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.9",
|
"version": "1.4.10",
|
||||||
"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",
|
||||||
|
|||||||
@ -173,6 +173,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private speedBytesLastWindow = 0;
|
private speedBytesLastWindow = 0;
|
||||||
|
|
||||||
|
private statsCache: DownloadStats | null = null;
|
||||||
|
|
||||||
|
private statsCacheAt = 0;
|
||||||
|
|
||||||
private lastPersistAt = 0;
|
private lastPersistAt = 0;
|
||||||
|
|
||||||
private cleanupQueue: Promise<void> = Promise.resolve();
|
private cleanupQueue: Promise<void> = Promise.resolve();
|
||||||
@ -239,11 +243,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const paused = this.session.running && this.session.paused;
|
const paused = this.session.running && this.session.paused;
|
||||||
const speedBps = paused ? 0 : this.speedBytesLastWindow / 3;
|
const speedBps = paused ? 0 : this.speedBytesLastWindow / 3;
|
||||||
|
|
||||||
let totalItems = Object.keys(this.session.items).length;
|
let totalItems = 0;
|
||||||
let doneItems = Object.values(this.session.items).filter((item) => isFinishedStatus(item.status)).length;
|
let doneItems = 0;
|
||||||
if (this.session.running && this.runItemIds.size > 0) {
|
if (this.session.running && this.runItemIds.size > 0) {
|
||||||
totalItems = this.runItemIds.size;
|
totalItems = this.runItemIds.size;
|
||||||
doneItems = 0;
|
|
||||||
for (const itemId of this.runItemIds) {
|
for (const itemId of this.runItemIds) {
|
||||||
if (this.runOutcomes.has(itemId)) {
|
if (this.runOutcomes.has(itemId)) {
|
||||||
doneItems += 1;
|
doneItems += 1;
|
||||||
@ -254,6 +257,14 @@ export class DownloadManager extends EventEmitter {
|
|||||||
doneItems += 1;
|
doneItems += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const sessionItems = Object.values(this.session.items);
|
||||||
|
totalItems = sessionItems.length;
|
||||||
|
for (const item of sessionItems) {
|
||||||
|
if (isFinishedStatus(item.status)) {
|
||||||
|
doneItems += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const elapsed = this.session.runStartedAt > 0 ? (now - this.session.runStartedAt) / 1000 : 0;
|
const elapsed = this.session.runStartedAt > 0 ? (now - this.session.runStartedAt) / 1000 : 0;
|
||||||
const rate = doneItems > 0 && elapsed > 0 ? doneItems / elapsed : 0;
|
const rate = doneItems > 0 && elapsed > 0 ? doneItems / elapsed : 0;
|
||||||
@ -266,7 +277,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
settings: this.settings,
|
settings: this.settings,
|
||||||
session: this.session,
|
session: this.session,
|
||||||
summary: this.summary,
|
summary: this.summary,
|
||||||
stats: this.getStats(),
|
stats: this.getStats(now),
|
||||||
speedText: `Geschwindigkeit: ${humanSize(Math.max(0, Math.floor(speedBps)))}/s`,
|
speedText: `Geschwindigkeit: ${humanSize(Math.max(0, Math.floor(speedBps)))}/s`,
|
||||||
etaText: paused ? "ETA: --" : `ETA: ${formatEta(eta)}`,
|
etaText: paused ? "ETA: --" : `ETA: ${formatEta(eta)}`,
|
||||||
canStart: !this.session.running,
|
canStart: !this.session.running,
|
||||||
@ -277,7 +288,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStats(): DownloadStats {
|
public getStats(now = nowMs()): DownloadStats {
|
||||||
|
const itemCount = Object.keys(this.session.items).length;
|
||||||
|
if (this.statsCache && this.session.running && itemCount >= 500 && now - this.statsCacheAt < 1500) {
|
||||||
|
return this.statsCache;
|
||||||
|
}
|
||||||
|
|
||||||
let totalDownloaded = 0;
|
let totalDownloaded = 0;
|
||||||
let totalFiles = 0;
|
let totalFiles = 0;
|
||||||
for (const item of Object.values(this.session.items)) {
|
for (const item of Object.values(this.session.items)) {
|
||||||
@ -300,12 +316,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
totalDownloaded = Math.max(totalDownloaded, this.session.totalDownloadedBytes);
|
totalDownloaded = Math.max(totalDownloaded, this.session.totalDownloadedBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const stats = {
|
||||||
totalDownloaded,
|
totalDownloaded,
|
||||||
totalFiles,
|
totalFiles,
|
||||||
totalPackages: Object.keys(this.session.packages).length,
|
totalPackages: Object.keys(this.session.packages).length,
|
||||||
sessionStartedAt: this.session.runStartedAt
|
sessionStartedAt: this.session.runStartedAt
|
||||||
};
|
};
|
||||||
|
this.statsCache = stats;
|
||||||
|
this.statsCacheAt = now;
|
||||||
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
public renamePackage(packageId: string, newName: string): void {
|
public renamePackage(packageId: string, newName: string): void {
|
||||||
@ -771,6 +790,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanupTargetsByPackage = new Map<string, Set<string>>();
|
const cleanupTargetsByPackage = new Map<string, Set<string>>();
|
||||||
|
const dirFilesCache = new Map<string, string[]>();
|
||||||
for (const packageId of this.session.packageOrder) {
|
for (const packageId of this.session.packageOrder) {
|
||||||
const pkg = this.session.packages[packageId];
|
const pkg = this.session.packages[packageId];
|
||||||
if (!pkg || pkg.cancelled || pkg.status !== "completed") {
|
if (!pkg || pkg.cancelled || pkg.status !== "completed") {
|
||||||
@ -802,7 +822,20 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (!targetPath || !isArchiveLikePath(targetPath)) {
|
if (!targetPath || !isArchiveLikePath(targetPath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const cleanupTarget of collectArchiveCleanupTargets(targetPath)) {
|
const dir = path.dirname(targetPath);
|
||||||
|
let filesInDir = dirFilesCache.get(dir);
|
||||||
|
if (!filesInDir) {
|
||||||
|
try {
|
||||||
|
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isFile())
|
||||||
|
.map((entry) => entry.name);
|
||||||
|
} catch {
|
||||||
|
filesInDir = [];
|
||||||
|
}
|
||||||
|
dirFilesCache.set(dir, filesInDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cleanupTarget of collectArchiveCleanupTargets(targetPath, filesInDir)) {
|
||||||
packageTargets.add(cleanupTarget);
|
packageTargets.add(cleanupTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -860,8 +893,14 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (!rootDir || !fs.existsSync(rootDir)) {
|
if (!rootDir || !fs.existsSync(rootDir)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const deadline = nowMs() + 55;
|
||||||
|
let inspectedDirs = 0;
|
||||||
const stack = [rootDir];
|
const stack = [rootDir];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
|
inspectedDirs += 1;
|
||||||
|
if (inspectedDirs > 6000 || nowMs() > deadline) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const current = stack.pop() as string;
|
const current = stack.pop() as string;
|
||||||
let entries: fs.Dirent[] = [];
|
let entries: fs.Dirent[] = [];
|
||||||
try {
|
try {
|
||||||
@ -1215,13 +1254,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const itemCount = Object.keys(this.session.items).length;
|
const itemCount = Object.keys(this.session.items).length;
|
||||||
const minGapMs = this.session.running
|
const minGapMs = this.session.running
|
||||||
? itemCount >= 1500
|
? itemCount >= 1500
|
||||||
? 1300
|
? 3000
|
||||||
: itemCount >= 700
|
: itemCount >= 700
|
||||||
? 950
|
? 2200
|
||||||
: itemCount >= 250
|
: itemCount >= 250
|
||||||
? 700
|
? 1500
|
||||||
: 450
|
: 700
|
||||||
: 250;
|
: 300;
|
||||||
const sinceLastPersist = nowMs() - this.lastPersistAt;
|
const sinceLastPersist = nowMs() - this.lastPersistAt;
|
||||||
const delay = Math.max(120, minGapMs - sinceLastPersist);
|
const delay = Math.max(120, minGapMs - sinceLastPersist);
|
||||||
|
|
||||||
@ -1251,12 +1290,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const itemCount = Object.keys(this.session.items).length;
|
const itemCount = Object.keys(this.session.items).length;
|
||||||
const emitDelay = this.session.running
|
const emitDelay = this.session.running
|
||||||
? itemCount >= 1500
|
? itemCount >= 1500
|
||||||
? 900
|
? 1200
|
||||||
: itemCount >= 700
|
: itemCount >= 700
|
||||||
? 650
|
? 900
|
||||||
: itemCount >= 250
|
: itemCount >= 250
|
||||||
? 420
|
? 560
|
||||||
: 280
|
: 320
|
||||||
: 260;
|
: 260;
|
||||||
this.stateEmitTimer = setTimeout(() => {
|
this.stateEmitTimer = setTimeout(() => {
|
||||||
this.stateEmitTimer = null;
|
this.stateEmitTimer = null;
|
||||||
@ -2568,6 +2607,35 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
logger.info(`Post-Processing Ende: pkg=${pkg.name}, status=${pkg.status}`);
|
logger.info(`Post-Processing Ende: pkg=${pkg.name}, status=${pkg.status}`);
|
||||||
|
|
||||||
|
this.applyPackageDoneCleanup(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyPackageDoneCleanup(packageId: string): void {
|
||||||
|
if (this.settings.completedCleanupPolicy !== "package_done") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = this.session.packages[packageId];
|
||||||
|
if (!pkg || pkg.status !== "completed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allCompleted = pkg.itemIds.every((itemId) => {
|
||||||
|
const item = this.session.items[itemId];
|
||||||
|
return !item || item.status === "completed";
|
||||||
|
});
|
||||||
|
if (!allCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const itemId of pkg.itemIds) {
|
||||||
|
delete this.session.items[itemId];
|
||||||
|
}
|
||||||
|
delete this.session.packages[packageId];
|
||||||
|
this.session.packageOrder = this.session.packageOrder.filter((id) => id !== packageId);
|
||||||
|
this.runPackageIds.delete(packageId);
|
||||||
|
this.runCompletedPackages.delete(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyCompletedCleanupPolicy(packageId: string, itemId: string): void {
|
private applyCompletedCleanupPolicy(packageId: string, itemId: string): void {
|
||||||
|
|||||||
@ -163,6 +163,21 @@ function archivePasswords(listInput: string): string[] {
|
|||||||
return Array.from(new Set(["", ...custom, ...fromEnv, ...DEFAULT_ARCHIVE_PASSWORDS]));
|
return Array.from(new Set(["", ...custom, ...fromEnv, ...DEFAULT_ARCHIVE_PASSWORDS]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prioritizePassword(passwords: string[], successful: string): string[] {
|
||||||
|
const target = String(successful || "");
|
||||||
|
if (!target || passwords.length <= 1) {
|
||||||
|
return passwords;
|
||||||
|
}
|
||||||
|
const index = passwords.findIndex((candidate) => candidate === target);
|
||||||
|
if (index <= 0) {
|
||||||
|
return passwords;
|
||||||
|
}
|
||||||
|
const next = [...passwords];
|
||||||
|
const [value] = next.splice(index, 1);
|
||||||
|
next.unshift(value);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
function winRarCandidates(): string[] {
|
function winRarCandidates(): string[] {
|
||||||
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
|
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
|
||||||
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
||||||
@ -334,7 +349,7 @@ async function runExternalExtract(
|
|||||||
passwordCandidates: string[],
|
passwordCandidates: string[],
|
||||||
onArchiveProgress?: (percent: number) => void,
|
onArchiveProgress?: (percent: number) => void,
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
): Promise<void> {
|
): Promise<string> {
|
||||||
const command = await resolveExtractorCommand();
|
const command = await resolveExtractorCommand();
|
||||||
const passwords = passwordCandidates;
|
const passwords = passwordCandidates;
|
||||||
let lastError = "";
|
let lastError = "";
|
||||||
@ -363,7 +378,7 @@ async function runExternalExtract(
|
|||||||
}, signal);
|
}, signal);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
onArchiveProgress?.(100);
|
onArchiveProgress?.(100);
|
||||||
return;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.aborted) {
|
if (result.aborted) {
|
||||||
@ -417,18 +432,20 @@ function escapeRegex(value: string): string {
|
|||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectArchiveCleanupTargets(sourceArchivePath: string): string[] {
|
export function collectArchiveCleanupTargets(sourceArchivePath: string, directoryFiles?: string[]): string[] {
|
||||||
const targets = new Set<string>([sourceArchivePath]);
|
const targets = new Set<string>([sourceArchivePath]);
|
||||||
const dir = path.dirname(sourceArchivePath);
|
const dir = path.dirname(sourceArchivePath);
|
||||||
const fileName = path.basename(sourceArchivePath);
|
const fileName = path.basename(sourceArchivePath);
|
||||||
|
|
||||||
let filesInDir: string[] = [];
|
let filesInDir: string[] = directoryFiles ?? [];
|
||||||
try {
|
if (!directoryFiles) {
|
||||||
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
|
try {
|
||||||
.filter((entry) => entry.isFile())
|
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
.map((entry) => entry.name);
|
.filter((entry) => entry.isFile())
|
||||||
} catch {
|
.map((entry) => entry.name);
|
||||||
return Array.from(targets);
|
} catch {
|
||||||
|
return Array.from(targets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addMatching = (pattern: RegExp): void => {
|
const addMatching = (pattern: RegExp): void => {
|
||||||
@ -476,8 +493,22 @@ function cleanupArchives(sourceFiles: string[], cleanupMode: CleanupMode): numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targets = new Set<string>();
|
const targets = new Set<string>();
|
||||||
|
const dirFilesCache = new Map<string, string[]>();
|
||||||
for (const sourceFile of sourceFiles) {
|
for (const sourceFile of sourceFiles) {
|
||||||
for (const target of collectArchiveCleanupTargets(sourceFile)) {
|
const dir = path.dirname(sourceFile);
|
||||||
|
let filesInDir = dirFilesCache.get(dir);
|
||||||
|
if (!filesInDir) {
|
||||||
|
try {
|
||||||
|
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isFile())
|
||||||
|
.map((entry) => entry.name);
|
||||||
|
} catch {
|
||||||
|
filesInDir = [];
|
||||||
|
}
|
||||||
|
dirFilesCache.set(dir, filesInDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of collectArchiveCleanupTargets(sourceFile, filesInDir)) {
|
||||||
targets.add(target);
|
targets.add(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -501,8 +532,14 @@ function hasAnyFilesRecursive(rootDir: string): boolean {
|
|||||||
if (!fs.existsSync(rootDir)) {
|
if (!fs.existsSync(rootDir)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const deadline = Date.now() + 70;
|
||||||
|
let inspectedDirs = 0;
|
||||||
const stack = [rootDir];
|
const stack = [rootDir];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
|
inspectedDirs += 1;
|
||||||
|
if (inspectedDirs > 8000 || Date.now() > deadline) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const current = stack.pop() as string;
|
const current = stack.pop() as string;
|
||||||
let entries: fs.Dirent[] = [];
|
let entries: fs.Dirent[] = [];
|
||||||
try {
|
try {
|
||||||
@ -523,6 +560,17 @@ function hasAnyFilesRecursive(rootDir: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasAnyEntries(rootDir: string): boolean {
|
||||||
|
if (!rootDir || !fs.existsSync(rootDir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return fs.readdirSync(rootDir).length > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function removeEmptyDirectoryTree(rootDir: string): number {
|
function removeEmptyDirectoryTree(rootDir: string): number {
|
||||||
if (!fs.existsSync(rootDir)) {
|
if (!fs.existsSync(rootDir)) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -572,7 +620,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
logger.info(`Entpacken gestartet: packageDir=${options.packageDir}, targetDir=${options.targetDir}, archives=${candidates.length}, cleanupMode=${options.cleanupMode}, conflictMode=${options.conflictMode}`);
|
logger.info(`Entpacken gestartet: packageDir=${options.packageDir}, targetDir=${options.targetDir}, archives=${candidates.length}, cleanupMode=${options.cleanupMode}, conflictMode=${options.conflictMode}`);
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
const existingResume = readExtractResumeState(options.packageDir);
|
const existingResume = readExtractResumeState(options.packageDir);
|
||||||
if (existingResume.size > 0 && hasAnyFilesRecursive(options.targetDir)) {
|
if (existingResume.size > 0 && hasAnyEntries(options.targetDir)) {
|
||||||
clearExtractResumeState(options.packageDir);
|
clearExtractResumeState(options.packageDir);
|
||||||
logger.info(`Entpacken übersprungen (Archive bereinigt, Ziel hat Dateien): ${options.packageDir}`);
|
logger.info(`Entpacken übersprungen (Archive bereinigt, Ziel hat Dateien): ${options.packageDir}`);
|
||||||
options.onProgress?.({
|
options.onProgress?.({
|
||||||
@ -590,7 +638,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const conflictMode = effectiveConflictMode(options.conflictMode);
|
const conflictMode = effectiveConflictMode(options.conflictMode);
|
||||||
const passwordCandidates = archivePasswords(options.passwordList || "");
|
let passwordCandidates = archivePasswords(options.passwordList || "");
|
||||||
const resumeCompleted = readExtractResumeState(options.packageDir);
|
const resumeCompleted = readExtractResumeState(options.packageDir);
|
||||||
const resumeCompletedAtStart = resumeCompleted.size;
|
const resumeCompletedAtStart = resumeCompleted.size;
|
||||||
const candidateNames = new Set(candidates.map((archivePath) => path.basename(archivePath)));
|
const candidateNames = new Set(candidates.map((archivePath) => path.basename(archivePath)));
|
||||||
@ -664,10 +712,11 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
const preferExternal = shouldPreferExternalZip(archivePath);
|
const preferExternal = shouldPreferExternalZip(archivePath);
|
||||||
if (preferExternal) {
|
if (preferExternal) {
|
||||||
try {
|
try {
|
||||||
await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
|
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
|
||||||
archivePercent = Math.max(archivePercent, value);
|
archivePercent = Math.max(archivePercent, value);
|
||||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||||
}, options.signal);
|
}, options.signal);
|
||||||
|
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isNoExtractorError(String(error))) {
|
if (isNoExtractorError(String(error))) {
|
||||||
extractZipArchive(archivePath, options.targetDir, options.conflictMode);
|
extractZipArchive(archivePath, options.targetDir, options.conflictMode);
|
||||||
@ -680,17 +729,19 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
extractZipArchive(archivePath, options.targetDir, options.conflictMode);
|
extractZipArchive(archivePath, options.targetDir, options.conflictMode);
|
||||||
archivePercent = 100;
|
archivePercent = 100;
|
||||||
} catch {
|
} catch {
|
||||||
await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
|
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
|
||||||
archivePercent = Math.max(archivePercent, value);
|
archivePercent = Math.max(archivePercent, value);
|
||||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||||
}, options.signal);
|
}, options.signal);
|
||||||
|
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
|
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
|
||||||
archivePercent = Math.max(archivePercent, value);
|
archivePercent = Math.max(archivePercent, value);
|
||||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||||
}, options.signal);
|
}, options.signal);
|
||||||
|
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||||
}
|
}
|
||||||
extracted += 1;
|
extracted += 1;
|
||||||
extractedArchives.add(archivePath);
|
extractedArchives.add(archivePath);
|
||||||
@ -722,7 +773,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (extracted > 0) {
|
if (extracted > 0) {
|
||||||
const hasOutputAfter = hasAnyFilesRecursive(options.targetDir);
|
const hasOutputAfter = hasAnyEntries(options.targetDir);
|
||||||
const hadResumeProgress = resumeCompletedAtStart > 0;
|
const hadResumeProgress = resumeCompletedAtStart > 0;
|
||||||
if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) {
|
if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) {
|
||||||
lastError = "Keine entpackten Dateien erkannt";
|
lastError = "Keine entpackten Dateien erkannt";
|
||||||
|
|||||||
@ -61,6 +61,8 @@ const cleanupLabels: Record<string, string> = {
|
|||||||
never: "Nie", immediate: "Sofort", on_start: "Beim App-Start", package_done: "Sobald Paket fertig ist"
|
never: "Nie", immediate: "Sofort", on_start: "Beim App-Start", package_done: "Sobald Paket fertig ist"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AUTO_RENDER_PACKAGE_LIMIT = 260;
|
||||||
|
|
||||||
const providerLabels: Record<DebridProvider, string> = {
|
const providerLabels: Record<DebridProvider, string> = {
|
||||||
realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid"
|
realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid"
|
||||||
};
|
};
|
||||||
@ -101,6 +103,7 @@ export function App(): ReactElement {
|
|||||||
const draggedPackageIdRef = useRef<string | null>(null);
|
const draggedPackageIdRef = useRef<string | null>(null);
|
||||||
const [collapsedPackages, setCollapsedPackages] = useState<Record<string, boolean>>({});
|
const [collapsedPackages, setCollapsedPackages] = useState<Record<string, boolean>>({});
|
||||||
const [downloadSearch, setDownloadSearch] = useState("");
|
const [downloadSearch, setDownloadSearch] = useState("");
|
||||||
|
const [showAllPackages, setShowAllPackages] = useState(false);
|
||||||
const [actionBusy, setActionBusy] = useState(false);
|
const [actionBusy, setActionBusy] = useState(false);
|
||||||
const actionBusyRef = useRef(false);
|
const actionBusyRef = useRef(false);
|
||||||
const dragOverRef = useRef(false);
|
const dragOverRef = useRef(false);
|
||||||
@ -272,6 +275,27 @@ export function App(): ReactElement {
|
|||||||
return packages.filter((pkg) => pkg.name.toLowerCase().includes(query));
|
return packages.filter((pkg) => pkg.name.toLowerCase().includes(query));
|
||||||
}, [packages, deferredDownloadSearch]);
|
}, [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(() => {
|
||||||
|
if (!snapshot.session.running) {
|
||||||
|
setShowAllPackages(false);
|
||||||
|
}
|
||||||
|
}, [snapshot.session.running]);
|
||||||
|
|
||||||
const allPackagesCollapsed = useMemo(() => (
|
const allPackagesCollapsed = useMemo(() => (
|
||||||
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
|
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
|
||||||
), [packages, collapsedPackages]);
|
), [packages, collapsedPackages]);
|
||||||
@ -833,7 +857,13 @@ export function App(): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
{packages.length === 0 && <div className="empty">Noch keine Pakete in der Queue.</div>}
|
{packages.length === 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>}
|
{packages.length > 0 && filteredPackages.length === 0 && <div className="empty">Keine Pakete passend zur Suche.</div>}
|
||||||
{filteredPackages.map((pkg) => (
|
{hiddenPackageCount > 0 && (
|
||||||
|
<div className="reconnect-banner">
|
||||||
|
Performance-Modus aktiv: {hiddenPackageCount} Paket(e) sind temporar ausgeblendet.
|
||||||
|
<button className="btn" onClick={() => setShowAllPackages(true)}>Alle trotzdem anzeigen</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{visiblePackages.map((pkg) => (
|
||||||
<PackageCard
|
<PackageCard
|
||||||
key={pkg.id}
|
key={pkg.id}
|
||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
|
|||||||
@ -2297,6 +2297,85 @@ describe("download manager", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes finished package when package_done cleanup policy is enabled", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
const zip = new AdmZip();
|
||||||
|
zip.addFile("episode.txt", Buffer.from("ok"));
|
||||||
|
const archiveBinary = zip.toBuffer();
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if ((req.url || "") !== "/cleanup-package") {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end("not-found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader("Accept-Ranges", "bytes");
|
||||||
|
res.setHeader("Content-Length", String(archiveBinary.length));
|
||||||
|
res.end(archiveBinary);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(0, "127.0.0.1");
|
||||||
|
await once(server, "listening");
|
||||||
|
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
throw new Error("server address unavailable");
|
||||||
|
}
|
||||||
|
const directUrl = `http://127.0.0.1:${address.port}/cleanup-package`;
|
||||||
|
|
||||||
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||||
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||||
|
if (url.includes("/unrestrict/link")) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
download: directUrl,
|
||||||
|
filename: "cleanup-package.zip",
|
||||||
|
filesize: archiveBinary.length
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return originalFetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "rd-token",
|
||||||
|
outputDir: path.join(root, "downloads"),
|
||||||
|
extractDir: path.join(root, "extract"),
|
||||||
|
autoExtract: true,
|
||||||
|
enableIntegrityCheck: false,
|
||||||
|
cleanupMode: "none",
|
||||||
|
completedCleanupPolicy: "package_done"
|
||||||
|
},
|
||||||
|
emptySession(),
|
||||||
|
createStoragePaths(path.join(root, "state"))
|
||||||
|
);
|
||||||
|
|
||||||
|
manager.addPackages([{ name: "cleanup-package", links: ["https://dummy/cleanup-package"] }]);
|
||||||
|
manager.start();
|
||||||
|
await waitFor(() => !manager.getSnapshot().session.running, 30000);
|
||||||
|
|
||||||
|
const snapshot = manager.getSnapshot();
|
||||||
|
const summary = manager.getSummary();
|
||||||
|
expect(snapshot.session.packageOrder).toHaveLength(0);
|
||||||
|
expect(Object.keys(snapshot.session.items)).toHaveLength(0);
|
||||||
|
expect(summary).not.toBeNull();
|
||||||
|
expect(summary?.success).toBe(1);
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
await once(server, "close");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("counts queued package cancellations in run summary", async () => {
|
it("counts queued package cancellations in run summary", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user