Release v1.5.86
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
15d0969cd9
commit
d63afcce89
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.5.85",
|
"version": "1.5.86",
|
||||||
"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",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
DuplicatePolicy,
|
DuplicatePolicy,
|
||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
PackageEntry,
|
PackageEntry,
|
||||||
|
PackagePriority,
|
||||||
ParsedPackageInput,
|
ParsedPackageInput,
|
||||||
SessionState,
|
SessionState,
|
||||||
StartConflictEntry,
|
StartConflictEntry,
|
||||||
@ -1210,6 +1211,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
itemIds: [],
|
itemIds: [],
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
priority: "normal",
|
||||||
createdAt: nowMs(),
|
createdAt: nowMs(),
|
||||||
updatedAt: nowMs()
|
updatedAt: nowMs()
|
||||||
};
|
};
|
||||||
@ -2430,6 +2432,30 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setPackagePriority(packageId: string, priority: PackagePriority): void {
|
||||||
|
const pkg = this.session.packages[packageId];
|
||||||
|
if (!pkg) return;
|
||||||
|
if (priority !== "high" && priority !== "normal" && priority !== "low") return;
|
||||||
|
pkg.priority = priority;
|
||||||
|
pkg.updatedAt = nowMs();
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public skipItems(itemIds: string[]): void {
|
||||||
|
for (const itemId of itemIds) {
|
||||||
|
const item = this.session.items[itemId];
|
||||||
|
if (!item) continue;
|
||||||
|
if (item.status !== "queued" && item.status !== "reconnect_wait") continue;
|
||||||
|
item.status = "cancelled";
|
||||||
|
item.fullStatus = "Übersprungen";
|
||||||
|
item.speedBps = 0;
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
}
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
}
|
||||||
|
|
||||||
public async startPackages(packageIds: string[]): Promise<void> {
|
public async startPackages(packageIds: string[]): Promise<void> {
|
||||||
const targetSet = new Set(packageIds);
|
const targetSet = new Set(packageIds);
|
||||||
|
|
||||||
@ -2839,6 +2865,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (pkg.enabled === undefined) {
|
if (pkg.enabled === undefined) {
|
||||||
pkg.enabled = true;
|
pkg.enabled = true;
|
||||||
}
|
}
|
||||||
|
if (!pkg.priority) {
|
||||||
|
pkg.priority = "normal";
|
||||||
|
}
|
||||||
if (pkg.status === "downloading"
|
if (pkg.status === "downloading"
|
||||||
|| pkg.status === "validating"
|
|| pkg.status === "validating"
|
||||||
|| pkg.status === "extracting"
|
|| pkg.status === "extracting"
|
||||||
@ -3720,11 +3749,16 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private findNextQueuedItem(): { packageId: string; itemId: string } | null {
|
private findNextQueuedItem(): { packageId: string; itemId: string } | null {
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
|
const priorityOrder: Array<PackagePriority> = ["high", "normal", "low"];
|
||||||
|
for (const prio of priorityOrder) {
|
||||||
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.enabled) {
|
if (!pkg || pkg.cancelled || !pkg.enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ((pkg.priority || "normal") !== prio) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (this.runPackageIds.size > 0 && !this.runPackageIds.has(packageId)) {
|
if (this.runPackageIds.size > 0 && !this.runPackageIds.has(packageId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -3745,6 +3779,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3995,7 +4030,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
item.targetPath = this.claimTargetPath(item.id, preferredTargetPath, Boolean(canReuseExistingTarget));
|
item.targetPath = this.claimTargetPath(item.id, preferredTargetPath, Boolean(canReuseExistingTarget));
|
||||||
item.totalBytes = unrestricted.fileSize;
|
item.totalBytes = unrestricted.fileSize;
|
||||||
item.status = "downloading";
|
item.status = "downloading";
|
||||||
item.fullStatus = `Download läuft (${unrestricted.providerLabel})`;
|
item.fullStatus = `Starte... (${unrestricted.providerLabel})`;
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
|
|
||||||
@ -4888,7 +4923,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
item.downloadedBytes = written;
|
item.downloadedBytes = written;
|
||||||
item.progressPercent = item.totalBytes ? Math.max(0, Math.min(100, Math.floor((written / item.totalBytes) * 100))) : 100;
|
item.progressPercent = item.totalBytes ? Math.max(0, Math.min(100, Math.floor((written / item.totalBytes) * 100))) : 100;
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
|
item.fullStatus = "Finalisierend...";
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
|
this.emitState();
|
||||||
return { resumable };
|
return { resumable };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (active.abortController.signal.aborted || String(error).includes("aborted:")) {
|
if (active.abortController.signal.aborted || String(error).includes("aborted:")) {
|
||||||
@ -5474,7 +5511,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
: "";
|
: "";
|
||||||
const activeArchive = 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));
|
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
|
||||||
const label = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
let label: string;
|
||||||
|
if (progress.passwordFound) {
|
||||||
|
label = `Passwort gefunden · ${progress.archiveName}`;
|
||||||
|
} else if (progress.passwordAttempt && progress.passwordTotal && progress.passwordTotal > 1) {
|
||||||
|
const pwPct = Math.round((progress.passwordAttempt / progress.passwordTotal) * 100);
|
||||||
|
label = `Passwort knacken: ${pwPct}% (${progress.passwordAttempt}/${progress.passwordTotal}) · ${progress.archiveName}`;
|
||||||
|
} else {
|
||||||
|
label = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
||||||
|
}
|
||||||
const updatedAt = nowMs();
|
const updatedAt = nowMs();
|
||||||
for (const entry of archItems) {
|
for (const entry of archItems) {
|
||||||
if (!isExtractedLabel(entry.fullStatus)) {
|
if (!isExtractedLabel(entry.fullStatus)) {
|
||||||
@ -5713,7 +5758,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
: "";
|
: "";
|
||||||
const activeArchive = 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));
|
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
|
||||||
const label = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
let label: string;
|
||||||
|
if (progress.passwordFound) {
|
||||||
|
label = `Passwort gefunden · ${progress.archiveName}`;
|
||||||
|
} else if (progress.passwordAttempt && progress.passwordTotal && progress.passwordTotal > 1) {
|
||||||
|
const pwPct = Math.round((progress.passwordAttempt / progress.passwordTotal) * 100);
|
||||||
|
label = `Passwort knacken: ${pwPct}% (${progress.passwordAttempt}/${progress.passwordTotal}) · ${progress.archiveName}`;
|
||||||
|
} else {
|
||||||
|
label = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
||||||
|
}
|
||||||
const updatedAt = nowMs();
|
const updatedAt = nowMs();
|
||||||
for (const entry of archiveItems) {
|
for (const entry of archiveItems) {
|
||||||
if (!isExtractedLabel(entry.fullStatus) && entry.fullStatus !== label) {
|
if (!isExtractedLabel(entry.fullStatus) && entry.fullStatus !== label) {
|
||||||
@ -5731,7 +5784,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
: "";
|
: "";
|
||||||
const activeArchive = 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));
|
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
|
||||||
const overallLabel = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
let overallLabel: string;
|
||||||
|
if (progress.passwordFound) {
|
||||||
|
overallLabel = `Passwort gefunden · ${progress.archiveName || ""}`;
|
||||||
|
} else if (progress.passwordAttempt && progress.passwordTotal && progress.passwordTotal > 1) {
|
||||||
|
const pwPct = Math.round((progress.passwordAttempt / progress.passwordTotal) * 100);
|
||||||
|
overallLabel = `Passwort knacken: ${pwPct}% (${progress.passwordAttempt}/${progress.passwordTotal}) · ${progress.archiveName || ""}`;
|
||||||
|
} else {
|
||||||
|
overallLabel = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
||||||
|
}
|
||||||
emitExtractStatus(overallLabel);
|
emitExtractStatus(overallLabel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -98,6 +98,9 @@ export interface ExtractProgressUpdate {
|
|||||||
archivePercent?: number;
|
archivePercent?: number;
|
||||||
elapsedMs?: number;
|
elapsedMs?: number;
|
||||||
phase: "extracting" | "done";
|
phase: "extracting" | "done";
|
||||||
|
passwordAttempt?: number;
|
||||||
|
passwordTotal?: number;
|
||||||
|
passwordFound?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
|
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
|
||||||
@ -1242,7 +1245,8 @@ async function runExternalExtract(
|
|||||||
passwordCandidates: string[],
|
passwordCandidates: string[],
|
||||||
onArchiveProgress?: (percent: number) => void,
|
onArchiveProgress?: (percent: number) => void,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
hybridMode = false
|
hybridMode = false,
|
||||||
|
onPasswordAttempt?: (attempt: number, total: number) => void
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const timeoutMs = await computeExtractTimeoutMs(archivePath);
|
const timeoutMs = await computeExtractTimeoutMs(archivePath);
|
||||||
const backendMode = extractorBackendMode();
|
const backendMode = extractorBackendMode();
|
||||||
@ -1328,7 +1332,8 @@ async function runExternalExtract(
|
|||||||
onArchiveProgress,
|
onArchiveProgress,
|
||||||
signal,
|
signal,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
hybridMode
|
hybridMode,
|
||||||
|
onPasswordAttempt
|
||||||
);
|
);
|
||||||
const extractorName = path.basename(command).replace(/\.exe$/i, "");
|
const extractorName = path.basename(command).replace(/\.exe$/i, "");
|
||||||
if (jvmFailureReason) {
|
if (jvmFailureReason) {
|
||||||
@ -1351,7 +1356,8 @@ async function runExternalExtractInner(
|
|||||||
onArchiveProgress: ((percent: number) => void) | undefined,
|
onArchiveProgress: ((percent: number) => void) | undefined,
|
||||||
signal: AbortSignal | undefined,
|
signal: AbortSignal | undefined,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
hybridMode = false
|
hybridMode = false,
|
||||||
|
onPasswordAttempt?: (attempt: number, total: number) => void
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const passwords = passwordCandidates;
|
const passwords = passwordCandidates;
|
||||||
let lastError = "";
|
let lastError = "";
|
||||||
@ -1375,6 +1381,9 @@ async function runExternalExtractInner(
|
|||||||
passwordAttempt += 1;
|
passwordAttempt += 1;
|
||||||
const quotedPw = password === "" ? '""' : `"${password}"`;
|
const quotedPw = password === "" ? '""' : `"${password}"`;
|
||||||
logger.info(`Legacy-Passwort-Versuch ${passwordAttempt}/${passwords.length} für ${path.basename(archivePath)}: ${quotedPw}`);
|
logger.info(`Legacy-Passwort-Versuch ${passwordAttempt}/${passwords.length} für ${path.basename(archivePath)}: ${quotedPw}`);
|
||||||
|
if (passwords.length > 1) {
|
||||||
|
onPasswordAttempt?.(passwordAttempt, passwords.length);
|
||||||
|
}
|
||||||
let args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password, usePerformanceFlags, hybridMode);
|
let args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password, usePerformanceFlags, hybridMode);
|
||||||
let result = await runExtractCommand(command, args, (chunk) => {
|
let result = await runExtractCommand(command, args, (chunk) => {
|
||||||
const parsed = parseProgressPercent(chunk);
|
const parsed = parseProgressPercent(chunk);
|
||||||
@ -1889,7 +1898,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
archiveName: string,
|
archiveName: string,
|
||||||
phase: "extracting" | "done",
|
phase: "extracting" | "done",
|
||||||
archivePercent?: number,
|
archivePercent?: number,
|
||||||
elapsedMs?: number
|
elapsedMs?: number,
|
||||||
|
pwInfo?: { passwordAttempt?: number; passwordTotal?: number; passwordFound?: boolean }
|
||||||
): void => {
|
): void => {
|
||||||
if (!options.onProgress) {
|
if (!options.onProgress) {
|
||||||
return;
|
return;
|
||||||
@ -1909,7 +1919,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
archiveName,
|
archiveName,
|
||||||
archivePercent,
|
archivePercent,
|
||||||
elapsedMs,
|
elapsedMs,
|
||||||
phase
|
phase,
|
||||||
|
...(pwInfo || {})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`onProgress callback Fehler unterdrückt: ${cleanErrorText(String(error))}`);
|
logger.warn(`onProgress callback Fehler unterdrückt: ${cleanErrorText(String(error))}`);
|
||||||
@ -1953,6 +1964,13 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Entpacke Archiv: ${path.basename(archivePath)} -> ${options.targetDir}${hybrid ? " (hybrid, reduced threads, low I/O)" : ""}`);
|
logger.info(`Entpacke Archiv: ${path.basename(archivePath)} -> ${options.targetDir}${hybrid ? " (hybrid, reduced threads, low I/O)" : ""}`);
|
||||||
|
const hasManyPasswords = archivePasswordCandidates.length > 1;
|
||||||
|
if (hasManyPasswords) {
|
||||||
|
emitProgress(extracted + failed, archiveName, "extracting", 0, 0, { passwordAttempt: 0, passwordTotal: archivePasswordCandidates.length });
|
||||||
|
}
|
||||||
|
const onPwAttempt = hasManyPasswords
|
||||||
|
? (attempt: number, total: number) => { emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordAttempt: attempt, passwordTotal: total }); }
|
||||||
|
: undefined;
|
||||||
try {
|
try {
|
||||||
const ext = path.extname(archivePath).toLowerCase();
|
const ext = path.extname(archivePath).toLowerCase();
|
||||||
if (ext === ".zip") {
|
if (ext === ".zip") {
|
||||||
@ -1962,7 +1980,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
|
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (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, hybrid);
|
}, options.signal, hybrid, onPwAttempt);
|
||||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isNoExtractorError(String(error))) {
|
if (isNoExtractorError(String(error))) {
|
||||||
@ -1983,7 +2001,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
|
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (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, hybrid);
|
}, options.signal, hybrid, onPwAttempt);
|
||||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||||
} catch (externalError) {
|
} catch (externalError) {
|
||||||
if (isNoExtractorError(String(externalError)) || isUnsupportedArchiveFormatError(String(externalError))) {
|
if (isNoExtractorError(String(externalError)) || isUnsupportedArchiveFormatError(String(externalError))) {
|
||||||
@ -1997,7 +2015,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
|
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (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, hybrid);
|
}, options.signal, hybrid, onPwAttempt);
|
||||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||||
}
|
}
|
||||||
extracted += 1;
|
extracted += 1;
|
||||||
@ -2006,7 +2024,11 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
|
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
|
||||||
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
|
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
|
||||||
archivePercent = 100;
|
archivePercent = 100;
|
||||||
|
if (hasManyPasswords) {
|
||||||
|
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordFound: true });
|
||||||
|
} else {
|
||||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failed += 1;
|
failed += 1;
|
||||||
const errorText = String(error);
|
const errorText = String(error);
|
||||||
|
|||||||
@ -330,6 +330,15 @@ function registerIpcHandlers(): void {
|
|||||||
validateString(packageId, "packageId");
|
validateString(packageId, "packageId");
|
||||||
return controller.resetPackage(packageId);
|
return controller.resetPackage(packageId);
|
||||||
});
|
});
|
||||||
|
ipcMain.handle(IPC_CHANNELS.SET_PACKAGE_PRIORITY, (_event: IpcMainInvokeEvent, packageId: string, priority: string) => {
|
||||||
|
validateString(packageId, "packageId");
|
||||||
|
validateString(priority, "priority");
|
||||||
|
return controller.setPackagePriority(packageId, priority as any);
|
||||||
|
});
|
||||||
|
ipcMain.handle(IPC_CHANNELS.SKIP_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
|
||||||
|
if (!Array.isArray(itemIds)) throw new Error("itemIds must be an array");
|
||||||
|
return controller.skipItems(itemIds);
|
||||||
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory());
|
ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory());
|
||||||
ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory());
|
ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory());
|
||||||
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
|
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
|
||||||
|
|||||||
@ -56,6 +56,8 @@ const api: ElectronApi = {
|
|||||||
getHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_HISTORY),
|
getHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_HISTORY),
|
||||||
clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
|
clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
|
||||||
removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId),
|
removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId),
|
||||||
|
setPackagePriority: (packageId: string, priority: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SET_PACKAGE_PRIORITY, packageId, priority),
|
||||||
|
skipItems: (itemIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SKIP_ITEMS, itemIds),
|
||||||
onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => {
|
onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => {
|
||||||
const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot);
|
const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot);
|
||||||
ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener);
|
ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener);
|
||||||
|
|||||||
@ -2229,6 +2229,7 @@ export function App(): ReactElement {
|
|||||||
];
|
];
|
||||||
})}
|
})}
|
||||||
<span className="pkg-col pkg-col-account">Service</span>
|
<span className="pkg-col pkg-col-account">Service</span>
|
||||||
|
<span className="pkg-col pkg-col-prio">Priorität</span>
|
||||||
<span className="pkg-col pkg-col-status">Status</span>
|
<span className="pkg-col pkg-col-status">Status</span>
|
||||||
<span className="pkg-col pkg-col-speed">Geschwindigkeit</span>
|
<span className="pkg-col pkg-col-speed">Geschwindigkeit</span>
|
||||||
</div>
|
</div>
|
||||||
@ -2339,6 +2340,7 @@ export function App(): ReactElement {
|
|||||||
<span className="pkg-col pkg-col-progress">{entry.status === "completed" ? "100%" : "-"}</span>
|
<span className="pkg-col pkg-col-progress">{entry.status === "completed" ? "100%" : "-"}</span>
|
||||||
<span className="pkg-col pkg-col-hoster">-</span>
|
<span className="pkg-col pkg-col-hoster">-</span>
|
||||||
<span className="pkg-col pkg-col-account">{entry.provider ? providerLabels[entry.provider] : "-"}</span>
|
<span className="pkg-col pkg-col-account">{entry.provider ? providerLabels[entry.provider] : "-"}</span>
|
||||||
|
<span className="pkg-col pkg-col-prio"></span>
|
||||||
<span className="pkg-col pkg-col-status">{entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}</span>
|
<span className="pkg-col pkg-col-status">{entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}</span>
|
||||||
<span className="pkg-col pkg-col-speed">-</span>
|
<span className="pkg-col pkg-col-speed">-</span>
|
||||||
</div>
|
</div>
|
||||||
@ -2814,6 +2816,26 @@ export function App(): ReactElement {
|
|||||||
)}
|
)}
|
||||||
</>);
|
</>);
|
||||||
})()}
|
})()}
|
||||||
|
{hasPackages && !contextMenu.itemId && (<>
|
||||||
|
<div className="ctx-menu-sep" />
|
||||||
|
<div className="ctx-menu-sub">
|
||||||
|
<button className="ctx-menu-item">Priorität ▸</button>
|
||||||
|
<div className="ctx-menu-sub-items">
|
||||||
|
{(["high", "normal", "low"] as const).map((p) => {
|
||||||
|
const label = p === "high" ? "Hoch" : p === "low" ? "Niedrig" : "Standard";
|
||||||
|
const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
||||||
|
const allMatch = pkgIds.every((id) => (snapshot.session.packages[id]?.priority || "normal") === p);
|
||||||
|
return <button key={p} className={`ctx-menu-item${allMatch ? " ctx-menu-active" : ""}`} onClick={() => { for (const id of pkgIds) void window.rd.setPackagePriority(id, p); setContextMenu(null); }}>{allMatch ? `✓ ${label}` : label}</button>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
{hasItems && (() => {
|
||||||
|
const itemIds = [...selectedIds].filter((id) => snapshot.session.items[id]);
|
||||||
|
const skippable = itemIds.filter((id) => { const it = snapshot.session.items[id]; return it && (it.status === "queued" || it.status === "reconnect_wait"); });
|
||||||
|
if (skippable.length === 0) return null;
|
||||||
|
return <button className="ctx-menu-item" onClick={() => { void window.rd.skipItems(skippable); setContextMenu(null); }}>Überspringen{skippable.length > 1 ? ` (${skippable.length})` : ""}</button>;
|
||||||
|
})()}
|
||||||
{hasPackages && (
|
{hasPackages && (
|
||||||
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
@ -3019,6 +3041,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
const providers = [...new Set(items.map((item) => item.provider).filter(Boolean))];
|
const providers = [...new Set(items.map((item) => item.provider).filter(Boolean))];
|
||||||
return providers.length > 0 ? providers.map((p) => providerLabels[p!] || p).join(", ") : "-";
|
return providers.length > 0 ? providers.map((p) => providerLabels[p!] || p).join(", ") : "-";
|
||||||
})()}</span>
|
})()}</span>
|
||||||
|
<span className={`pkg-col pkg-col-prio${pkg.priority === "high" ? " prio-high" : pkg.priority === "low" ? " prio-low" : ""}`}>{pkg.priority === "high" ? "Hoch" : pkg.priority === "low" ? "Niedrig" : "-"}</span>
|
||||||
<span className="pkg-col pkg-col-status">[{done}/{total}{done === total && total > 0 ? " - Done" : ""}{failed > 0 ? ` · ${failed} Fehler` : ""}{cancelled > 0 ? ` · ${cancelled} abgebr.` : ""}]</span>
|
<span className="pkg-col pkg-col-status">[{done}/{total}{done === total && total > 0 ? " - Done" : ""}{failed > 0 ? ` · ${failed} Fehler` : ""}{cancelled > 0 ? ` · ${cancelled} abgebr.` : ""}]</span>
|
||||||
<span className="pkg-col pkg-col-speed">{packageSpeed > 0 ? formatSpeedMbps(packageSpeed) : "-"}</span>
|
<span className="pkg-col pkg-col-speed">{packageSpeed > 0 ? formatSpeedMbps(packageSpeed) : "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -3057,6 +3080,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
</span>
|
</span>
|
||||||
<span className="pkg-col pkg-col-hoster" title={extractHoster(item.url)}>{extractHoster(item.url) || "-"}</span>
|
<span className="pkg-col pkg-col-hoster" title={extractHoster(item.url)}>{extractHoster(item.url) || "-"}</span>
|
||||||
<span className="pkg-col pkg-col-account">{item.provider ? providerLabels[item.provider] : "-"}</span>
|
<span className="pkg-col pkg-col-account">{item.provider ? providerLabels[item.provider] : "-"}</span>
|
||||||
|
<span className="pkg-col pkg-col-prio"></span>
|
||||||
<span className="pkg-col pkg-col-status" title={item.retries > 0 ? `${item.fullStatus} · R${item.retries}` : item.fullStatus}>
|
<span className="pkg-col pkg-col-status" title={item.retries > 0 ? `${item.fullStatus} · R${item.retries}` : item.fullStatus}>
|
||||||
{item.fullStatus}
|
{item.fullStatus}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -577,7 +577,7 @@ body,
|
|||||||
|
|
||||||
.pkg-column-header {
|
.pkg-column-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 170px 90px 140px 130px 180px 100px;
|
grid-template-columns: 1fr 160px 80px 110px 110px 70px 160px 90px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
@ -593,6 +593,7 @@ body,
|
|||||||
.pkg-column-header .pkg-col-size,
|
.pkg-column-header .pkg-col-size,
|
||||||
.pkg-column-header .pkg-col-hoster,
|
.pkg-column-header .pkg-col-hoster,
|
||||||
.pkg-column-header .pkg-col-account,
|
.pkg-column-header .pkg-col-account,
|
||||||
|
.pkg-column-header .pkg-col-prio,
|
||||||
.pkg-column-header .pkg-col-status,
|
.pkg-column-header .pkg-col-status,
|
||||||
.pkg-column-header .pkg-col-speed {
|
.pkg-column-header .pkg-col-speed {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -612,7 +613,7 @@ body,
|
|||||||
|
|
||||||
.pkg-columns {
|
.pkg-columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 170px 90px 140px 130px 180px 100px;
|
grid-template-columns: 1fr 160px 80px 110px 110px 70px 160px 90px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -636,6 +637,7 @@ body,
|
|||||||
.pkg-columns .pkg-col-size,
|
.pkg-columns .pkg-col-size,
|
||||||
.pkg-columns .pkg-col-hoster,
|
.pkg-columns .pkg-col-hoster,
|
||||||
.pkg-columns .pkg-col-account,
|
.pkg-columns .pkg-col-account,
|
||||||
|
.pkg-columns .pkg-col-prio,
|
||||||
.pkg-columns .pkg-col-status,
|
.pkg-columns .pkg-col-status,
|
||||||
.pkg-columns .pkg-col-speed {
|
.pkg-columns .pkg-col-speed {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -1284,7 +1286,7 @@ td {
|
|||||||
|
|
||||||
.item-row {
|
.item-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 170px 90px 140px 130px 180px 100px;
|
grid-template-columns: 1fr 160px 80px 110px 110px 70px 160px 90px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 -12px;
|
margin: 0 -12px;
|
||||||
@ -1337,6 +1339,45 @@ td {
|
|||||||
box-shadow: 0 0 4px #f59e0b80;
|
box-shadow: 0 0 4px #f59e0b80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prio-high {
|
||||||
|
color: #f59e0b !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prio-low {
|
||||||
|
color: #64748b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-menu-sub {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-menu-sub > .ctx-menu-item::after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-menu-sub-items {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
min-width: 120px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,.3);
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-menu-sub:hover .ctx-menu-sub-items {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-menu-active {
|
||||||
|
color: var(--accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.item-remove {
|
.item-remove {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@ -1774,6 +1815,7 @@ td {
|
|||||||
.pkg-column-header .pkg-col-size,
|
.pkg-column-header .pkg-col-size,
|
||||||
.pkg-column-header .pkg-col-hoster,
|
.pkg-column-header .pkg-col-hoster,
|
||||||
.pkg-column-header .pkg-col-account,
|
.pkg-column-header .pkg-col-account,
|
||||||
|
.pkg-column-header .pkg-col-prio,
|
||||||
.pkg-column-header .pkg-col-status,
|
.pkg-column-header .pkg-col-status,
|
||||||
.pkg-column-header .pkg-col-speed {
|
.pkg-column-header .pkg-col-speed {
|
||||||
display: none;
|
display: none;
|
||||||
@ -1783,6 +1825,7 @@ td {
|
|||||||
.pkg-columns .pkg-col-size,
|
.pkg-columns .pkg-col-size,
|
||||||
.pkg-columns .pkg-col-hoster,
|
.pkg-columns .pkg-col-hoster,
|
||||||
.pkg-columns .pkg-col-account,
|
.pkg-columns .pkg-col-account,
|
||||||
|
.pkg-columns .pkg-col-prio,
|
||||||
.pkg-columns .pkg-col-status,
|
.pkg-columns .pkg-col-status,
|
||||||
.pkg-columns .pkg-col-speed {
|
.pkg-columns .pkg-col-speed {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@ -39,5 +39,7 @@ export const IPC_CHANNELS = {
|
|||||||
RESET_PACKAGE: "queue:reset-package",
|
RESET_PACKAGE: "queue:reset-package",
|
||||||
GET_HISTORY: "history:get",
|
GET_HISTORY: "history:get",
|
||||||
CLEAR_HISTORY: "history:clear",
|
CLEAR_HISTORY: "history:clear",
|
||||||
REMOVE_HISTORY_ENTRY: "history:remove-entry"
|
REMOVE_HISTORY_ENTRY: "history:remove-entry",
|
||||||
|
SET_PACKAGE_PRIORITY: "queue:set-package-priority",
|
||||||
|
SKIP_ITEMS: "queue:skip-items"
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type {
|
|||||||
AppSettings,
|
AppSettings,
|
||||||
DuplicatePolicy,
|
DuplicatePolicy,
|
||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
|
PackagePriority,
|
||||||
SessionStats,
|
SessionStats,
|
||||||
StartConflictEntry,
|
StartConflictEntry,
|
||||||
StartConflictResolutionResult,
|
StartConflictResolutionResult,
|
||||||
@ -51,6 +52,8 @@ export interface ElectronApi {
|
|||||||
getHistory: () => Promise<HistoryEntry[]>;
|
getHistory: () => Promise<HistoryEntry[]>;
|
||||||
clearHistory: () => Promise<void>;
|
clearHistory: () => Promise<void>;
|
||||||
removeHistoryEntry: (entryId: string) => Promise<void>;
|
removeHistoryEntry: (entryId: string) => Promise<void>;
|
||||||
|
setPackagePriority: (packageId: string, priority: PackagePriority) => Promise<void>;
|
||||||
|
skipItems: (itemIds: string[]) => Promise<void>;
|
||||||
onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void;
|
onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void;
|
||||||
onClipboardDetected: (callback: (links: string[]) => void) => () => void;
|
onClipboardDetected: (callback: (links: string[]) => void) => () => void;
|
||||||
onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void;
|
onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "packag
|
|||||||
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid";
|
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid";
|
||||||
export type DebridFallbackProvider = DebridProvider | "none";
|
export type DebridFallbackProvider = DebridProvider | "none";
|
||||||
export type AppTheme = "dark" | "light";
|
export type AppTheme = "dark" | "light";
|
||||||
|
export type PackagePriority = "high" | "normal" | "low";
|
||||||
|
|
||||||
export interface BandwidthScheduleEntry {
|
export interface BandwidthScheduleEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@ -113,6 +114,7 @@ export interface PackageEntry {
|
|||||||
itemIds: string[];
|
itemIds: string[];
|
||||||
cancelled: boolean;
|
cancelled: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
priority: PackagePriority;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user