Compare commits
No commits in common. "main" and "v1.6.55" have entirely different histories.
31
README.md
31
README.md
@ -160,7 +160,7 @@ The app stores runtime files in Electron's `userData` directory, including:
|
||||
## Troubleshooting
|
||||
|
||||
- Download does not start: verify token and selected provider in Settings.
|
||||
- Extraction fails: check archive passwords and native extractor installation (7-Zip/WinRAR). Optional JVM extractor can be forced with `RD_EXTRACT_BACKEND=jvm`.
|
||||
- Extraction fails: check archive passwords, JVM runtime (`resources/extractor-jvm`), or force legacy mode with `RD_EXTRACT_BACKEND=legacy`.
|
||||
- Very slow downloads: check active speed limit and bandwidth schedules.
|
||||
- Unexpected interruptions: enable reconnect and fallback providers.
|
||||
- Stalled downloads: the app auto-detects stalls within 10 seconds and retries automatically.
|
||||
@ -169,35 +169,6 @@ The app stores runtime files in Electron's `userData` directory, including:
|
||||
|
||||
Release history is available on [git.24-music.de Releases](https://git.24-music.de/Administrator/real-debrid-downloader/releases).
|
||||
|
||||
### v1.6.61 (2026-03-05)
|
||||
|
||||
- Fixed leftover empty package folders in `Downloader Unfertig` after successful extraction.
|
||||
- Resume marker files (`.rd_extract_progress*.json`) are now treated as ignorable for empty-folder cleanup.
|
||||
- Deferred post-processing now clears resume markers before running empty-directory removal.
|
||||
|
||||
### v1.6.60 (2026-03-05)
|
||||
|
||||
- Added package-scoped password cache for extraction: once the first archive in a package is solved, following archives in the same package reuse that password first.
|
||||
- Kept fallback behavior intact (`""` and other candidates are still tested), but moved empty-password probing behind the learned password to reduce per-archive delays.
|
||||
- Added cache invalidation on real `wrong_password` failures so stale passwords are automatically discarded.
|
||||
|
||||
### v1.6.59 (2026-03-05)
|
||||
|
||||
- Switched default extraction backend to native tools (`legacy`) for more stable archive-to-archive flow.
|
||||
- Prioritized 7-Zip as primary native extractor, with WinRAR/UnRAR as fallback.
|
||||
- JVM extractor remains available as opt-in via `RD_EXTRACT_BACKEND=jvm`.
|
||||
|
||||
### v1.6.58 (2026-03-05)
|
||||
|
||||
- Fixed extraction progress oscillation (`1% -> 100% -> 1%` loops) during password retries.
|
||||
- Kept strict archive completion logic, but normalized in-progress archive percent to avoid false visual done states before real completion.
|
||||
|
||||
### v1.6.57 (2026-03-05)
|
||||
|
||||
- Fixed extraction flow so archives are marked done only on real completion, not on temporary `100%` progress spikes.
|
||||
- Improved password handling: after the first successful archive, the discovered password is prioritized for subsequent archives.
|
||||
- Fixed progress parsing for password retries (reset/restart handling), reducing visible and real gaps between archive extractions.
|
||||
|
||||
## License
|
||||
|
||||
MIT - see `LICENSE`.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.6.66",
|
||||
"version": "1.6.55",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -2,15 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const NPM_RELEASE_WIN = process.platform === "win32"
|
||||
? {
|
||||
command: process.env.ComSpec || "cmd.exe",
|
||||
args: ["/d", "/s", "/c", "npm run release:win"]
|
||||
}
|
||||
: {
|
||||
command: "npm",
|
||||
args: ["run", "release:win"]
|
||||
};
|
||||
const NPM_EXECUTABLE = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
@ -333,7 +325,7 @@ async function main() {
|
||||
updatePackageVersion(rootDir, version);
|
||||
|
||||
process.stdout.write(`Building release artifacts for ${tag}...\n`);
|
||||
run(NPM_RELEASE_WIN.command, NPM_RELEASE_WIN.args);
|
||||
run(NPM_EXECUTABLE, ["run", "release:win"]);
|
||||
const assets = ensureAssetsExist(rootDir, version);
|
||||
|
||||
run("git", ["add", "package.json"]);
|
||||
|
||||
@ -106,7 +106,6 @@ export class AppController {
|
||||
|| settings.bestToken.trim()
|
||||
|| settings.allDebridToken.trim()
|
||||
|| (settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim())
|
||||
|| settings.oneFichierApiKey.trim()
|
||||
);
|
||||
}
|
||||
|
||||
@ -287,7 +286,7 @@ export class AppController {
|
||||
|
||||
public exportBackup(): string {
|
||||
const settings = { ...this.settings };
|
||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword", "oneFichierApiKey"];
|
||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"];
|
||||
for (const key of SENSITIVE_KEYS) {
|
||||
const val = settings[key];
|
||||
if (typeof val === "string" && val.length > 0) {
|
||||
@ -309,7 +308,7 @@ export class AppController {
|
||||
return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" };
|
||||
}
|
||||
const importedSettings = parsed.settings as AppSettings;
|
||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword", "oneFichierApiKey"];
|
||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"];
|
||||
for (const key of SENSITIVE_KEYS) {
|
||||
const val = (importedSettings as Record<string, unknown>)[key];
|
||||
if (typeof val === "string" && val.startsWith("***")) {
|
||||
|
||||
@ -47,7 +47,6 @@ export function defaultSettings(): AppSettings {
|
||||
allDebridToken: "",
|
||||
ddownloadLogin: "",
|
||||
ddownloadPassword: "",
|
||||
oneFichierApiKey: "",
|
||||
archivePasswordList: "",
|
||||
rememberToken: true,
|
||||
providerPrimary: "realdebrid",
|
||||
|
||||
@ -11,16 +11,12 @@ const RAPIDGATOR_SCAN_MAX_BYTES = 512 * 1024;
|
||||
const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1";
|
||||
const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4";
|
||||
|
||||
const ONEFICHIER_API_BASE = "https://api.1fichier.com/v1";
|
||||
const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.com|cjoint\.net|desfichiers\.com|dfichiers\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com|dl4free\.com)\/\?([a-z0-9]{5,20})$/i;
|
||||
|
||||
const PROVIDER_LABELS: Record<DebridProvider, string> = {
|
||||
realdebrid: "Real-Debrid",
|
||||
megadebrid: "Mega-Debrid",
|
||||
bestdebrid: "BestDebrid",
|
||||
alldebrid: "AllDebrid",
|
||||
ddownload: "DDownload",
|
||||
onefichier: "1Fichier"
|
||||
ddownload: "DDownload"
|
||||
};
|
||||
|
||||
interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
||||
@ -963,66 +959,6 @@ class AllDebridClient {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1Fichier Client ──
|
||||
|
||||
class OneFichierClient {
|
||||
private apiKey: string;
|
||||
|
||||
public constructor(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
|
||||
if (!ONEFICHIER_URL_RE.test(link)) {
|
||||
throw new Error("Kein 1Fichier-Link");
|
||||
}
|
||||
|
||||
let lastError = "";
|
||||
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
|
||||
if (signal?.aborted) throw new Error("aborted:debrid");
|
||||
try {
|
||||
const res = await fetch(`${ONEFICHIER_API_BASE}/download/get_token.cgi`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`
|
||||
},
|
||||
body: JSON.stringify({ url: link, pretty: 1 }),
|
||||
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
|
||||
});
|
||||
|
||||
const json = await res.json() as Record<string, unknown>;
|
||||
|
||||
if (json.status === "KO" || json.error) {
|
||||
const msg = String(json.message || json.error || "Unbekannter 1Fichier-Fehler");
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
const directUrl = String(json.url || "");
|
||||
if (!directUrl) {
|
||||
throw new Error("1Fichier: Keine Download-URL in Antwort");
|
||||
}
|
||||
|
||||
return {
|
||||
fileName: filenameFromUrl(directUrl) || filenameFromUrl(link),
|
||||
directUrl,
|
||||
fileSize: null,
|
||||
retriesUsed: attempt - 1
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = compactErrorText(error);
|
||||
if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) {
|
||||
throw error;
|
||||
}
|
||||
if (attempt < REQUEST_RETRIES) {
|
||||
await sleep(retryDelay(attempt), signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`1Fichier-Unrestrict fehlgeschlagen: ${lastError}`);
|
||||
}
|
||||
}
|
||||
|
||||
const DDOWNLOAD_URL_RE = /^https?:\/\/(?:www\.)?(?:ddownload\.com|ddl\.to)\/([a-z0-9]+)/i;
|
||||
const DDOWNLOAD_WEB_BASE = "https://ddownload.com";
|
||||
const DDOWNLOAD_WEB_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36";
|
||||
@ -1293,25 +1229,6 @@ export class DebridService {
|
||||
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise<ProviderUnrestrictedLink> {
|
||||
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
|
||||
|
||||
// 1Fichier is a direct file hoster. If the link is a 1fichier.com URL
|
||||
// and the API key is configured, use 1Fichier directly before debrid providers.
|
||||
if (ONEFICHIER_URL_RE.test(link) && this.isProviderConfiguredFor(settings, "onefichier")) {
|
||||
try {
|
||||
const result = await this.unrestrictViaProvider(settings, "onefichier", link, signal);
|
||||
return {
|
||||
...result,
|
||||
provider: "onefichier",
|
||||
providerLabel: PROVIDER_LABELS["onefichier"]
|
||||
};
|
||||
} catch (error) {
|
||||
const errorText = compactErrorText(error);
|
||||
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||
throw error;
|
||||
}
|
||||
// Fall through to normal provider chain
|
||||
}
|
||||
}
|
||||
|
||||
// DDownload is a direct file hoster, not a debrid service.
|
||||
// If the link is a ddownload.com/ddl.to URL and the account is configured,
|
||||
// use DDownload directly before trying any debrid providers.
|
||||
@ -1420,9 +1337,6 @@ export class DebridService {
|
||||
if (provider === "ddownload") {
|
||||
return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim());
|
||||
}
|
||||
if (provider === "onefichier") {
|
||||
return Boolean(settings.oneFichierApiKey.trim());
|
||||
}
|
||||
return Boolean(settings.bestToken.trim());
|
||||
}
|
||||
|
||||
@ -1439,9 +1353,6 @@ export class DebridService {
|
||||
if (provider === "ddownload") {
|
||||
return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal);
|
||||
}
|
||||
if (provider === "onefichier") {
|
||||
return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal);
|
||||
}
|
||||
return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -334,11 +334,9 @@ const EMPTY_DIR_IGNORED_FILE_NAMES = new Set([
|
||||
"desktop.ini",
|
||||
".ds_store"
|
||||
]);
|
||||
const EMPTY_DIR_IGNORED_FILE_RE = /^\.rd_extract_progress(?:_[^.\\/]+)?\.json$/i;
|
||||
|
||||
function isIgnorableEmptyDirFileName(fileName: string): boolean {
|
||||
const normalized = String(fileName || "").trim().toLowerCase();
|
||||
return EMPTY_DIR_IGNORED_FILE_NAMES.has(normalized) || EMPTY_DIR_IGNORED_FILE_RE.test(normalized);
|
||||
return EMPTY_DIR_IGNORED_FILE_NAMES.has(String(fileName || "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
function toWindowsLongPathIfNeeded(filePath: string): string {
|
||||
@ -6309,6 +6307,19 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
if (readyArchives.size === 0) {
|
||||
logger.info(`Hybrid-Extract: pkg=${pkg.name}, keine fertigen Archive-Sets`);
|
||||
// Relabel completed items that are part of incomplete multi-part archives
|
||||
// from "Ausstehend" to "Warten auf Parts" so the UI accurately reflects
|
||||
// that extraction is waiting for remaining parts to finish downloading.
|
||||
const allDone = items.every((i) => i.status === "completed" || i.status === "failed" || i.status === "cancelled");
|
||||
if (!allDone) {
|
||||
for (const entry of items) {
|
||||
if (entry.status === "completed" && entry.fullStatus === "Entpacken - Ausstehend") {
|
||||
entry.fullStatus = "Entpacken - Warten auf Parts";
|
||||
entry.updatedAt = nowMs();
|
||||
}
|
||||
}
|
||||
this.emitState();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -6385,30 +6396,27 @@ export class DownloadManager extends EventEmitter {
|
||||
const hybridResolvedItems = new Map<string, DownloadItem[]>();
|
||||
const hybridStartTimes = new Map<string, number>();
|
||||
let hybridLastEmitAt = 0;
|
||||
let hybridLastProgressCurrent: number | null = null;
|
||||
|
||||
// Mark items based on whether their archive is actually ready for extraction.
|
||||
// Only items whose archive is in readyArchives get "Ausstehend"; others keep
|
||||
// their current label to avoid flicker between hybrid runs.
|
||||
// "Warten auf Parts" to avoid flicker between hybrid runs.
|
||||
const allDownloaded = completedItems.length >= items.length;
|
||||
let labelsChanged = false;
|
||||
for (const entry of completedItems) {
|
||||
if (isExtractedLabel(entry.fullStatus)) {
|
||||
continue;
|
||||
}
|
||||
const belongsToReady = allDownloaded
|
||||
|| hybridFileNames.has((entry.fileName || "").toLowerCase())
|
||||
|| (entry.targetPath && hybridFileNames.has(path.basename(entry.targetPath).toLowerCase()));
|
||||
const targetLabel = belongsToReady ? "Entpacken - Ausstehend" : "Entpacken - Warten auf Parts";
|
||||
if (entry.fullStatus !== targetLabel) {
|
||||
entry.fullStatus = targetLabel;
|
||||
if (allDownloaded) {
|
||||
// Everything downloaded — all remaining items will be extracted
|
||||
entry.fullStatus = "Entpacken - Ausstehend";
|
||||
} else if (hybridFileNames.has((entry.fileName || "").toLowerCase()) ||
|
||||
(entry.targetPath && hybridFileNames.has(path.basename(entry.targetPath).toLowerCase()))) {
|
||||
entry.fullStatus = "Entpacken - Ausstehend";
|
||||
} else {
|
||||
entry.fullStatus = "Entpacken - Warten auf Parts";
|
||||
}
|
||||
entry.updatedAt = nowMs();
|
||||
labelsChanged = true;
|
||||
}
|
||||
}
|
||||
if (labelsChanged) {
|
||||
this.emitState();
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await extractPackageArchives({
|
||||
@ -6435,15 +6443,9 @@ export class DownloadManager extends EventEmitter {
|
||||
if (progress.phase === "done") {
|
||||
hybridResolvedItems.clear();
|
||||
hybridStartTimes.clear();
|
||||
hybridLastProgressCurrent = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCount = Math.max(0, Number(progress.current ?? 0));
|
||||
const archiveFinished = progress.archiveDone === true
|
||||
|| (hybridLastProgressCurrent !== null && currentCount > hybridLastProgressCurrent);
|
||||
hybridLastProgressCurrent = currentCount;
|
||||
|
||||
if (progress.archiveName) {
|
||||
// Resolve items for this archive if not yet tracked
|
||||
if (!hybridResolvedItems.has(progress.archiveName)) {
|
||||
@ -6468,14 +6470,11 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
const archItems = hybridResolvedItems.get(progress.archiveName) || [];
|
||||
|
||||
// Only mark as finished on explicit archive-done signal (or real current increment),
|
||||
// never on raw 100% archivePercent, because password retries can report 100% mid-run.
|
||||
if (archiveFinished) {
|
||||
// If archive is at 100%, mark its items as done and remove from active
|
||||
if (Number(progress.archivePercent ?? 0) >= 100) {
|
||||
const doneAt = nowMs();
|
||||
const startedAt = hybridStartTimes.get(progress.archiveName) || doneAt;
|
||||
const doneLabel = progress.archiveSuccess === false
|
||||
? "Entpacken - Error"
|
||||
: formatExtractDone(doneAt - startedAt);
|
||||
const doneLabel = formatExtractDone(doneAt - startedAt);
|
||||
for (const entry of archItems) {
|
||||
if (!isExtractedLabel(entry.fullStatus)) {
|
||||
entry.fullStatus = doneLabel;
|
||||
@ -6485,7 +6484,7 @@ export class DownloadManager extends EventEmitter {
|
||||
hybridResolvedItems.delete(progress.archiveName);
|
||||
hybridStartTimes.delete(progress.archiveName);
|
||||
// Show transitional label while next archive initializes
|
||||
const done = currentCount;
|
||||
const done = progress.current + 1;
|
||||
if (done < progress.total) {
|
||||
pkg.postProcessLabel = `Entpacken (${done}/${progress.total}) - Naechstes Archiv...`;
|
||||
this.emitState();
|
||||
@ -6517,7 +6516,7 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
|
||||
// Update package-level label with overall extraction progress
|
||||
const activeArchive = !archiveFinished && Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
|
||||
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
|
||||
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
|
||||
if (progress.passwordFound) {
|
||||
pkg.postProcessLabel = `Passwort gefunden · ${progress.archiveName || ""}`;
|
||||
@ -6778,7 +6777,6 @@ export class DownloadManager extends EventEmitter {
|
||||
// Track archives for parallel extraction progress
|
||||
const fullResolvedItems = new Map<string, DownloadItem[]>();
|
||||
const fullStartTimes = new Map<string, number>();
|
||||
let fullLastProgressCurrent: number | null = null;
|
||||
|
||||
const result = await extractPackageArchives({
|
||||
packageDir: pkg.outputDir,
|
||||
@ -6804,16 +6802,10 @@ export class DownloadManager extends EventEmitter {
|
||||
if (progress.phase === "done") {
|
||||
fullResolvedItems.clear();
|
||||
fullStartTimes.clear();
|
||||
fullLastProgressCurrent = null;
|
||||
emitExtractStatus("Entpacken 100%", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCount = Math.max(0, Number(progress.current ?? 0));
|
||||
const archiveFinished = progress.archiveDone === true
|
||||
|| (fullLastProgressCurrent !== null && currentCount > fullLastProgressCurrent);
|
||||
fullLastProgressCurrent = currentCount;
|
||||
|
||||
if (progress.archiveName) {
|
||||
// Resolve items for this archive if not yet tracked
|
||||
if (!fullResolvedItems.has(progress.archiveName)) {
|
||||
@ -6837,14 +6829,11 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
const archiveItems = fullResolvedItems.get(progress.archiveName) || [];
|
||||
|
||||
// Only finalize on explicit archive completion (or real current increment),
|
||||
// not on plain 100% archivePercent.
|
||||
if (archiveFinished) {
|
||||
// If archive is at 100%, mark its items as done and remove from active
|
||||
if (Number(progress.archivePercent ?? 0) >= 100) {
|
||||
const doneAt = nowMs();
|
||||
const startedAt = fullStartTimes.get(progress.archiveName) || doneAt;
|
||||
const doneLabel = progress.archiveSuccess === false
|
||||
? "Entpacken - Error"
|
||||
: formatExtractDone(doneAt - startedAt);
|
||||
const doneLabel = formatExtractDone(doneAt - startedAt);
|
||||
for (const entry of archiveItems) {
|
||||
if (!isExtractedLabel(entry.fullStatus)) {
|
||||
entry.fullStatus = doneLabel;
|
||||
@ -6854,7 +6843,7 @@ export class DownloadManager extends EventEmitter {
|
||||
fullResolvedItems.delete(progress.archiveName);
|
||||
fullStartTimes.delete(progress.archiveName);
|
||||
// Show transitional label while next archive initializes
|
||||
const done = currentCount;
|
||||
const done = progress.current + 1;
|
||||
if (done < progress.total) {
|
||||
emitExtractStatus(`Entpacken (${done}/${progress.total}) - Naechstes Archiv...`, true);
|
||||
}
|
||||
@ -6889,7 +6878,7 @@ export class DownloadManager extends EventEmitter {
|
||||
const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000
|
||||
? ` · ${Math.floor(progress.elapsedMs / 1000)}s`
|
||||
: "";
|
||||
const activeArchive = !archiveFinished && Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
|
||||
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
|
||||
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
|
||||
let overallLabel: string;
|
||||
if (progress.passwordFound) {
|
||||
@ -7039,7 +7028,7 @@ export class DownloadManager extends EventEmitter {
|
||||
): Promise<void> {
|
||||
try {
|
||||
// ── Nested extraction: extract archives found inside the extracted output ──
|
||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.autoExtract) {
|
||||
if (extractedCount > 0 && failed === 0 && this.settings.autoExtract) {
|
||||
const nestedBlacklist = /\.(iso|img|bin|dmg|vhd|vhdx|vmdk|wim)$/i;
|
||||
const nestedCandidates = (await findArchiveCandidates(pkg.extractDir))
|
||||
.filter((p) => !nestedBlacklist.test(p));
|
||||
@ -7066,16 +7055,14 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
|
||||
// ── Auto-Rename ──
|
||||
if (extractedCount > 0 || alreadyMarkedExtracted) {
|
||||
if (extractedCount > 0) {
|
||||
pkg.postProcessLabel = "Renaming...";
|
||||
this.emitState();
|
||||
await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg);
|
||||
}
|
||||
|
||||
// ── Archive cleanup (source archives in outputDir) ──
|
||||
// Also run when hybrid extraction already handled everything (extractedCount=0
|
||||
// but alreadyMarkedExtracted=true) so archives are still cleaned up.
|
||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.cleanupMode !== "none") {
|
||||
if (extractedCount > 0 && failed === 0 && this.settings.cleanupMode !== "none") {
|
||||
pkg.postProcessLabel = "Aufräumen...";
|
||||
this.emitState();
|
||||
const sourceAndTargetEqual = path.resolve(pkg.outputDir).toLowerCase() === path.resolve(pkg.extractDir).toLowerCase();
|
||||
@ -7099,7 +7086,7 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
|
||||
// ── Link/Sample artifact removal ──
|
||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0) {
|
||||
if (extractedCount > 0 && failed === 0) {
|
||||
if (this.settings.removeLinkFilesAfterExtract) {
|
||||
const removedLinks = await removeDownloadLinkArtifacts(pkg.extractDir);
|
||||
if (removedLinks > 0) {
|
||||
@ -7114,15 +7101,8 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resume state cleanup ──
|
||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0) {
|
||||
await clearExtractResumeState(pkg.outputDir, packageId);
|
||||
// Backward compatibility: older versions used .rd_extract_progress.json without package suffix.
|
||||
await clearExtractResumeState(pkg.outputDir);
|
||||
}
|
||||
|
||||
// ── Empty directory tree removal ──
|
||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.cleanupMode === "delete") {
|
||||
if (extractedCount > 0 && failed === 0 && this.settings.cleanupMode === "delete") {
|
||||
if (!(await hasAnyFilesRecursive(pkg.outputDir))) {
|
||||
const removedDirs = await removeEmptyDirectoryTree(pkg.outputDir);
|
||||
if (removedDirs > 0) {
|
||||
@ -7131,6 +7111,11 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resume state cleanup ──
|
||||
if (extractedCount > 0 && failed === 0) {
|
||||
await clearExtractResumeState(pkg.outputDir, packageId);
|
||||
}
|
||||
|
||||
// ── MKV collection ──
|
||||
if (success > 0 && (pkg.status === "completed" || pkg.status === "failed")) {
|
||||
pkg.postProcessLabel = "Verschiebe MKVs...";
|
||||
|
||||
@ -10,7 +10,7 @@ import { removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const DEFAULT_ARCHIVE_PASSWORDS = ["", "serienfans.org", "serienjunkies.org"];
|
||||
const NO_EXTRACTOR_MESSAGE = "Kein nativer Entpacker gefunden (7-Zip/WinRAR). Bitte 7-Zip oder WinRAR installieren.";
|
||||
const NO_EXTRACTOR_MESSAGE = "WinRAR/UnRAR nicht gefunden. Bitte WinRAR installieren.";
|
||||
const NO_JVM_EXTRACTOR_MESSAGE = "7-Zip-JBinding Runtime nicht gefunden. Bitte resources/extractor-jvm prüfen.";
|
||||
const JVM_EXTRACTOR_MAIN_CLASS = "com.sucukdeluxe.extractor.JBindExtractorMain";
|
||||
const JVM_EXTRACTOR_CLASSES_SUBDIR = "classes";
|
||||
@ -123,8 +123,6 @@ export interface ExtractProgressUpdate {
|
||||
passwordAttempt?: number;
|
||||
passwordTotal?: number;
|
||||
passwordFound?: boolean;
|
||||
archiveDone?: boolean;
|
||||
archiveSuccess?: boolean;
|
||||
}
|
||||
|
||||
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
|
||||
@ -135,8 +133,6 @@ const EXTRACT_MAX_TIMEOUT_MS = 120 * 60 * 1000;
|
||||
const ARCHIVE_SORT_COLLATOR = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
||||
const DISK_SPACE_SAFETY_FACTOR = 1.1;
|
||||
const NESTED_EXTRACT_BLACKLIST_RE = /\.(iso|img|bin|dmg|vhd|vhdx|vmdk|wim)$/i;
|
||||
const PACKAGE_PASSWORD_CACHE_LIMIT = 256;
|
||||
const packageLearnedPasswords = new Map<string, string>();
|
||||
|
||||
export type ArchiveSignature = "rar" | "7z" | "zip" | "gzip" | "bzip2" | "xz" | null;
|
||||
|
||||
@ -149,54 +145,6 @@ const ARCHIVE_SIGNATURES: { prefix: string; type: ArchiveSignature }[] = [
|
||||
{ prefix: "fd377a585a00", type: "xz" },
|
||||
];
|
||||
|
||||
function packagePasswordCacheKey(packageDir: string, packageId?: string): string {
|
||||
const normalizedPackageId = String(packageId || "").trim();
|
||||
if (normalizedPackageId) {
|
||||
return `pkg:${normalizedPackageId}`;
|
||||
}
|
||||
return `dir:${pathSetKey(path.resolve(packageDir))}`;
|
||||
}
|
||||
|
||||
function packagePasswordCacheLabel(packageDir: string, packageId?: string): string {
|
||||
const normalizedPackageId = String(packageId || "").trim();
|
||||
if (normalizedPackageId) {
|
||||
return `packageId=${normalizedPackageId.slice(0, 8)}`;
|
||||
}
|
||||
return `packageDir=${path.basename(path.resolve(packageDir))}`;
|
||||
}
|
||||
|
||||
function readCachedPackagePassword(cacheKey: string): string {
|
||||
const cached = packageLearnedPasswords.get(cacheKey);
|
||||
if (!cached) {
|
||||
return "";
|
||||
}
|
||||
// Refresh insertion order to keep recently used package caches alive.
|
||||
packageLearnedPasswords.delete(cacheKey);
|
||||
packageLearnedPasswords.set(cacheKey, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
function writeCachedPackagePassword(cacheKey: string, password: string): void {
|
||||
const normalized = String(password || "").trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (packageLearnedPasswords.has(cacheKey)) {
|
||||
packageLearnedPasswords.delete(cacheKey);
|
||||
}
|
||||
packageLearnedPasswords.set(cacheKey, normalized);
|
||||
if (packageLearnedPasswords.size > PACKAGE_PASSWORD_CACHE_LIMIT) {
|
||||
const oldestKey = packageLearnedPasswords.keys().next().value as string | undefined;
|
||||
if (oldestKey) {
|
||||
packageLearnedPasswords.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearCachedPackagePassword(cacheKey: string): void {
|
||||
packageLearnedPasswords.delete(cacheKey);
|
||||
}
|
||||
|
||||
export async function detectArchiveSignature(filePath: string): Promise<ArchiveSignature> {
|
||||
let fd: fs.promises.FileHandle | null = null;
|
||||
try {
|
||||
@ -430,12 +378,6 @@ function parseProgressPercent(chunk: string): number | null {
|
||||
return latest;
|
||||
}
|
||||
|
||||
function nextArchivePercent(previous: number, incoming: number): number {
|
||||
const prev = Math.max(0, Math.min(100, Math.floor(Number(previous) || 0)));
|
||||
const next = Math.max(0, Math.min(100, Math.floor(Number(incoming) || 0)));
|
||||
return next >= prev ? next : prev;
|
||||
}
|
||||
|
||||
async function shouldPreferExternalZip(archivePath: string): Promise<boolean> {
|
||||
if (extractorBackendMode() !== "legacy") {
|
||||
return true;
|
||||
@ -587,63 +529,32 @@ function prioritizePassword(passwords: string[], successful: string): string[] {
|
||||
return passwords;
|
||||
}
|
||||
const index = passwords.findIndex((candidate) => candidate === target);
|
||||
if (index === 0) {
|
||||
if (index <= 0) {
|
||||
return passwords;
|
||||
}
|
||||
if (index < 0) {
|
||||
return [target, ...passwords.filter((candidate) => candidate !== target)];
|
||||
}
|
||||
const next = [...passwords];
|
||||
const [value] = next.splice(index, 1);
|
||||
next.unshift(value);
|
||||
return next;
|
||||
}
|
||||
|
||||
function nativeExtractorCandidates(): string[] {
|
||||
function winRarCandidates(): string[] {
|
||||
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
|
||||
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
||||
const localAppData = process.env.LOCALAPPDATA || "";
|
||||
|
||||
const sevenZipInstalled = [
|
||||
process.env.RD_7Z_BIN || "",
|
||||
path.join(programFiles, "7-Zip", "7z.exe"),
|
||||
path.join(programFilesX86, "7-Zip", "7z.exe")
|
||||
];
|
||||
if (localAppData) {
|
||||
sevenZipInstalled.push(path.join(localAppData, "Programs", "7-Zip", "7z.exe"));
|
||||
}
|
||||
|
||||
const winRarInstalled = [
|
||||
const installed = [
|
||||
path.join(programFiles, "WinRAR", "UnRAR.exe"),
|
||||
path.join(programFilesX86, "WinRAR", "UnRAR.exe")
|
||||
];
|
||||
|
||||
if (localAppData) {
|
||||
winRarInstalled.push(path.join(localAppData, "Programs", "WinRAR", "UnRAR.exe"));
|
||||
installed.push(path.join(localAppData, "Programs", "WinRAR", "UnRAR.exe"));
|
||||
}
|
||||
|
||||
const ordered = resolvedExtractorCommand
|
||||
? [
|
||||
resolvedExtractorCommand,
|
||||
...sevenZipInstalled,
|
||||
"7z.exe",
|
||||
"7z",
|
||||
"7za.exe",
|
||||
"7za",
|
||||
...winRarInstalled,
|
||||
"UnRAR.exe",
|
||||
"unrar"
|
||||
]
|
||||
: [
|
||||
...sevenZipInstalled,
|
||||
"7z.exe",
|
||||
"7z",
|
||||
"7za.exe",
|
||||
"7za",
|
||||
...winRarInstalled,
|
||||
"UnRAR.exe",
|
||||
"unrar"
|
||||
];
|
||||
? [resolvedExtractorCommand, ...installed, "UnRAR.exe", "unrar"]
|
||||
: [...installed, "UnRAR.exe", "unrar"];
|
||||
return Array.from(new Set(ordered.filter(Boolean)));
|
||||
}
|
||||
|
||||
@ -938,7 +849,7 @@ type JvmExtractResult = {
|
||||
};
|
||||
|
||||
function extractorBackendMode(): ExtractBackendMode {
|
||||
const defaultMode = "legacy";
|
||||
const defaultMode = process.env.VITEST ? "legacy" : "jvm";
|
||||
const raw = String(process.env.RD_EXTRACT_BACKEND || defaultMode).trim().toLowerCase();
|
||||
if (raw === "legacy") {
|
||||
return "legacy";
|
||||
@ -1050,12 +961,9 @@ function parseJvmLine(
|
||||
|
||||
if (trimmed.startsWith("RD_PROGRESS ")) {
|
||||
const parsed = parseProgressPercent(trimmed);
|
||||
if (parsed !== null) {
|
||||
const next = nextArchivePercent(state.bestPercent, parsed);
|
||||
if (next !== state.bestPercent) {
|
||||
state.bestPercent = next;
|
||||
onArchiveProgress?.(next);
|
||||
}
|
||||
if (parsed !== null && parsed > state.bestPercent) {
|
||||
state.bestPercent = parsed;
|
||||
onArchiveProgress?.(parsed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -1090,9 +998,6 @@ interface DaemonRequest {
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
parseState: { bestPercent: number; usedPassword: string; backend: string; reportedError: string };
|
||||
archiveName: string;
|
||||
startedAt: number;
|
||||
passwordCount: number;
|
||||
}
|
||||
|
||||
let daemonProcess: ChildProcess | null = null;
|
||||
@ -1155,11 +1060,6 @@ function handleDaemonLine(line: string): void {
|
||||
const code = parseInt(trimmed.slice("RD_REQUEST_DONE ".length).trim(), 10);
|
||||
const req = daemonCurrentRequest;
|
||||
if (!req) return;
|
||||
const elapsedMs = Date.now() - req.startedAt;
|
||||
logger.info(
|
||||
`JVM Daemon Request Ende: archive=${req.archiveName}, code=${code}, ms=${elapsedMs}, pwCandidates=${req.passwordCount}, ` +
|
||||
`bestPercent=${req.parseState.bestPercent}, backend=${req.parseState.backend || "unknown"}, usedPassword=${req.parseState.usedPassword ? "yes" : "no"}`
|
||||
);
|
||||
|
||||
if (code === 0) {
|
||||
req.onArchiveProgress?.(100);
|
||||
@ -1187,8 +1087,6 @@ function handleDaemonLine(line: string): void {
|
||||
|
||||
function startDaemon(layout: JvmExtractorLayout): boolean {
|
||||
if (daemonProcess && daemonReady) return true;
|
||||
// Don't kill a daemon that's still booting — it will become ready soon
|
||||
if (daemonProcess) return false;
|
||||
shutdownDaemon();
|
||||
|
||||
const jvmTmpDir = path.join(os.tmpdir(), `rd-extract-daemon-${crypto.randomUUID()}`);
|
||||
@ -1284,22 +1182,6 @@ function isDaemonAvailable(layout: JvmExtractorLayout): boolean {
|
||||
return Boolean(daemonProcess && daemonReady && !daemonBusy);
|
||||
}
|
||||
|
||||
/** Wait for the daemon to become ready (boot phase) or free (busy phase), with timeout. */
|
||||
function waitForDaemonReady(maxWaitMs: number, signal?: AbortSignal): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
const check = () => {
|
||||
if (signal?.aborted) { resolve(false); return; }
|
||||
if (daemonProcess && daemonReady && !daemonBusy) { resolve(true); return; }
|
||||
// Daemon died while we were waiting
|
||||
if (!daemonProcess) { resolve(false); return; }
|
||||
if (Date.now() - start >= maxWaitMs) { resolve(false); return; }
|
||||
setTimeout(check, 50);
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
function sendDaemonRequest(
|
||||
archivePath: string,
|
||||
targetDir: string,
|
||||
@ -1312,21 +1194,10 @@ function sendDaemonRequest(
|
||||
return new Promise((resolve) => {
|
||||
const mode = effectiveConflictMode(conflictMode);
|
||||
const parseState = { bestPercent: 0, usedPassword: "", backend: "", reportedError: "" };
|
||||
const archiveName = path.basename(archivePath);
|
||||
|
||||
daemonBusy = true;
|
||||
daemonOutput = "";
|
||||
daemonCurrentRequest = {
|
||||
resolve,
|
||||
onArchiveProgress,
|
||||
signal,
|
||||
timeoutMs,
|
||||
parseState,
|
||||
archiveName,
|
||||
startedAt: Date.now(),
|
||||
passwordCount: passwordCandidates.length
|
||||
};
|
||||
logger.info(`JVM Daemon Request Start: archive=${archiveName}, pwCandidates=${passwordCandidates.length}, timeoutMs=${timeoutMs || 0}, conflict=${mode}`);
|
||||
daemonCurrentRequest = { resolve, onArchiveProgress, signal, timeoutMs, parseState };
|
||||
|
||||
// Set up timeout
|
||||
if (timeoutMs && timeoutMs > 0) {
|
||||
@ -1385,7 +1256,7 @@ function sendDaemonRequest(
|
||||
});
|
||||
}
|
||||
|
||||
async function runJvmExtractCommand(
|
||||
function runJvmExtractCommand(
|
||||
layout: JvmExtractorLayout,
|
||||
archivePath: string,
|
||||
targetDir: string,
|
||||
@ -1410,26 +1281,12 @@ async function runJvmExtractCommand(
|
||||
|
||||
// Try persistent daemon first — saves ~5s JVM boot per archive
|
||||
if (isDaemonAvailable(layout)) {
|
||||
logger.info(`JVM Daemon: Sofort verfügbar, sende Request für ${path.basename(archivePath)} (pwCandidates=${passwordCandidates.length})`);
|
||||
logger.info(`JVM Daemon: Sende Request für ${path.basename(archivePath)}`);
|
||||
return sendDaemonRequest(archivePath, targetDir, conflictMode, passwordCandidates, onArchiveProgress, signal, timeoutMs);
|
||||
}
|
||||
|
||||
// Daemon exists but is still booting or busy — wait up to 15s for it
|
||||
if (daemonProcess) {
|
||||
const reason = !daemonReady ? "booting" : "busy";
|
||||
const waitStartedAt = Date.now();
|
||||
logger.info(`JVM Daemon: Warte auf ${reason} Daemon für ${path.basename(archivePath)}...`);
|
||||
const ready = await waitForDaemonReady(15_000, signal);
|
||||
const waitedMs = Date.now() - waitStartedAt;
|
||||
if (ready) {
|
||||
logger.info(`JVM Daemon: Bereit nach ${waitedMs}ms — sende Request für ${path.basename(archivePath)}`);
|
||||
return sendDaemonRequest(archivePath, targetDir, conflictMode, passwordCandidates, onArchiveProgress, signal, timeoutMs);
|
||||
}
|
||||
logger.warn(`JVM Daemon: Timeout nach ${waitedMs}ms beim Warten — Fallback auf neuen Prozess für ${path.basename(archivePath)}`);
|
||||
}
|
||||
|
||||
// Fallback: spawn a new JVM process (daemon not available after waiting)
|
||||
logger.info(`JVM Spawn: Neuer Prozess für ${path.basename(archivePath)}`);
|
||||
// Fallback: spawn a new JVM process (daemon busy or not available)
|
||||
logger.info(`JVM Spawn: Neuer Prozess für ${path.basename(archivePath)}${daemonBusy ? " (Daemon busy)" : ""}`);
|
||||
|
||||
const mode = effectiveConflictMode(conflictMode);
|
||||
// Each JVM process needs its own temp dir so parallel SevenZipJBinding
|
||||
@ -1673,7 +1530,7 @@ async function resolveExtractorCommandInternal(): Promise<string> {
|
||||
resolveFailureAt = 0;
|
||||
}
|
||||
|
||||
const candidates = nativeExtractorCandidates();
|
||||
const candidates = winRarCandidates();
|
||||
for (const command of candidates) {
|
||||
if (isAbsoluteCommand(command) && !fs.existsSync(command)) {
|
||||
continue;
|
||||
@ -1726,11 +1583,7 @@ async function runExternalExtract(
|
||||
): Promise<string> {
|
||||
const timeoutMs = await computeExtractTimeoutMs(archivePath);
|
||||
const backendMode = extractorBackendMode();
|
||||
const archiveName = path.basename(archivePath);
|
||||
const totalStartedAt = Date.now();
|
||||
let jvmFailureReason = "";
|
||||
let fallbackFromJvm = false;
|
||||
logger.info(`Extract-Backend Start: archive=${archiveName}, mode=${backendMode}, pwCandidates=${passwordCandidates.length}, timeoutMs=${timeoutMs}, hybrid=${hybridMode}`);
|
||||
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
|
||||
@ -1751,8 +1604,7 @@ async function runExternalExtract(
|
||||
logger.warn(`JVM-Extractor nicht verfügbar, nutze Legacy-Extractor: ${path.basename(archivePath)}`);
|
||||
} else {
|
||||
const quotedPasswords = passwordCandidates.map((p) => p === "" ? '""' : `"${p}"`);
|
||||
logger.info(`JVM-Extractor aktiv (${layout.rootDir}): ${archiveName}, ${passwordCandidates.length} Passwörter: [${quotedPasswords.join(", ")}]`);
|
||||
const jvmStartedAt = Date.now();
|
||||
logger.info(`JVM-Extractor aktiv (${layout.rootDir}): ${path.basename(archivePath)}, ${passwordCandidates.length} Passwörter: [${quotedPasswords.join(", ")}]`);
|
||||
const jvmResult = await runJvmExtractCommand(
|
||||
layout,
|
||||
archivePath,
|
||||
@ -1763,12 +1615,9 @@ async function runExternalExtract(
|
||||
signal,
|
||||
timeoutMs
|
||||
);
|
||||
const jvmMs = Date.now() - jvmStartedAt;
|
||||
logger.info(`JVM-Extractor Ergebnis: archive=${archiveName}, ok=${jvmResult.ok}, ms=${jvmMs}, timedOut=${jvmResult.timedOut}, aborted=${jvmResult.aborted}, backend=${jvmResult.backend || "unknown"}, usedPassword=${jvmResult.usedPassword ? "yes" : "no"}`);
|
||||
|
||||
if (jvmResult.ok) {
|
||||
logger.info(`Entpackt via ${jvmResult.backend || "jvm"}: ${archiveName}`);
|
||||
logger.info(`Extract-Backend Ende: archive=${archiveName}, backend=${jvmResult.backend || "jvm"}, mode=${backendMode}, ms=${Date.now() - totalStartedAt}, fallbackFromJvm=false, usedPassword=${jvmResult.usedPassword ? "yes" : "no"}`);
|
||||
logger.info(`Entpackt via ${jvmResult.backend || "jvm"}: ${path.basename(archivePath)}`);
|
||||
return jvmResult.usedPassword;
|
||||
}
|
||||
if (jvmResult.aborted) {
|
||||
@ -1779,7 +1628,6 @@ async function runExternalExtract(
|
||||
}
|
||||
|
||||
jvmFailureReason = jvmResult.errorText || "JVM-Extractor fehlgeschlagen";
|
||||
fallbackFromJvm = true;
|
||||
const jvmFailureLower = jvmFailureReason.toLowerCase();
|
||||
const isUnsupportedMethod = jvmFailureReason.includes("UNSUPPORTEDMETHOD");
|
||||
const isCodecError = jvmFailureLower.includes("registered codecs")
|
||||
@ -1808,7 +1656,6 @@ async function runExternalExtract(
|
||||
const effectiveTargetDir = subst ? `${subst.drive}:\\` : targetDir;
|
||||
|
||||
const command = await resolveExtractorCommand();
|
||||
const legacyStartedAt = Date.now();
|
||||
const password = await runExternalExtractInner(
|
||||
command,
|
||||
archivePath,
|
||||
@ -1821,14 +1668,12 @@ async function runExternalExtract(
|
||||
hybridMode,
|
||||
onPasswordAttempt
|
||||
);
|
||||
const legacyMs = Date.now() - legacyStartedAt;
|
||||
const extractorName = path.basename(command).replace(/\.exe$/i, "");
|
||||
if (jvmFailureReason) {
|
||||
logger.info(`Entpackt via legacy/${extractorName} (nach JVM-Fehler): ${archiveName}`);
|
||||
logger.info(`Entpackt via legacy/${extractorName} (nach JVM-Fehler): ${path.basename(archivePath)}`);
|
||||
} else {
|
||||
logger.info(`Entpackt via legacy/${extractorName}: ${archiveName}`);
|
||||
logger.info(`Entpackt via legacy/${extractorName}: ${path.basename(archivePath)}`);
|
||||
}
|
||||
logger.info(`Extract-Backend Ende: archive=${archiveName}, backend=legacy/${extractorName}, mode=${backendMode}, ms=${Date.now() - totalStartedAt}, legacyMs=${legacyMs}, fallbackFromJvm=${fallbackFromJvm}, usedPassword=${password ? "yes" : "no"}`);
|
||||
return password;
|
||||
} finally {
|
||||
if (subst) removeSubstMapping(subst);
|
||||
@ -1867,7 +1712,6 @@ async function runExternalExtractInner(
|
||||
onArchiveProgress?.(0);
|
||||
}
|
||||
passwordAttempt += 1;
|
||||
const attemptStartedAt = Date.now();
|
||||
const quotedPw = password === "" ? '""' : `"${password}"`;
|
||||
logger.info(`Legacy-Passwort-Versuch ${passwordAttempt}/${passwords.length} für ${path.basename(archivePath)}: ${quotedPw}`);
|
||||
if (passwords.length > 1) {
|
||||
@ -1876,14 +1720,11 @@ async function runExternalExtractInner(
|
||||
let args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password, usePerformanceFlags, hybridMode);
|
||||
let result = await runExtractCommand(command, args, (chunk) => {
|
||||
const parsed = parseProgressPercent(chunk);
|
||||
if (parsed === null) {
|
||||
if (parsed === null || parsed <= bestPercent) {
|
||||
return;
|
||||
}
|
||||
const next = nextArchivePercent(bestPercent, parsed);
|
||||
if (next !== bestPercent) {
|
||||
bestPercent = next;
|
||||
bestPercent = parsed;
|
||||
onArchiveProgress?.(bestPercent);
|
||||
}
|
||||
}, signal, timeoutMs);
|
||||
|
||||
if (!result.ok && usePerformanceFlags && isUnsupportedExtractorSwitchError(result.errorText)) {
|
||||
@ -1893,22 +1734,14 @@ async function runExternalExtractInner(
|
||||
args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password, false, hybridMode);
|
||||
result = await runExtractCommand(command, args, (chunk) => {
|
||||
const parsed = parseProgressPercent(chunk);
|
||||
if (parsed === null) {
|
||||
if (parsed === null || parsed <= bestPercent) {
|
||||
return;
|
||||
}
|
||||
const next = nextArchivePercent(bestPercent, parsed);
|
||||
if (next !== bestPercent) {
|
||||
bestPercent = next;
|
||||
bestPercent = parsed;
|
||||
onArchiveProgress?.(bestPercent);
|
||||
}
|
||||
}, signal, timeoutMs);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Legacy-Passwort-Versuch Ergebnis: archive=${path.basename(archivePath)}, attempt=${passwordAttempt}/${passwords.length}, ` +
|
||||
`ms=${Date.now() - attemptStartedAt}, ok=${result.ok}, timedOut=${result.timedOut}, missingCommand=${result.missingCommand}, bestPercent=${bestPercent}`
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
onArchiveProgress?.(100);
|
||||
return password;
|
||||
@ -2376,14 +2209,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
if (options.conflictMode === "ask") {
|
||||
logger.warn("Extract-ConflictMode 'ask' wird ohne Prompt als 'skip' behandelt");
|
||||
}
|
||||
const passwordCacheKey = packagePasswordCacheKey(options.packageDir, options.packageId);
|
||||
const passwordCacheLabel = packagePasswordCacheLabel(options.packageDir, options.packageId);
|
||||
let passwordCandidates = archivePasswords(options.passwordList || "");
|
||||
const cachedPackagePassword = readCachedPackagePassword(passwordCacheKey);
|
||||
if (cachedPackagePassword) {
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, cachedPackagePassword);
|
||||
logger.info(`Passwort-Cache Treffer: ${passwordCacheLabel}, bekanntes Passwort wird zuerst getestet`);
|
||||
}
|
||||
const resumeCompleted = await readExtractResumeState(options.packageDir, options.packageId);
|
||||
const resumeCompletedAtStart = resumeCompleted.size;
|
||||
const allCandidateNames = new Set(allCandidates.map((archivePath) => archiveNameKey(path.basename(archivePath))));
|
||||
@ -2402,7 +2228,6 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
let extracted = candidates.length - pendingCandidates.length;
|
||||
let failed = 0;
|
||||
let lastError = "";
|
||||
let learnedPassword = cachedPackagePassword;
|
||||
const extractedArchives = new Set<string>();
|
||||
for (const archivePath of candidates) {
|
||||
if (resumeCompleted.has(archiveNameKey(path.basename(archivePath)))) {
|
||||
@ -2410,41 +2235,23 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
const rememberLearnedPassword = (password: string): void => {
|
||||
const normalized = String(password || "").trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const changed = normalized !== learnedPassword;
|
||||
learnedPassword = normalized;
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, normalized);
|
||||
writeCachedPackagePassword(passwordCacheKey, normalized);
|
||||
if (changed) {
|
||||
logger.info(`Passwort-Cache Update: ${passwordCacheLabel}, neues Passwort gelernt`);
|
||||
}
|
||||
};
|
||||
|
||||
const emitProgress = (
|
||||
current: number,
|
||||
archiveName: string,
|
||||
phase: "extracting" | "done",
|
||||
archivePercent?: number,
|
||||
elapsedMs?: number,
|
||||
pwInfo?: { passwordAttempt?: number; passwordTotal?: number; passwordFound?: boolean },
|
||||
archiveInfo?: { archiveDone?: boolean; archiveSuccess?: boolean }
|
||||
pwInfo?: { passwordAttempt?: number; passwordTotal?: number; passwordFound?: boolean }
|
||||
): void => {
|
||||
if (!options.onProgress) {
|
||||
return;
|
||||
}
|
||||
const total = Math.max(1, candidates.length);
|
||||
let percent = Math.max(0, Math.min(100, Math.floor((current / total) * 100)));
|
||||
let normalizedArchivePercent = Math.max(0, Math.min(100, Number(archivePercent ?? 0)));
|
||||
if (phase !== "done") {
|
||||
const boundedCurrent = Math.max(0, Math.min(total, current));
|
||||
if (archiveInfo?.archiveDone !== true && normalizedArchivePercent >= 100) {
|
||||
normalizedArchivePercent = 99;
|
||||
}
|
||||
percent = Math.max(0, Math.min(100, Math.floor(((boundedCurrent + (normalizedArchivePercent / 100)) / total) * 100)));
|
||||
const boundedArchivePercent = Math.max(0, Math.min(100, Number(archivePercent ?? 0)));
|
||||
percent = Math.max(0, Math.min(100, Math.floor(((boundedCurrent + (boundedArchivePercent / 100)) / total) * 100)));
|
||||
}
|
||||
try {
|
||||
options.onProgress({
|
||||
@ -2452,10 +2259,9 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
total,
|
||||
percent,
|
||||
archiveName,
|
||||
archivePercent: normalizedArchivePercent,
|
||||
archivePercent,
|
||||
elapsedMs,
|
||||
phase,
|
||||
...(archiveInfo || {}),
|
||||
...(pwInfo || {})
|
||||
});
|
||||
} catch (error) {
|
||||
@ -2470,13 +2276,12 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
// rather than leaving them as "Entpacken - Ausstehend" until all extraction finishes.
|
||||
for (const archivePath of candidates) {
|
||||
if (resumeCompleted.has(archiveNameKey(path.basename(archivePath)))) {
|
||||
emitProgress(extracted, path.basename(archivePath), "extracting", 100, 0, undefined, { archiveDone: true, archiveSuccess: true });
|
||||
emitProgress(extracted, path.basename(archivePath), "extracting", 100, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const maxParallel = Math.max(1, options.maxParallel || 1);
|
||||
let noExtractorEncountered = false;
|
||||
let lastArchiveFinishedAt: number | null = null;
|
||||
|
||||
const extractSingleArchive = async (archivePath: string): Promise<void> => {
|
||||
if (options.signal?.aborted) {
|
||||
@ -2488,36 +2293,17 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
const archiveName = path.basename(archivePath);
|
||||
const archiveResumeKey = archiveNameKey(archiveName);
|
||||
const archiveStartedAt = Date.now();
|
||||
const startedCurrent = extracted + failed;
|
||||
if (lastArchiveFinishedAt !== null) {
|
||||
logger.info(`Extract-Trace Gap: before=${archiveName}, prevDoneToStartMs=${archiveStartedAt - lastArchiveFinishedAt}, progress=${startedCurrent}/${candidates.length}`);
|
||||
}
|
||||
let archivePercent = 0;
|
||||
let reached99At: number | null = null;
|
||||
let archiveOutcome: "success" | "failed" | "skipped" = "failed";
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, 0);
|
||||
const pulseTimer = setInterval(() => {
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}, 1100);
|
||||
const hybrid = Boolean(options.hybridMode);
|
||||
// Before the first successful extraction, filename-derived candidates are useful.
|
||||
// After a known password is learned, try that first to avoid per-archive delays.
|
||||
// Insert archive-filename-derived passwords after "" but before custom passwords
|
||||
const filenamePasswords = archiveFilenamePasswords(archiveName);
|
||||
const nonEmptyBasePasswords = passwordCandidates.filter((p) => p !== "");
|
||||
const orderedNonEmpty = learnedPassword
|
||||
? [learnedPassword, ...nonEmptyBasePasswords.filter((p) => p !== learnedPassword), ...filenamePasswords]
|
||||
: [...filenamePasswords, ...nonEmptyBasePasswords];
|
||||
const archivePasswordCandidates = learnedPassword
|
||||
? Array.from(new Set([...orderedNonEmpty, ""]))
|
||||
: Array.from(new Set(["", ...orderedNonEmpty]));
|
||||
const reportArchiveProgress = (value: number): void => {
|
||||
archivePercent = nextArchivePercent(archivePercent, value);
|
||||
if (reached99At === null && archivePercent >= 99) {
|
||||
reached99At = Date.now();
|
||||
logger.info(`Extract-Trace 99%: archive=${archiveName}, elapsedMs=${reached99At - archiveStartedAt}`);
|
||||
}
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
};
|
||||
const archivePasswordCandidates = filenamePasswords.length > 0
|
||||
? Array.from(new Set(["", ...filenamePasswords, ...passwordCandidates.filter((p) => p !== "")]))
|
||||
: passwordCandidates;
|
||||
|
||||
// Validate generic .001 splits via file signature before attempting extraction
|
||||
const isGenericSplit = /\.\d{3}$/i.test(archiveName) && !/\.(zip|7z)\.\d{3}$/i.test(archiveName);
|
||||
@ -2530,10 +2316,6 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
extractedArchives.add(archivePath);
|
||||
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
|
||||
clearInterval(pulseTimer);
|
||||
archiveOutcome = "skipped";
|
||||
const skippedAt = Date.now();
|
||||
lastArchiveFinishedAt = skippedAt;
|
||||
logger.info(`Extract-Trace Archiv Übersprungen: archive=${archiveName}, ms=${skippedAt - archiveStartedAt}, reason=no-signature`);
|
||||
return;
|
||||
}
|
||||
logger.info(`Generische Split-Datei verifiziert (Signatur: ${sig}): ${archiveName}`);
|
||||
@ -2556,9 +2338,10 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
if (preferExternal) {
|
||||
try {
|
||||
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
|
||||
reportArchiveProgress(value);
|
||||
archivePercent = Math.max(archivePercent, value);
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}, options.signal, hybrid, onPwAttempt);
|
||||
rememberLearnedPassword(usedPassword);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||
} catch (error) {
|
||||
if (isNoExtractorError(String(error))) {
|
||||
await extractZipArchive(archivePath, options.targetDir, options.conflictMode, options.signal);
|
||||
@ -2576,9 +2359,10 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
}
|
||||
try {
|
||||
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
|
||||
reportArchiveProgress(value);
|
||||
archivePercent = Math.max(archivePercent, value);
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}, options.signal, hybrid, onPwAttempt);
|
||||
rememberLearnedPassword(usedPassword);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||
} catch (externalError) {
|
||||
if (isNoExtractorError(String(externalError)) || isUnsupportedArchiveFormatError(String(externalError))) {
|
||||
throw error;
|
||||
@ -2589,25 +2373,21 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
}
|
||||
} else {
|
||||
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
|
||||
reportArchiveProgress(value);
|
||||
archivePercent = Math.max(archivePercent, value);
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}, options.signal, hybrid, onPwAttempt);
|
||||
rememberLearnedPassword(usedPassword);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||
}
|
||||
extracted += 1;
|
||||
extractedArchives.add(archivePath);
|
||||
resumeCompleted.add(archiveResumeKey);
|
||||
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
|
||||
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
|
||||
archiveOutcome = "success";
|
||||
const successAt = Date.now();
|
||||
const tailAfter99Ms = reached99At ? (successAt - reached99At) : -1;
|
||||
logger.info(`Extract-Trace Archiv Erfolg: archive=${archiveName}, totalMs=${successAt - archiveStartedAt}, tailAfter99Ms=${tailAfter99Ms >= 0 ? tailAfter99Ms : "n/a"}, pwCandidates=${archivePasswordCandidates.length}`);
|
||||
lastArchiveFinishedAt = successAt;
|
||||
archivePercent = 100;
|
||||
if (hasManyPasswords) {
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordFound: true }, { archiveDone: true, archiveSuccess: true });
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordFound: true });
|
||||
} else {
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, undefined, { archiveDone: true, archiveSuccess: true });
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorText = String(error);
|
||||
@ -2618,25 +2398,12 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
lastError = errorText;
|
||||
const errorCategory = classifyExtractionError(errorText);
|
||||
logger.error(`Entpack-Fehler ${path.basename(archivePath)} [${errorCategory}]: ${errorText}`);
|
||||
if (errorCategory === "wrong_password" && learnedPassword) {
|
||||
learnedPassword = "";
|
||||
clearCachedPackagePassword(passwordCacheKey);
|
||||
logger.warn(`Passwort-Cache verworfen: ${passwordCacheLabel} (wrong_password)`);
|
||||
}
|
||||
const failedAt = Date.now();
|
||||
const tailAfter99Ms = reached99At ? (failedAt - reached99At) : -1;
|
||||
logger.warn(`Extract-Trace Archiv Fehler: archive=${archiveName}, totalMs=${failedAt - archiveStartedAt}, tailAfter99Ms=${tailAfter99Ms >= 0 ? tailAfter99Ms : "n/a"}, category=${errorCategory}`);
|
||||
lastArchiveFinishedAt = failedAt;
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, undefined, { archiveDone: true, archiveSuccess: false });
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
if (isNoExtractorError(errorText)) {
|
||||
noExtractorEncountered = true;
|
||||
}
|
||||
} finally {
|
||||
clearInterval(pulseTimer);
|
||||
if (lastArchiveFinishedAt === null || lastArchiveFinishedAt < archiveStartedAt) {
|
||||
lastArchiveFinishedAt = Date.now();
|
||||
}
|
||||
logger.info(`Extract-Trace Archiv Ende: archive=${archiveName}, outcome=${archiveOutcome}, elapsedMs=${lastArchiveFinishedAt - archiveStartedAt}, percent=${archivePercent}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -2761,11 +2528,11 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
} catch (zipErr) {
|
||||
if (!shouldFallbackToExternalZip(zipErr)) throw zipErr;
|
||||
const usedPw = await runExternalExtract(nestedArchive, options.targetDir, options.conflictMode, passwordCandidates, (v) => { nestedPercent = Math.max(nestedPercent, v); }, options.signal, hybrid);
|
||||
rememberLearnedPassword(usedPw);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPw);
|
||||
}
|
||||
} else {
|
||||
const usedPw = await runExternalExtract(nestedArchive, options.targetDir, options.conflictMode, passwordCandidates, (v) => { nestedPercent = Math.max(nestedPercent, v); }, options.signal, hybrid);
|
||||
rememberLearnedPassword(usedPw);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPw);
|
||||
}
|
||||
extracted += 1;
|
||||
nestedExtracted += 1;
|
||||
|
||||
@ -5,8 +5,8 @@ import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, Down
|
||||
import { defaultSettings } from "./constants";
|
||||
import { logger } from "./logger";
|
||||
|
||||
const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]);
|
||||
const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]);
|
||||
const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]);
|
||||
const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]);
|
||||
const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]);
|
||||
const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]);
|
||||
const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]);
|
||||
@ -17,7 +17,7 @@ const VALID_PACKAGE_PRIORITIES = new Set<string>(["high", "normal", "low"]);
|
||||
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
|
||||
"queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled"
|
||||
]);
|
||||
const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]);
|
||||
const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]);
|
||||
const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]);
|
||||
|
||||
function asText(value: unknown): string {
|
||||
@ -113,7 +113,6 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
allDebridToken: asText(settings.allDebridToken),
|
||||
ddownloadLogin: asText(settings.ddownloadLogin),
|
||||
ddownloadPassword: asText(settings.ddownloadPassword),
|
||||
oneFichierApiKey: asText(settings.oneFichierApiKey),
|
||||
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
|
||||
rememberToken: Boolean(settings.rememberToken),
|
||||
providerPrimary: settings.providerPrimary,
|
||||
@ -205,8 +204,7 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
ddownloadLogin: "",
|
||||
ddownloadPassword: "",
|
||||
oneFichierApiKey: ""
|
||||
ddownloadPassword: ""
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ const emptyStats = (): DownloadStats => ({
|
||||
|
||||
const emptySnapshot = (): UiSnapshot => ({
|
||||
settings: {
|
||||
token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "",
|
||||
token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "",
|
||||
archivePasswordList: "",
|
||||
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
||||
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||
@ -94,7 +94,7 @@ const cleanupLabels: Record<string, string> = {
|
||||
const AUTO_RENDER_PACKAGE_LIMIT = 260;
|
||||
|
||||
const providerLabels: Record<DebridProvider, string> = {
|
||||
realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier"
|
||||
realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload"
|
||||
};
|
||||
|
||||
function formatDateTime(ts: number): string {
|
||||
@ -930,11 +930,7 @@ export function App(): ReactElement {
|
||||
Boolean((settingsDraft.ddownloadLogin || "").trim() && (settingsDraft.ddownloadPassword || "").trim()),
|
||||
[settingsDraft.ddownloadLogin, settingsDraft.ddownloadPassword]);
|
||||
|
||||
const hasOneFichierAccount = useMemo(() =>
|
||||
Boolean((settingsDraft.oneFichierApiKey || "").trim()),
|
||||
[settingsDraft.oneFichierApiKey]);
|
||||
|
||||
const totalConfiguredAccounts = configuredProviders.length + (hasDdownloadAccount ? 1 : 0) + (hasOneFichierAccount ? 1 : 0);
|
||||
const totalConfiguredAccounts = configuredProviders.length + (hasDdownloadAccount ? 1 : 0);
|
||||
|
||||
const primaryProviderValue: DebridProvider = useMemo(() => {
|
||||
if (configuredProviders.includes(settingsDraft.providerPrimary)) {
|
||||
@ -2748,8 +2744,6 @@ export function App(): ReactElement {
|
||||
<input value={settingsDraft.ddownloadLogin || ""} onChange={(e) => setText("ddownloadLogin", e.target.value)} />
|
||||
<label>DDownload Passwort</label>
|
||||
<input type="password" value={settingsDraft.ddownloadPassword || ""} onChange={(e) => setText("ddownloadPassword", e.target.value)} />
|
||||
<label>1Fichier API Key</label>
|
||||
<input type="password" value={settingsDraft.oneFichierApiKey || ""} onChange={(e) => setText("oneFichierApiKey", e.target.value)} />
|
||||
{configuredProviders.length === 0 && (
|
||||
<div className="hint">Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.</div>
|
||||
)}
|
||||
@ -2989,7 +2983,7 @@ export function App(): ReactElement {
|
||||
<span>Links: {Object.keys(snapshot.session.items).length}</span>
|
||||
<span>Session: {humanSize(snapshot.stats.totalDownloaded)}</span>
|
||||
<span>Gesamt: {humanSize(snapshot.stats.totalDownloadedAllTime)}</span>
|
||||
<span>Hoster: {providerStats.length}</span>
|
||||
<span>Hoster: {totalConfiguredAccounts}</span>
|
||||
<span>{snapshot.speedText}</span>
|
||||
<span>{snapshot.etaText}</span>
|
||||
<span className="footer-spacer" />
|
||||
|
||||
@ -14,7 +14,7 @@ export type CleanupMode = "none" | "trash" | "delete";
|
||||
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
|
||||
export type SpeedMode = "global" | "per_download";
|
||||
export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done";
|
||||
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier";
|
||||
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload";
|
||||
export type DebridFallbackProvider = DebridProvider | "none";
|
||||
export type AppTheme = "dark" | "light";
|
||||
export type PackagePriority = "high" | "normal" | "low";
|
||||
@ -44,7 +44,6 @@ export interface AppSettings {
|
||||
allDebridToken: string;
|
||||
ddownloadLogin: string;
|
||||
ddownloadPassword: string;
|
||||
oneFichierApiKey: string;
|
||||
archivePasswordList: string;
|
||||
rememberToken: boolean;
|
||||
providerPrimary: DebridProvider;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user