Release v1.5.86
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
15d0969cd9
commit
d63afcce89
@@ -12,6 +12,7 @@ import {
|
||||
DuplicatePolicy,
|
||||
HistoryEntry,
|
||||
PackageEntry,
|
||||
PackagePriority,
|
||||
ParsedPackageInput,
|
||||
SessionState,
|
||||
StartConflictEntry,
|
||||
@@ -1210,6 +1211,7 @@ export class DownloadManager extends EventEmitter {
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
priority: "normal",
|
||||
createdAt: nowMs(),
|
||||
updatedAt: nowMs()
|
||||
};
|
||||
@@ -2430,6 +2432,30 @@ export class DownloadManager extends EventEmitter {
|
||||
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> {
|
||||
const targetSet = new Set(packageIds);
|
||||
|
||||
@@ -2839,6 +2865,9 @@ export class DownloadManager extends EventEmitter {
|
||||
if (pkg.enabled === undefined) {
|
||||
pkg.enabled = true;
|
||||
}
|
||||
if (!pkg.priority) {
|
||||
pkg.priority = "normal";
|
||||
}
|
||||
if (pkg.status === "downloading"
|
||||
|| pkg.status === "validating"
|
||||
|| pkg.status === "extracting"
|
||||
@@ -3720,28 +3749,34 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
private findNextQueuedItem(): { packageId: string; itemId: string } | null {
|
||||
const now = nowMs();
|
||||
for (const packageId of this.session.packageOrder) {
|
||||
const pkg = this.session.packages[packageId];
|
||||
if (!pkg || pkg.cancelled || !pkg.enabled) {
|
||||
continue;
|
||||
}
|
||||
if (this.runPackageIds.size > 0 && !this.runPackageIds.has(packageId)) {
|
||||
continue;
|
||||
}
|
||||
for (const itemId of pkg.itemIds) {
|
||||
const item = this.session.items[itemId];
|
||||
if (!item) {
|
||||
const priorityOrder: Array<PackagePriority> = ["high", "normal", "low"];
|
||||
for (const prio of priorityOrder) {
|
||||
for (const packageId of this.session.packageOrder) {
|
||||
const pkg = this.session.packages[packageId];
|
||||
if (!pkg || pkg.cancelled || !pkg.enabled) {
|
||||
continue;
|
||||
}
|
||||
const retryAfter = this.retryAfterByItem.get(itemId) || 0;
|
||||
if (retryAfter > now) {
|
||||
if ((pkg.priority || "normal") !== prio) {
|
||||
continue;
|
||||
}
|
||||
if (retryAfter > 0) {
|
||||
this.retryAfterByItem.delete(itemId);
|
||||
if (this.runPackageIds.size > 0 && !this.runPackageIds.has(packageId)) {
|
||||
continue;
|
||||
}
|
||||
if (item.status === "queued" || item.status === "reconnect_wait") {
|
||||
return { packageId, itemId };
|
||||
for (const itemId of pkg.itemIds) {
|
||||
const item = this.session.items[itemId];
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const retryAfter = this.retryAfterByItem.get(itemId) || 0;
|
||||
if (retryAfter > now) {
|
||||
continue;
|
||||
}
|
||||
if (retryAfter > 0) {
|
||||
this.retryAfterByItem.delete(itemId);
|
||||
}
|
||||
if (item.status === "queued" || item.status === "reconnect_wait") {
|
||||
return { packageId, itemId };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3995,7 +4030,7 @@ export class DownloadManager extends EventEmitter {
|
||||
item.targetPath = this.claimTargetPath(item.id, preferredTargetPath, Boolean(canReuseExistingTarget));
|
||||
item.totalBytes = unrestricted.fileSize;
|
||||
item.status = "downloading";
|
||||
item.fullStatus = `Download läuft (${unrestricted.providerLabel})`;
|
||||
item.fullStatus = `Starte... (${unrestricted.providerLabel})`;
|
||||
item.updatedAt = nowMs();
|
||||
this.emitState();
|
||||
|
||||
@@ -4888,7 +4923,9 @@ export class DownloadManager extends EventEmitter {
|
||||
item.downloadedBytes = written;
|
||||
item.progressPercent = item.totalBytes ? Math.max(0, Math.min(100, Math.floor((written / item.totalBytes) * 100))) : 100;
|
||||
item.speedBps = 0;
|
||||
item.fullStatus = "Finalisierend...";
|
||||
item.updatedAt = nowMs();
|
||||
this.emitState();
|
||||
return { resumable };
|
||||
} catch (error) {
|
||||
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 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();
|
||||
for (const entry of archItems) {
|
||||
if (!isExtractedLabel(entry.fullStatus)) {
|
||||
@@ -5713,7 +5758,15 @@ export class DownloadManager extends EventEmitter {
|
||||
: "";
|
||||
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
|
||||
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();
|
||||
for (const entry of archiveItems) {
|
||||
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 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);
|
||||
}
|
||||
});
|
||||
|
||||
+31
-9
@@ -98,6 +98,9 @@ export interface ExtractProgressUpdate {
|
||||
archivePercent?: number;
|
||||
elapsedMs?: number;
|
||||
phase: "extracting" | "done";
|
||||
passwordAttempt?: number;
|
||||
passwordTotal?: number;
|
||||
passwordFound?: boolean;
|
||||
}
|
||||
|
||||
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
|
||||
@@ -1242,7 +1245,8 @@ async function runExternalExtract(
|
||||
passwordCandidates: string[],
|
||||
onArchiveProgress?: (percent: number) => void,
|
||||
signal?: AbortSignal,
|
||||
hybridMode = false
|
||||
hybridMode = false,
|
||||
onPasswordAttempt?: (attempt: number, total: number) => void
|
||||
): Promise<string> {
|
||||
const timeoutMs = await computeExtractTimeoutMs(archivePath);
|
||||
const backendMode = extractorBackendMode();
|
||||
@@ -1328,7 +1332,8 @@ async function runExternalExtract(
|
||||
onArchiveProgress,
|
||||
signal,
|
||||
timeoutMs,
|
||||
hybridMode
|
||||
hybridMode,
|
||||
onPasswordAttempt
|
||||
);
|
||||
const extractorName = path.basename(command).replace(/\.exe$/i, "");
|
||||
if (jvmFailureReason) {
|
||||
@@ -1351,7 +1356,8 @@ async function runExternalExtractInner(
|
||||
onArchiveProgress: ((percent: number) => void) | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
timeoutMs: number,
|
||||
hybridMode = false
|
||||
hybridMode = false,
|
||||
onPasswordAttempt?: (attempt: number, total: number) => void
|
||||
): Promise<string> {
|
||||
const passwords = passwordCandidates;
|
||||
let lastError = "";
|
||||
@@ -1375,6 +1381,9 @@ async function runExternalExtractInner(
|
||||
passwordAttempt += 1;
|
||||
const quotedPw = password === "" ? '""' : `"${password}"`;
|
||||
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 result = await runExtractCommand(command, args, (chunk) => {
|
||||
const parsed = parseProgressPercent(chunk);
|
||||
@@ -1889,7 +1898,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
archiveName: string,
|
||||
phase: "extracting" | "done",
|
||||
archivePercent?: number,
|
||||
elapsedMs?: number
|
||||
elapsedMs?: number,
|
||||
pwInfo?: { passwordAttempt?: number; passwordTotal?: number; passwordFound?: boolean }
|
||||
): void => {
|
||||
if (!options.onProgress) {
|
||||
return;
|
||||
@@ -1909,7 +1919,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
archiveName,
|
||||
archivePercent,
|
||||
elapsedMs,
|
||||
phase
|
||||
phase,
|
||||
...(pwInfo || {})
|
||||
});
|
||||
} catch (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)" : ""}`);
|
||||
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 {
|
||||
const ext = path.extname(archivePath).toLowerCase();
|
||||
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) => {
|
||||
archivePercent = Math.max(archivePercent, value);
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}, options.signal, hybrid);
|
||||
}, options.signal, hybrid, onPwAttempt);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||
} catch (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) => {
|
||||
archivePercent = Math.max(archivePercent, value);
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}, options.signal, hybrid);
|
||||
}, options.signal, hybrid, onPwAttempt);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||
} catch (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) => {
|
||||
archivePercent = Math.max(archivePercent, value);
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}, options.signal, hybrid);
|
||||
}, options.signal, hybrid, onPwAttempt);
|
||||
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
|
||||
}
|
||||
extracted += 1;
|
||||
@@ -2006,7 +2024,11 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
|
||||
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
|
||||
archivePercent = 100;
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
if (hasManyPasswords) {
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordFound: true });
|
||||
} else {
|
||||
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
|
||||
}
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
const errorText = String(error);
|
||||
|
||||
@@ -330,6 +330,15 @@ function registerIpcHandlers(): void {
|
||||
validateString(packageId, "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.CLEAR_HISTORY, () => controller.clearHistory());
|
||||
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
|
||||
|
||||
Reference in New Issue
Block a user