feat: erzwinge festen Proxy für API-Anfragen

This commit is contained in:
Sucukdeluxe
2026-08-31 13:20:03 +02:00
parent 17ac99d373
commit a562f9162a
23 changed files with 530 additions and 64 deletions
+13 -9
View File
@@ -3,6 +3,7 @@ import { AllDebridHostInfo } from "../shared/types";
import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils";
import { ALLDEBRID_LOGIN_HOSTS, applyRemoteLoginSecurity, createRemoteLoginWebPreferences } from "./browser-security";
import { configureElectronProxySession } from "./network-proxy";
const ALLDEBRID_BASE_URL = "https://alldebrid.com";
const ALLDEBRID_LOGIN_URL = `${ALLDEBRID_BASE_URL}/register/?from=de`;
@@ -211,9 +212,10 @@ export class AllDebridWebFallback {
window.focus();
}
public async getHostInfo(host: string): Promise<AllDebridHostInfo> {
const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(ALLDEBRID_STATUS_URL, {
public async getHostInfo(host: string): Promise<AllDebridHostInfo> {
const currentSession = session.fromPartition(this.getPartition());
await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(ALLDEBRID_STATUS_URL, {
headers: {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Referer: ALLDEBRID_SERVICE_REFERER,
@@ -285,9 +287,10 @@ export class AllDebridWebFallback {
return run;
}
private async ensureLoginWindow(): Promise<BrowserWindow> {
const partition = this.getPartition();
const existing = this.loginWindow;
private async ensureLoginWindow(): Promise<BrowserWindow> {
const partition = this.getPartition();
await configureElectronProxySession(session.fromPartition(partition));
const existing = this.loginWindow;
if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) {
return existing;
}
@@ -327,9 +330,10 @@ export class AllDebridWebFallback {
body: URLSearchParams,
referer: string,
signal?: AbortSignal
): Promise<{ response: Response; text: string }> {
const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(url, {
): Promise<{ response: Response; text: string }> {
const currentSession = session.fromPartition(this.getPartition());
await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(url, {
method: "POST",
headers: {
Accept: "application/json, text/javascript, */*; q=0.01",
+17
View File
@@ -88,6 +88,7 @@ import { NotificationOutbox } from "./notification-outbox";
import { sendNotification } from "./notify";
import { DownloadHealthMonitor } from "./download-health-monitor";
import { shouldDeferAutoResumeToDailyStart } from "./daily-start-scheduler";
import { configureNetworkProxy, shutdownNetworkProxy } from "./network-proxy";
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
@@ -168,6 +169,7 @@ export class AppController {
this.logDirectory
);
}
this.applyNetworkProxyConfiguration();
this.initializeLogStorage();
this.runHistoryLifecycleCleanup("Start", () => resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode));
const loadResult = loadSessionWithStatus(this.storagePaths);
@@ -613,6 +615,7 @@ export class AppController {
this.overlayLiveUsageCounters(restoredSettings);
saveSettings(this.storagePaths, restoredSettings);
this.settings = restoredSettings;
this.applyNetworkProxyConfiguration();
runtimeApplied = true;
this.manager.setSettings(this.settings, { settingsOnlyImport: true });
this.manager.persistNowSync();
@@ -622,6 +625,7 @@ export class AppController {
this.rollbackImportPersistence(rollback, "fehlgeschlagenem Settings-Import");
}
this.settings = previousSettings;
this.applyNetworkProxyConfiguration();
if (runtimeApplied) {
try {
this.manager.setSettings(previousSettings, { settingsOnlyImport: true });
@@ -688,6 +692,7 @@ export class AppController {
throw error;
}
this.settings = nextSettings;
this.applyNetworkProxyConfiguration();
this.manager.setSettings(this.settings);
this.audit("INFO", "Einstellungen aktualisiert", {
changedKeys: Object.keys(sanitizedPatch),
@@ -1492,6 +1497,7 @@ export class AppController {
this.manager.skipShutdownPersist = true;
this.manager.blockAllPersistence = true;
this.settings = restoredSettings;
this.applyNetworkProxyConfiguration();
runtimeApplied = true;
this.manager.setSettings(this.settings);
this.manager.stop();
@@ -1505,6 +1511,7 @@ export class AppController {
this.manager.skipShutdownPersist = previousSkipShutdownPersist;
this.manager.blockAllPersistence = previousBlockAllPersistence;
this.settings = previousSettings;
this.applyNetworkProxyConfiguration();
if (runtimeApplied) {
try {
this.manager.setSettings(previousSettings, { settingsOnlyImport: true });
@@ -1617,6 +1624,7 @@ export class AppController {
this.pendingRealDebridWebAccountIds.clear();
this.allDebridWebFallback.dispose();
this.bestDebridWebFallback.dispose();
await shutdownNetworkProxy();
if (this.settings.historyRetentionMode === "session") {
this.runHistoryLifecycleCleanup("Beenden", () => clearHistory(this.storagePaths));
}
@@ -1629,6 +1637,15 @@ export class AppController {
logger.info("App beendet");
}
private applyNetworkProxyConfiguration(): void {
const state = configureNetworkProxy(this.settings);
if (state.status === "active") {
logger.info(`Proxy-only aktiv: fester API-Proxy ${state.selectedIndex}/${state.proxyCount}`);
} else if (state.status === "blocked") {
logger.warn(`Proxy-only blockiert Netzwerkanfragen: ${state.reason}`);
}
}
private async waitForShutdownTask(task: Promise<unknown>, deadlineAt: number): Promise<void> {
const remainingMs = Math.max(0, deadlineAt - Date.now());
if (remainingMs <= 0) {
+9 -6
View File
@@ -2,7 +2,8 @@ import fs from "node:fs";
import { session, type Session } from "electron";
import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils";
import { logger } from "./logger";
import { logger } from "./logger";
import { configureElectronProxySession } from "./network-proxy";
const BESTDEBRID_BASE_URL = "https://bestdebrid.com";
const BESTDEBRID_DOWNLOADER_URL = `${BESTDEBRID_BASE_URL}/en/downloader/`;
@@ -178,8 +179,9 @@ export class BestDebridWebFallback {
throw new Error("BestDebrid: Cookie-Datei enthält keinen Login-Cookie. Bitte nach dem Login erneut exportieren.");
}
const currentSession = session.fromPartition(this.getPartition());
await this.clearPartitionState(currentSession);
const currentSession = session.fromPartition(this.getPartition());
await configureElectronProxySession(currentSession);
await this.clearPartitionState(currentSession);
for (const cookie of bestDebridCookies) {
const url = `https://${cookie.domain.replace(/^\./, "")}${cookie.path}`;
@@ -244,9 +246,10 @@ export class BestDebridWebFallback {
}
private async generate(link: string, signal?: AbortSignal): Promise<{ kind: "success"; value: UnrestrictedLink } | { kind: "login_required" }> {
throwIfAborted(signal);
const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, {
throwIfAborted(signal);
const currentSession = session.fromPartition(this.getPartition());
await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, {
method: "POST",
headers: {
Accept: "application/json, text/javascript, */*; q=0.01",
+1
View File
@@ -108,6 +108,7 @@ export function defaultSettings(): AppSettings {
speedLimitMode: "global",
proxyDownloadEnabled: false,
proxyListPath: "",
proxyApiProxyIndex: 1,
proxyConnectionsPerDownload: 16,
updateRepo: DEFAULT_UPDATE_REPO,
autoUpdateCheck: true,
+3 -3
View File
@@ -11001,7 +11001,7 @@ export class DownloadManager extends EventEmitter {
if (!proxyAttempted && attempt === 1 && existingBytes === 0 && this.settings.proxyDownloadEnabled) {
proxyAttempted = true;
if (this.settings.speedLimitEnabled) {
logAttemptEvent("INFO", "Proxy-Segmentierung wegen aktivem Geschwindigkeitslimit übersprungen", {
logAttemptEvent("INFO", "Proxy-Segmentierung wegen aktivem Geschwindigkeitslimit übersprungen; fester Proxy wird verwendet", {
attempt
});
} else {
@@ -11104,10 +11104,10 @@ export class DownloadManager extends EventEmitter {
item.downloadedBytes = 0;
item.progressPercent = 0;
item.speedBps = 0;
item.fullStatus = "Direktdownload wird gestartet";
item.fullStatus = "Einzel-Proxy-Download wird gestartet";
item.updatedAt = nowMs();
this.emitState();
logAttemptEvent("WARN", "Proxy-Segmentdownload nicht verwendet, Direktdownload folgt", {
logAttemptEvent("WARN", "Proxy-Segmentdownload nicht verwendet, fester Einzel-Proxy folgt", {
attempt,
reason: proxyReasonLabels[proxyResult.reason]
});
+9 -1
View File
@@ -32,6 +32,7 @@ import {
validateCollectorPersistenceState,
validateCollectorTextPreparationRequest
} from "../shared/collector";
import { getProxyAuthentication } from "./network-proxy";
forceDarkNativeTheme(nativeTheme);
@@ -1271,7 +1272,14 @@ function formatRendererErrorReport(rawReport: unknown): string {
return parts.join(" ");
}
app.on("child-process-gone", (_event, details) => {
app.on("login", (event, _webContents, _requestDetails, authInfo, callback) => {
const credentials = getProxyAuthentication(authInfo);
if (!credentials) return;
event.preventDefault();
callback(credentials.username, credentials.password);
});
app.on("child-process-gone", (_event, details) => {
const killed = details.reason !== "clean-exit" && details.reason !== "killed";
const line = `Subprozess beendet: type=${details.type} reason=${details.reason} exitCode=${details.exitCode ?? "?"}${details.name ? ` name=${details.name}` : ""}${details.serviceName ? ` service=${details.serviceName}` : ""}`;
if (killed) {
+165
View File
@@ -0,0 +1,165 @@
import type { Session } from "electron";
import { getGlobalDispatcher, ProxyAgent, setGlobalDispatcher, type Dispatcher } from "undici";
import type { AppSettings } from "../shared/types";
import { selectFixedProxy, type ProxyEndpoint } from "./proxy-segmented-download";
export type NetworkProxyState =
| { status: "disabled" }
| { status: "active"; selectedIndex: number; proxyCount: number }
| { status: "blocked"; reason: "proxy_file_unavailable" | "no_valid_proxies" | "proxy_index_unavailable" };
interface ActiveProxyConfiguration {
status: "active";
proxy: ProxyEndpoint;
selectedIndex: number;
proxyCount: number;
fingerprint: string;
}
interface BlockedProxyConfiguration {
status: "blocked";
reason: "proxy_file_unavailable" | "no_valid_proxies" | "proxy_index_unavailable";
fingerprint: string;
}
type ProxyConfiguration = { status: "disabled"; fingerprint: "disabled" } | ActiveProxyConfiguration | BlockedProxyConfiguration;
const originalDispatcher = getGlobalDispatcher();
const configuredSessions = new WeakMap<object, string>();
const knownSessions = new Set<Session>();
let currentConfiguration: ProxyConfiguration = { status: "disabled", fingerprint: "disabled" };
let installedDispatcher: Dispatcher | null = null;
function blockedDispatcher(reason: string): Dispatcher {
return {
dispatch: (_options, handler) => {
queueMicrotask(() => handler.onError?.(new Error(`proxy_only_blocked:${reason}`)));
return true;
},
close: async () => {},
destroy: async () => {}
} as Dispatcher;
}
function closeInstalledDispatcher(dispatcher: Dispatcher | null): void {
if (!dispatcher || dispatcher === originalDispatcher) return;
void dispatcher.close().catch(() => {});
}
function installDispatcher(dispatcher: Dispatcher | null): void {
const previous = installedDispatcher;
installedDispatcher = dispatcher;
setGlobalDispatcher(dispatcher || originalDispatcher);
if (previous && previous !== dispatcher) closeInstalledDispatcher(previous);
}
function refreshKnownElectronSessions(): void {
for (const currentSession of knownSessions) {
void configureElectronProxySession(currentSession).catch(() => {});
}
}
export function configureNetworkProxy(settings: Pick<AppSettings, "proxyDownloadEnabled" | "proxyListPath" | "proxyApiProxyIndex">): NetworkProxyState {
if (!settings.proxyDownloadEnabled) {
if (currentConfiguration.status !== "disabled") {
currentConfiguration = { status: "disabled", fingerprint: "disabled" };
installDispatcher(null);
refreshKnownElectronSessions();
}
return { status: "disabled" };
}
const selected = selectFixedProxy(settings.proxyListPath, settings.proxyApiProxyIndex);
if (selected.status !== "ok") {
const fingerprint = `blocked:${selected.status}`;
if (currentConfiguration.fingerprint !== fingerprint) {
currentConfiguration = { status: "blocked", reason: selected.status, fingerprint };
installDispatcher(blockedDispatcher(selected.status));
refreshKnownElectronSessions();
}
return { status: "blocked", reason: selected.status };
}
const fingerprint = `${selected.proxy.url}\0${selected.proxy.authorization}\0${selected.selectedIndex}`;
if (currentConfiguration.fingerprint !== fingerprint) {
const dispatcher = new ProxyAgent({
uri: selected.proxy.url,
...(selected.proxy.authorization ? { token: selected.proxy.authorization } : {})
});
currentConfiguration = {
status: "active",
proxy: selected.proxy,
selectedIndex: selected.selectedIndex,
proxyCount: selected.proxyCount,
fingerprint
};
installDispatcher(dispatcher);
refreshKnownElectronSessions();
}
return { status: "active", selectedIndex: selected.selectedIndex, proxyCount: selected.proxyCount };
}
export function getNetworkProxyState(): NetworkProxyState {
if (currentConfiguration.status === "disabled") return { status: "disabled" };
if (currentConfiguration.status === "blocked") {
return { status: "blocked", reason: currentConfiguration.reason };
}
return {
status: "active",
selectedIndex: currentConfiguration.selectedIndex,
proxyCount: currentConfiguration.proxyCount
};
}
export function getProxyAuthentication(authInfo: { isProxy: boolean; host: string; port: number }): { username: string; password: string } | null {
if (!authInfo.isProxy || currentConfiguration.status !== "active") return null;
const proxy = currentConfiguration.proxy;
if (authInfo.host.toLowerCase() !== proxy.hostname.toLowerCase() || authInfo.port !== proxy.port) return null;
if (!proxy.username && !proxy.password) return null;
return { username: proxy.username, password: proxy.password };
}
export async function configureElectronProxySession(currentSession: Session): Promise<void> {
knownSessions.add(currentSession);
const previousFingerprint = configuredSessions.get(currentSession);
if (previousFingerprint === currentConfiguration.fingerprint) return;
try {
if (currentConfiguration.status === "disabled") {
if (previousFingerprint !== undefined) {
await currentSession.setProxy({ mode: "direct" });
await currentSession.closeAllConnections();
}
} else if (currentConfiguration.status === "active") {
await currentSession.setProxy({
mode: "fixed_servers",
proxyRules: currentConfiguration.proxy.url.replace(/\/$/, "")
});
await currentSession.closeAllConnections();
} else {
await currentSession.setProxy({
mode: "fixed_servers",
proxyRules: "http://127.0.0.1:1"
});
await currentSession.closeAllConnections();
}
configuredSessions.set(currentSession, currentConfiguration.fingerprint);
} catch {
if (currentConfiguration.status !== "disabled") {
await currentSession.setProxy({ mode: "fixed_servers", proxyRules: "http://127.0.0.1:1" }).catch(() => {});
await currentSession.closeAllConnections().catch(() => {});
}
throw new Error("proxy_only_session_configuration_failed");
}
}
export async function shutdownNetworkProxy(): Promise<void> {
const active = installedDispatcher;
installedDispatcher = null;
currentConfiguration = { status: "disabled", fingerprint: "disabled" };
setGlobalDispatcher(originalDispatcher);
knownSessions.clear();
if (active && active !== originalDispatcher) {
await active.close().catch(() => {});
}
}
+58 -16
View File
@@ -15,10 +15,14 @@ const MAX_REDIRECTS = 5;
const MAX_PROXY_FILE_BYTES = 8 * 1024 * 1024;
const DISK_ERROR_CODES = new Set(["ENOSPC", "EDQUOT", "EACCES", "EPERM", "EROFS", "EIO", "ENODEV"]);
interface ProxyEndpoint {
export interface ProxyEndpoint {
id: number;
url: string;
authorization: string;
username: string;
password: string;
hostname: string;
port: number;
}
interface CachedProxyFile {
@@ -110,9 +114,11 @@ function parseProxyLine(rawLine: string, id: number): ProxyEndpoint | null {
return null;
}
let authorization = "";
let username = "";
let password = "";
if (parsed.username || parsed.password) {
const username = decodeURIComponent(parsed.username);
const password = decodeURIComponent(parsed.password);
username = decodeURIComponent(parsed.username);
password = decodeURIComponent(parsed.password);
authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
parsed.username = "";
parsed.password = "";
@@ -120,7 +126,15 @@ function parseProxyLine(rawLine: string, id: number): ProxyEndpoint | null {
parsed.pathname = "";
parsed.search = "";
parsed.hash = "";
return { id, url: parsed.toString(), authorization };
return {
id,
url: parsed.toString(),
authorization,
username,
password,
hostname: parsed.hostname,
port
};
} catch {
return null;
}
@@ -133,6 +147,45 @@ export function parseProxyList(content: string): number {
.reduce((count, line, index) => count + (parseProxyLine(line, index) ? 1 : 0), 0);
}
function parseUniqueProxyEndpoints(content: string): ProxyEndpoint[] {
const seen = new Set<string>();
return content
.replace(/^\uFEFF/, "")
.split(/\r?\n/)
.map((line, index) => parseProxyLine(line, index))
.filter((proxy): proxy is ProxyEndpoint => {
if (!proxy) return false;
const key = `${proxy.url}\0${proxy.authorization}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
export type FixedProxySelection =
| { status: "ok"; proxy: ProxyEndpoint; selectedIndex: number; proxyCount: number }
| { status: "proxy_file_unavailable" | "no_valid_proxies" | "proxy_index_unavailable" };
export function selectFixedProxy(proxyListPath: string, requestedIndex: number): FixedProxySelection {
const filePath = String(proxyListPath || "").trim();
if (!filePath) return { status: "proxy_file_unavailable" };
try {
const normalizedPath = path.resolve(filePath);
const stat = fs.statSync(normalizedPath);
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_PROXY_FILE_BYTES) {
return { status: stat.size <= 0 ? "no_valid_proxies" : "proxy_file_unavailable" };
}
const proxies = parseUniqueProxyEndpoints(fs.readFileSync(normalizedPath, "utf8"));
if (proxies.length === 0) return { status: "no_valid_proxies" };
const selectedIndex = Math.max(1, Math.floor(requestedIndex || 1));
const proxy = proxies[selectedIndex - 1];
if (!proxy) return { status: "proxy_index_unavailable" };
return { status: "ok", proxy, selectedIndex, proxyCount: proxies.length };
} catch {
return { status: "proxy_file_unavailable" };
}
}
async function loadProxyFile(filePath: string): Promise<
{ status: "ok"; proxies: ProxyEndpoint[] }
| { status: "unavailable" }
@@ -151,18 +204,7 @@ async function loadProxyFile(filePath: string): Promise<
: { status: "empty" };
}
const content = await fs.promises.readFile(normalizedPath, "utf8");
const seen = new Set<string>();
const proxies = content
.replace(/^\uFEFF/, "")
.split(/\r?\n/)
.map((line, index) => parseProxyLine(line, index))
.filter((proxy): proxy is ProxyEndpoint => {
if (!proxy) return false;
const key = `${proxy.url}\0${proxy.authorization}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
const proxies = parseUniqueProxyEndpoints(content);
proxyFileCache.set(normalizedPath, { mtimeMs: stat.mtimeMs, size: stat.size, proxies });
return proxies.length > 0 ? { status: "ok", proxies } : { status: "empty" };
} catch {
+9 -5
View File
@@ -4,6 +4,7 @@ import { sleep } from "./utils";
import { API_BASE_URL, REQUEST_RETRIES } from "./constants";
import { applyRemoteLoginSecurity, createRemoteLoginWebPreferences, REALDEBRID_LOGIN_HOSTS } from "./browser-security";
import { buildRealDebridWebGenerationScript, normalizeRealDebridWebGenerationResult } from "./realdebrid-web-page";
import { configureElectronProxySession } from "./network-proxy";
const RD_BASE_URL = "https://real-debrid.com";
const RD_LOGIN_URL = RD_BASE_URL;
@@ -366,9 +367,10 @@ export class RealDebridWebFallback {
return raceWithAbort(run, signal);
}
private async ensureLoginWindow(): Promise<BrowserWindow> {
const partition = this.getPartition();
const existing = this.loginWindow;
private async ensureLoginWindow(): Promise<BrowserWindow> {
const partition = this.getPartition();
await configureElectronProxySession(session.fromPartition(partition));
const existing = this.loginWindow;
if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) {
return existing;
}
@@ -431,6 +433,7 @@ export class RealDebridWebFallback {
this.throwIfDisposed();
throwIfAborted(signal);
const partition = this.getPartition();
await configureElectronProxySession(session.fromPartition(partition));
const existing = this.generatorWindow;
if (existing && !existing.isDestroyed() && this.generatorWindowPartition === partition) {
return existing;
@@ -603,8 +606,9 @@ export class RealDebridWebFallback {
}
}
const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(RD_APITOKEN_URL, {
const currentSession = session.fromPartition(this.getPartition());
await configureElectronProxySession(currentSession);
const response = await currentSession.fetch(RD_APITOKEN_URL, {
headers: {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Referer: RD_BASE_URL + "/",
+1
View File
@@ -206,6 +206,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
speedLimitMode: settings.speedLimitMode,
proxyDownloadEnabled: settings.proxyDownloadEnabled,
proxyListPath: settings.proxyListPath,
proxyApiProxyIndex: settings.proxyApiProxyIndex,
proxyConnectionsPerDownload: settings.proxyConnectionsPerDownload,
updateRepo: settings.updateRepo,
autoUpdateCheck: settings.autoUpdateCheck,
+1
View File
@@ -614,6 +614,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
speedLimitMode: settings.speedLimitMode,
proxyDownloadEnabled: Boolean(settings.proxyDownloadEnabled),
proxyListPath: asText(settings.proxyListPath).slice(0, 4096),
proxyApiProxyIndex: clampNumber(settings.proxyApiProxyIndex, defaults.proxyApiProxyIndex, 1, 100000),
proxyConnectionsPerDownload: clampNumber(settings.proxyConnectionsPerDownload, defaults.proxyConnectionsPerDownload, 2, 32),
autoUpdateCheck: Boolean(settings.autoUpdateCheck),
updateRepo: migrateUpdateRepo(asText(settings.updateRepo), defaults.updateRepo),
+2 -1
View File
@@ -953,7 +953,7 @@ const emptySnapshot = (): UiSnapshot => ({
removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true,
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
proxyDownloadEnabled: false, proxyListPath: "", proxyConnectionsPerDownload: 16,
proxyDownloadEnabled: false, proxyListPath: "", proxyApiProxyIndex: 1, proxyConnectionsPerDownload: 16,
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
theme: "dark", themePreference: "dark", logStorageLocation: "appdata", collapseNewPackages: true, animatePackageDisclosure: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: false, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, backupIncludeRemoteDiagnostics: false,
notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
@@ -6066,6 +6066,7 @@ export function App(): ReactElement {
historyMaxAgeDays: [0, 3650, 0],
maxParallelExtract: [1, 8, 2],
reconnectWaitSeconds: [10, 600, 45],
proxyApiProxyIndex: [1, 100000, 1],
proxyConnectionsPerDownload: [2, 32, 16]
};
const bounds = numericLimits[fieldId as keyof RendererSettingsDraft];
+15 -5
View File
@@ -424,15 +424,15 @@ export function buildSettingsFormViewModel({
},
{
id: "speed-proxy",
title: "Proxy-Download",
description: "Teilt neue Downloads in Byte-Bereiche und lädt sie parallel über verschiedene HTTP-Proxys. Falls der Server keine Bereiche unterstützt oder die Proxys ausfallen, läuft der bestehende Direktdownload weiter.",
title: "Proxy-only",
description: "Leitet API-, Login- und Link-Anfragen über einen festen HTTP-Proxy. Downloads verwenden zusätzlich mehrere Proxys parallel. Bei aktivem Modus gibt es keinen direkten Rückfall über deine echte Verbindung.",
fields: [
{
id: "proxyDownloadEnabled",
kind: "switch",
label: "Proxy-Download aktivieren",
label: "Proxy-only aktivieren",
value: settings.proxyDownloadEnabled,
help: "Gilt für neue Dateien ab 8 MiB. Ein aktives Geschwindigkeitslimit verwendet weiterhin den Direktdownload."
help: "Ist der feste API-Proxy nicht erreichbar oder ungültig, schlagen Netzwerkanfragen geschlossen fehl. Es wird nie ungefragt direkt verbunden."
},
{
id: "proxyListPath",
@@ -444,6 +444,16 @@ export function buildSettingsFormViewModel({
disabled: !settings.proxyDownloadEnabled,
help: "Unterstützt unter anderem Benutzer:Passwort@Host:Port. Zugangsdaten werden nicht protokolliert."
},
{
id: "proxyApiProxyIndex",
kind: "number",
label: "Fester API-Proxy (gültiger Listeneintrag)",
value: String(settings.proxyApiProxyIndex),
min: 1,
max: 100000,
disabled: !settings.proxyDownloadEnabled,
help: "Dieser 1-basierte Eintrag bleibt für API, Login und Link-Auflösung fest. Ist er offline, wird nicht automatisch ein anderer Proxy oder die direkte Verbindung verwendet."
},
{
id: "proxyConnectionsPerDownload",
kind: "number",
@@ -452,7 +462,7 @@ export function buildSettingsFormViewModel({
min: 2,
max: 32,
disabled: !settings.proxyDownloadEnabled,
help: `Bei ${settings.maxParallel} gleichzeitigen Downloads sind bis zu ${settings.maxParallel * settings.proxyConnectionsPerDownload} Proxy-Verbindungen möglich.`
help: `Bei ${settings.maxParallel} gleichzeitigen Downloads sind bis zu ${settings.maxParallel * settings.proxyConnectionsPerDownload} parallele Segmentverbindungen möglich. Kleine Dateien, Teil-Downloads und aktive Geschwindigkeitslimits laufen über den festen API-Proxy.`
}
]
},
+1
View File
@@ -142,6 +142,7 @@ export interface DailyStartSettings {
export interface ProxyDownloadSettings {
proxyDownloadEnabled: boolean;
proxyListPath: string;
proxyApiProxyIndex: number;
proxyConnectionsPerDownload: number;
}