release: prepare v2.0.82

This commit is contained in:
Sucukdeluxe
2026-09-01 00:13:30 +02:00
parent d58cfba817
commit e7639bc4df
23 changed files with 313 additions and 59 deletions
+14
View File
@@ -4,6 +4,20 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
## [Unreleased]
## [2.0.82] - 2026-09-01
### Startup work folders
- Add a disabled-by-default General setting that recreates a missing download folder when the application starts.
- Also recreate the configured extraction and video-library folders when their corresponding features are enabled.
- Preserve every existing directory and file, and report an unavailable target without blocking the remaining startup folders or the application launch.
### Settings and large queues
- Rename the Speed settings category to Speed & Proxy so Proxy-only configuration is easier to locate.
- Increase the visible precision of remaining sizes above one TiB so the Remaining counter visibly keeps pace with Session on very large queues while retaining the shared 750 ms snapshot cadence.
- Keep download, extraction, password, and post-processing behavior unchanged.
## [2.0.81] - 2026-08-31
### Account management
+32 -16
View File
@@ -8,15 +8,15 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
## Zuletzt verifizierter Stand
- Verifiziert am: 31. August 2026, Europe/Berlin
- Verifiziert am: 1. September 2026, Europe/Berlin
- Lokaler Pfad: `C:\Users\Sascha\Desktop\Claude & ChatGPT Projekte\Multi-Debrid-Downloader`
- Arbeitsbranch: `release/v2.0.81`
- Quellbasis: `fix/account-select-all`
- Release-Tag: `v2.0.80`
- Baseline-Commit: `486507d4b2f42e9b0ccad9f2a026919d590149a0`
- Arbeitsbranch: `release/v2.0.82`
- Quellbasis: `release/v2.0.81`
- Release-Tag: `v2.0.81`
- Baseline-Commit: `d58cfba817bd73d015c3c295e593082becc3211f`
- Hotfix-Basis: `5cee459d0bcc5c19d8d6483948a036a43c9d91b1`
- Paketversion: `2.0.81`
- Letztes Release: `Multi-Debrid-Downloader v2.0.80`, veröffentlicht am 31. August 2026 auf GitHub und Forgejo
- Paketversion: `2.0.82`
- Letztes Release: `Multi-Debrid-Downloader v2.0.81`, veröffentlicht am 31. August 2026 auf GitHub; die Forgejo-Veröffentlichung wird zusammen mit `v2.0.82` nachgeholt
- Runtime-Voraussetzung: Node.js `>=20`; lokal verifiziert mit Node.js `24.19.0` und npm `11.17.0`
`main` ist derzeit keine verlässliche Arbeitsbasis:
@@ -173,7 +173,15 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
- Online-Sicherungen nehmen den exakten Inhalt einer konfigurierten Proxy-Liste in den bereits clientseitig AES-256-GCM-verschlüsselten MDD2-Datensatz auf. Der Online-Dienst erhält weiterhin ausschließlich den authentifiziert verschlüsselten Blob und keine Proxy-Zugangsdaten im Klartext.
- Beim Import wird die Liste atomar in eine verwaltete Datei im Runtime-Verzeichnis geschrieben und der lokale Pfad darauf umgestellt. Ein Schreib- oder Settingsfehler stellt sowohl die vorherige Datei als auch die vorherigen Einstellungen wieder her.
- Ältere Online-Sicherungen ohne eingebettete Liste bleiben importierbar. Enthalten sie einen fremden Proxy-Pfad beziehungsweise aktiviertes Proxy-only, wird Proxy-only beim Import sicher deaktiviert, statt alle Netzwerkanfragen mit einem nicht portablen Pfad zu blockieren. Lokale Backup-Importe behalten ihre bisherige Pfadsemantik.
- Die Änderungen sind auf `release/v2.0.81` versioniert und für die ausdrücklich freigegebene Veröffentlichung vorbereitet, aber noch nicht veröffentlicht. Entpack-, Passwort- und Nachbearbeitungslogik wurden nicht verändert.
- Die Änderungen sind als `v2.0.81` auf GitHub veröffentlicht; die Forgejo-Veröffentlichung steht noch aus. Entpack-, Passwort- und Nachbearbeitungslogik wurden nicht verändert.
## Änderungen in v2.0.82
- Unter „Allgemein“ gibt es die standardmäßig ausgeschaltete Option „Fehlende Arbeitsordner beim Start anlegen“. Sie legt beim Programmstart einen fehlenden Download-Ordner an und berücksichtigt den Entpack- beziehungsweise Videosammelordner nur, wenn die jeweilige Funktion aktiviert ist.
- Vorhandene Ordner und Inhalte bleiben unberührt. Fehler eines einzelnen Ziels, etwa ein nicht erreichbares Netzlaufwerk, eine Datei am Zielpfad oder fehlende Berechtigung, werden protokolliert und blockieren weder weitere Ordner noch den Programmstart.
- Der Einstellungsbereich „Geschwindigkeit“ heißt nun „Geschwindigkeit & Proxy“; zugehörige Proxy-only-Hinweise und die englische Oberfläche verweisen ebenfalls auf den neuen Namen.
- „Sitzung“ und „Verbleibend“ stammen weiterhin aus demselben 750-Millisekunden-Snapshot. Bei Restmengen ab einem TiB wurde die sichtbare TB-Präzision von vier auf fünf Nachkommastellen erhöht: Ein Anzeigeschritt sinkt dadurch von rund 105 MiB auf rund 10,5 MiB und bleibt auch bei großen Queues sichtbar im gleichen Takt.
- Download-, Entpack-, Passwort- und Nachbearbeitungslogik wurden nicht verändert.
## Start-, Build- und Testbefehle
@@ -204,6 +212,12 @@ npm exec -- tsc --noEmit
`npm run release:win` baut Release-Artefakte und darf nur nach ausdrücklicher Release-Freigabe ausgeführt werden.
## Verifizierungen vom 1. September 2026
- Kandidat für `v2.0.82`: TypeScript-Prüfung und 616 fokussierte Ordner-, Storage-, Renderer-, Einstellungs-, Übersetzungs-, Account- und Downloadansichtstests erfolgreich. Die Ordnerregressionen bestätigen den ausgeschalteten Standard, selektive Zielerstellung, den Erhalt vorhandener Dateien und die Fehlerisolation zwischen Zielen.
- Vollständiger Clientlauf: 143 Testdateien erfolgreich, 1 optionale JVM-Testdatei übersprungen; 2.725 Tests erfolgreich und 4 übersprungen. Nur die zwei bekannten Windows-Symlink-Fixtures endeten mangels Berechtigung vor ihrer Produktassertion mit `EPERM`. Die unveränderten 265 Download-Manager- und 84 Entpacktests sind vollständig grün.
- Backup-API: 16 von 16 Tests erfolgreich.
## Verifizierungen vom 31. August 2026
- Release-Build `v2.0.81`: Installer und Portable-Datei wurden aus dem versionierten Release-Vorbereitungsstand neu erzeugt. Die Release-Prüfung bestätigte Paket- und Bundle-Version, Update-Metadaten, Artefaktnamen, Dateigröße und SHA-512 des Installers, Icon, Lizenzdateien sowie den entpackten Inhalt beider EXE-Archive. Von den 24 Public-Release-Metadatentests waren 22 erfolgreich; nur die zwei unter Windows ohne Symlink-Berechtigung nicht ausführbaren Fixtures endeten vor ihrer Produktassertion mit `EPERM`.
@@ -295,14 +309,16 @@ npm exec -- tsc --noEmit
- Der 32/40-Proxybereich und der synchrone Live-Takt von „Sitzung“ und „Verbleibend“ sind als `v2.0.78` veröffentlicht. Eine Serverinstallation oder ein produktiver Neustart wurde nicht vorgenommen.
- Der Proxy-Readback- und Nullbereich-Hotfix ist als `v2.0.79` auf GitHub und Forgejo veröffentlicht, aber noch nicht auf einem produktiven System installiert oder gestartet.
- Die einheitliche Geschwindigkeitsanzeige und die getrennten Paket-Reset-Optionen sind als `v2.0.80` auf GitHub und Forgejo veröffentlicht, aber noch nicht auf einem produktiven System installiert oder gestartet.
- Die Account-Übersicht unterstützt `Strg+A`; Proxy-only-Accountfehler werden verständlich aufgelöst und Online-Sicherungen können die Proxy-Liste verschlüsselt portieren. `release/v2.0.81` ist für die freigegebene Veröffentlichung vorbereitet, aber noch nicht veröffentlicht oder installiert.
- Die Account-Übersicht unterstützt `Strg+A`; Proxy-only-Accountfehler werden verständlich aufgelöst und Online-Sicherungen können die Proxy-Liste verschlüsselt portieren. `v2.0.81` ist auf GitHub veröffentlicht; die Forgejo-Veröffentlichung steht noch aus. Eine produktive Installation oder ein Neustart wurde nicht vorgenommen.
- Die optionale Arbeitsordner-Erstellung, der neue Kategoriename und die feinere TiB-Restanzeige sind als `v2.0.82` vollständig getestet und zur ausdrücklich freigegebenen Veröffentlichung vorbereitet. Eine produktive Installation oder ein Neustart ist nicht Bestandteil des Releases.
## Nächste sinnvolle Schritte
1. Den vollständig geprüften und freigegebenen Kandidaten als `v2.0.81` bauen, auf GitHub und Forgejo veröffentlichen und die Assets beider Plattformen gegeneinander verifizieren.
2. Nach einer getrennt freigegebenen Serverinstallation die Account-Fehlerführung, den verschlüsselten Proxy-Listen-Import und `Strg+A` am echten Zielsystem prüfen.
3. Nach einer getrennt freigegebenen Serverinstallation die einheitliche Geschwindigkeitsanzeige und den selektiven Paket-Reset am echten Queue-Fall prüfen.
4. Nach einer getrennt freigegebenen Serverinstallation den Ablauf Real-Debrid-Web-Download, Hauptfenster schließen und unmittelbar neu starten am echten Zielsystem verifizieren; vor jedem Eingriff Prozessbaum und Logtail sichern.
5. Sicherheitsabhängigkeiten in einem separaten Upgrade-Branch aktualisieren, Electron-/Vite-/Vitest-Major-Wechsel einzeln testen und danach den vollständigen Windows-Paketpfad prüfen.
6. Eine nichtdestruktive Strategie zur Bereinigung der divergierenden `main`-Branches abstimmen; kein Force-Push ohne ausdrückliche Freigabe.
7. Optional Developer Mode für lokale Symlink-Tests bereitstellen oder die Test-Fixtures plattformgerecht ohne privilegierte Symlinks gestalten.
1. Den vollständig geprüften und freigegebenen Kandidaten als `v2.0.82` bauen, auf GitHub und Forgejo veröffentlichen und die Assets beider Plattformen gegeneinander verifizieren; dabei die ausstehende Forgejo-Veröffentlichung von `v2.0.81` nachholen.
2. Nach einer getrennt freigegebenen Serverinstallation die neue Arbeitsordner-Option und die feinere Restanzeige mit der großen echten Queue prüfen.
3. Nach einer getrennt freigegebenen Serverinstallation die Account-Fehlerführung, den verschlüsselten Proxy-Listen-Import und `Strg+A` am echten Zielsystem prüfen.
4. Nach einer getrennt freigegebenen Serverinstallation die einheitliche Geschwindigkeitsanzeige und den selektiven Paket-Reset am echten Queue-Fall prüfen.
5. Nach einer getrennt freigegebenen Serverinstallation den Ablauf Real-Debrid-Web-Download, Hauptfenster schließen und unmittelbar neu starten am echten Zielsystem verifizieren; vor jedem Eingriff Prozessbaum und Logtail sichern.
6. Sicherheitsabhängigkeiten in einem separaten Upgrade-Branch aktualisieren, Electron-/Vite-/Vitest-Major-Wechsel einzeln testen und danach den vollständigen Windows-Paketpfad prüfen.
7. Eine nichtdestruktive Strategie zur Bereinigung der divergierenden `main`-Branches abstimmen; kein Force-Push ohne ausdrückliche Freigabe.
8. Optional Developer Mode für lokale Symlink-Tests bereitstellen oder die Test-Fixtures plattformgerecht ohne privilegierte Symlinks gestalten.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "multi-debrid-downloader",
"version": "2.0.81",
"version": "2.0.82",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-debrid-downloader",
"version": "2.0.81",
"version": "2.0.82",
"license": "MIT",
"dependencies": {
"adm-zip": "0.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "multi-debrid-downloader",
"version": "2.0.81",
"version": "2.0.82",
"description": "Desktop downloader",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",
+8
View File
@@ -88,6 +88,7 @@ import { normalizeStatisticsLedger, saveStatisticsLedger } from "./statistics-le
import { NotificationOutbox } from "./notification-outbox";
import { sendNotification } from "./notify";
import { DownloadHealthMonitor } from "./download-health-monitor";
import { ensureStartupWorkDirectories } from "./startup-work-directories";
import { shouldDeferAutoResumeToDailyStart } from "./daily-start-scheduler";
import { configureNetworkProxy, getNetworkProxyState, shutdownNetworkProxy } from "./network-proxy";
import { createProxyOnlyAccountError, resolveProxyOnlyAccountErrorCode } from "./proxy-account-errors";
@@ -173,6 +174,13 @@ export class AppController {
}
this.applyNetworkProxyConfiguration();
this.initializeLogStorage();
const workDirectories = ensureStartupWorkDirectories(this.settings);
if (workDirectories.created.length > 0) {
logger.info(`Arbeitsordner beim Start angelegt: ${workDirectories.created.map((entry) => entry.kind).join(", ")}`);
}
for (const failure of workDirectories.failures) {
logger.warn(`Arbeitsordner konnte beim Start nicht angelegt werden (${failure.kind}): ${failure.error}`);
}
this.runHistoryLifecycleCleanup("Start", () => resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode));
const loadResult = loadSessionWithStatus(this.storagePaths);
const session = loadResult.session;
+1
View File
@@ -81,6 +81,7 @@ export function defaultSettings(): AppSettings {
providerTertiary: "bestdebrid",
autoProviderFallback: true,
outputDir: baseDir,
createWorkDirectoriesOnStartup: false,
packageName: "",
autoExtract: true,
autoRename4sf4sj: false,
+1
View File
@@ -179,6 +179,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
providerTertiary: settings.providerTertiary,
autoProviderFallback: settings.autoProviderFallback,
outputDir: settings.outputDir,
createWorkDirectoriesOnStartup: settings.createWorkDirectoriesOnStartup,
packageName: settings.packageName,
autoExtract: settings.autoExtract,
autoRename4sf4sj: settings.autoRename4sf4sj,
+57
View File
@@ -0,0 +1,57 @@
import fs from "node:fs";
import path from "node:path";
import type { AppSettings } from "../shared/types";
export type StartupWorkDirectoryKind = "download" | "extract" | "video-library";
export interface StartupWorkDirectoryEntry {
kind: StartupWorkDirectoryKind;
directory: string;
}
export interface StartupWorkDirectoryFailure extends StartupWorkDirectoryEntry {
error: string;
}
export interface StartupWorkDirectoryResult {
created: StartupWorkDirectoryEntry[];
existing: StartupWorkDirectoryEntry[];
failures: StartupWorkDirectoryFailure[];
}
function directoryKey(directory: string): string {
const resolved = path.resolve(directory);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}
export function ensureStartupWorkDirectories(settings: AppSettings): StartupWorkDirectoryResult {
const result: StartupWorkDirectoryResult = { created: [], existing: [], failures: [] };
if (!settings.createWorkDirectoriesOnStartup) {
return result;
}
const candidates: StartupWorkDirectoryEntry[] = [
{ kind: "download", directory: settings.outputDir },
...(settings.autoExtract ? [{ kind: "extract" as const, directory: settings.extractDir }] : []),
...(settings.collectMkvToLibrary ? [{ kind: "video-library" as const, directory: settings.mkvLibraryDir }] : [])
];
const seen = new Set<string>();
for (const candidate of candidates) {
const entry = { ...candidate, directory: path.resolve(candidate.directory) };
const key = directoryKey(entry.directory);
if (seen.has(key)) {
continue;
}
seen.add(key);
const existed = fs.existsSync(entry.directory);
try {
fs.mkdirSync(entry.directory, { recursive: true });
(existed ? result.existing : result.created).push(entry);
} catch (error) {
result.failures.push({ ...entry, error: String(error) });
}
}
return result;
}
+3
View File
@@ -587,6 +587,9 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
providerTertiary: normalizeFallbackProvider(settings.providerTertiary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled),
autoProviderFallback: Boolean(settings.autoProviderFallback),
outputDir: normalizeAbsoluteDir(directorySettings.outputDir, defaults.outputDir),
createWorkDirectoriesOnStartup: settings.createWorkDirectoriesOnStartup !== undefined
? Boolean(settings.createWorkDirectoriesOnStartup)
: defaults.createWorkDirectoriesOnStartup,
packageName: asText(settings.packageName),
autoExtract: Boolean(settings.autoExtract),
autoRename4sf4sj: Boolean(settings.autoRename4sf4sj),
+1 -1
View File
@@ -946,7 +946,7 @@ const emptySnapshot = (): UiSnapshot => ({
debridLinkDisabledKeyIds: [],
archivePasswordListConfigured: false, notifyUrlConfigured: false,
rememberToken: true, configuredProviders: [], providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
providerTertiary: "none", autoProviderFallback: true, outputDir: "", packageName: "",
providerTertiary: "none", autoProviderFallback: true, outputDir: "", createWorkDirectoriesOnStartup: false, packageName: "",
autoExtract: true, autoRename4sf4sj: false, keepGermanAudioOnly: false, germanAudioMode: "tag", extractDir: "", createExtractSubfolder: true, hybridExtract: true,
collectMkvToLibrary: false, mkvLibraryDir: "",
cleanupMode: "none", extractConflictMode: "overwrite", removeLinkFilesAfterExtract: false,
+4 -4
View File
@@ -1,11 +1,11 @@
import type { DebridProvider, RendererSettings, RendererSettingsUpdate } from "../shared/types";
const proxyOnlyAccountMessages = {
proxy_list_missing: "Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit.",
proxy_list_unreadable: "Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste kann nicht gelesen werden. Prüfe die Datei unter Einstellungen → Geschwindigkeit.",
proxy_list_missing: "Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit & Proxy.",
proxy_list_unreadable: "Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste kann nicht gelesen werden. Prüfe die Datei unter Einstellungen → Geschwindigkeit & Proxy.",
proxy_list_empty: "Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste ist leer oder enthält keine gültigen HTTP-Proxys.",
proxy_index_unavailable: "Proxy-only ist aktiviert, aber der feste API-Proxy ist in der Liste nicht verfügbar. Prüfe den Listeneintrag unter Einstellungen → Geschwindigkeit.",
proxy_unreachable: "Proxy-only ist aktiviert, aber der feste API-Proxy ist nicht erreichbar oder lehnt die Verbindung ab. Prüfe den Proxy unter Einstellungen → Geschwindigkeit."
proxy_index_unavailable: "Proxy-only ist aktiviert, aber der feste API-Proxy ist in der Liste nicht verfügbar. Prüfe den Listeneintrag unter Einstellungen → Geschwindigkeit & Proxy.",
proxy_unreachable: "Proxy-only ist aktiviert, aber der feste API-Proxy ist nicht erreichbar oder lehnt die Verbindung ab. Prüfe den Proxy unter Einstellungen → Geschwindigkeit & Proxy."
} as const;
export function formatAccountOperationError(prefix: string, error: unknown): string {
+7 -7
View File
@@ -5,12 +5,12 @@ const pairs = [
["Datei", "File"], ["Hilfe", "Help"], ["Kontomenü", "Account menu"], ["Allgemein", "General"], ["Accounts", "Accounts"],
["Hauptnavigation", "Main navigation"], ["Globale Aktionen", "Global actions"], ["Anwendungsmenü", "Application menu"], ["Seitenleiste einklappen", "Collapse sidebar"], ["Seitenleiste ausklappen", "Expand sidebar"],
["Aktuelle Download-Geschwindigkeit (geglättet)", "Current download speed (smoothed)"], ["Einstellungsbereich", "Settings area"],
["Entpacken", "Extraction"], ["Geschwindigkeit", "Speed"], ["Bereinigung", "Cleanup"], ["Updates", "Updates"],
["Entpacken", "Extraction"], ["Geschwindigkeit", "Speed"], ["Geschwindigkeit & Proxy", "Speed & Proxy"], ["Bereinigung", "Cleanup"], ["Updates", "Updates"],
["Einstellungen speichern", "Save settings"], ["Änderungen verwerfen", "Discard changes"], ["Stellt den letzten gespeicherten Stand wieder her.", "Restores the last saved settings."], ["Ungespeicherte Änderungen verworfen", "Unsaved changes discarded"], ["Zwischenstand gespeichert weitere Änderungen sind ungespeichert", "Progress saved additional changes remain unsaved"], ["Gespeichert", "Saved"], ["Ungespeicherte Änderungen", "Unsaved changes"], ["Wird gespeichert…", "Saving…"], ["Speichern fehlgeschlagen", "Save failed"],
["Sprache", "Language"], ["Speicherort", "Storage location"], ["Download-Verhalten", "Download behavior"], ["Oberfläche und Bedienung", "Interface and controls"], ["Discord-Benachrichtigungen", "Discord notifications"],
["Speicherort, Download-Verhalten, Verlauf, Oberfläche und Benachrichtigungen.", "Storage location, download behavior, history, interface and notifications."], ["Hell", "Light"], ["Dunkel", "Dark"],
["Download-Ordner", "Download folder"], ["Paketname (optional)", "Package name (optional)"], ["Max. gleichzeitige Downloads", "Max. concurrent downloads"], ["Automatische Wiederholungen", "Automatic retries"],
["Zielordner für heruntergeladene Dateien.", "Destination folder for downloaded files."],
["Zielordner für heruntergeladene Dateien.", "Destination folder for downloaded files."], ["Fehlende Arbeitsordner beim Start anlegen", "Create missing work folders on startup"], ["Erstellt den Download-Ordner sowie bei aktiver Funktion den Entpack- und Videosammelordner neu. Vorhandene Ordner und Inhalte bleiben unverändert.", "Creates the download folder and, when enabled, the extraction and video library folders. Existing folders and contents remain unchanged."],
["Beim Start automatisch fortsetzen", "Resume automatically on startup"], ["Zwischenablage überwachen", "Monitor clipboard"], ["Verlauf speichern", "Save history"], ["Nur aktuelle Session", "Current session only"], ["Nur letzte 100 Einträge", "Last 100 entries only"], ["Nur letzte 250 Einträge", "Last 250 entries only"], ["Dauerhaft", "Permanent"],
["Maximale Verlauf-Einträge", "Maximum history entries"], ["Einträge löschen älter als (Tage)", "Delete entries older than (days)"], ["Neue Pakete eingeklappt zeigen", "Show new packages collapsed"], ["Animationen", "Animations"],
["In den Infobereich minimieren", "Minimize to tray"], ["Vor dem Löschen nachfragen", "Confirm before deleting"], ["Download-Liste mitsichern", "Include download list in backup"],
@@ -46,7 +46,7 @@ const pairs = [
["Ablageform", "Output layout"], ["Automatisch umbenennen", "Rename automatically"], ["In Paket-Unterordner ablegen", "Store in package subfolder"], ["Videos in Sammelordner verschieben", "Move videos to library folder"],
["Video-Sammelordner", "Video library folder"], ["Leistung", "Performance"], ["Hybrid-Entpacken", "Hybrid extraction"], ["Gleichzeitige Entpackungen", "Concurrent extractions"], ["CPU-Priorität beim Entpacken", "CPU priority during extraction"],
["Passwörter", "Passwords"], ["Passwortliste für Archive", "Archive password list"], ["Ein Passwort pro Zeile", "One password per line"], ["Hoch (80% CPU)", "High (80% CPU)"], ["Mittel (50% CPU)", "Medium (50% CPU)"], ["Niedrig (25% CPU)", "Low (25% CPU)"],
["Tempo, Wiederverbindung und zeitgesteuerte Bandbreitenregeln.", "Speed, reconnection and scheduled bandwidth rules."], ["Tempo-Begrenzung", "Speed limit"], ["Geschwindigkeit begrenzen", "Limit speed"],
["Tempo, Proxy, Wiederverbindung und zeitgesteuerte Bandbreitenregeln.", "Speed, proxy, reconnection and scheduled bandwidth rules."], ["Tempo-Begrenzung", "Speed limit"], ["Geschwindigkeit begrenzen", "Limit speed"],
["Höchstgeschwindigkeit (MB/s)", "Maximum speed (MB/s)"], ["Limit gilt für", "Limit applies to"], ["Verbindung", "Connection"], ["Automatisch neu verbinden", "Reconnect automatically"], ["Wartezeit vor neuem Versuch (Sek.)", "Wait before retry (sec.)"],
["Von (Stunde)", "From (hour)"], ["Bis (Stunde)", "To (hour)"], ["Limit (MB/s)", "Limit (MB/s)"], ["Zeitregel aktiviert", "Schedule rule enabled"], ["Zeitregel entfernen", "Remove schedule rule"],
["Bandbreitenplanung", "Bandwidth schedule"], ["Jede Regel legt für ein Zeitfenster ein eigenes Limit fest.", "Each rule sets a separate limit for a time window."], ["Weitere Zeitregel", "Additional schedule rule"], ["Zeitregel hinzufügen", "Add schedule rule"],
@@ -197,11 +197,11 @@ const pairs = [
["DDownload Login", "DDownload login"], ["Debrid-Link API", "Debrid-Link API"], ["LinkSnappy Web-Login", "LinkSnappy web login"],
["Tageslimit erreicht. Neue Links wechseln auf den nächsten Hoster.", "Daily limit reached. New links will switch to the next hoster."], ["Mega-Debrid: Bitte Login und Passwort eintragen.", "Mega-Debrid: Enter a login and password."],
["Dieser Mega-Debrid-Account ist bereits vorhanden.", "This Mega-Debrid account already exists."], ["Debrid-Link: Bitte genau einen API-Key eintragen.", "Debrid-Link: Enter exactly one API key."],
["Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but no proxy list is configured. Add one under Settings → Speed."],
["Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste kann nicht gelesen werden. Prüfe die Datei unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but the configured proxy list cannot be read. Check the file under Settings → Speed."],
["Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but no proxy list is configured. Add one under Settings → Speed & Proxy."],
["Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste kann nicht gelesen werden. Prüfe die Datei unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but the configured proxy list cannot be read. Check the file under Settings → Speed & Proxy."],
["Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste ist leer oder enthält keine gültigen HTTP-Proxys.", "Proxy-only is enabled, but the configured proxy list is empty or contains no valid HTTP proxies."],
["Proxy-only ist aktiviert, aber der feste API-Proxy ist in der Liste nicht verfügbar. Prüfe den Listeneintrag unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but the fixed API proxy is not available in the list. Check the list entry under Settings → Speed."],
["Proxy-only ist aktiviert, aber der feste API-Proxy ist nicht erreichbar oder lehnt die Verbindung ab. Prüfe den Proxy unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but the fixed API proxy is unreachable or refuses the connection. Check the proxy under Settings → Speed."],
["Proxy-only ist aktiviert, aber der feste API-Proxy ist in der Liste nicht verfügbar. Prüfe den Listeneintrag unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but the fixed API proxy is not available in the list. Check the list entry under Settings → Speed & Proxy."],
["Proxy-only ist aktiviert, aber der feste API-Proxy ist nicht erreichbar oder lehnt die Verbindung ab. Prüfe den Proxy unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but the fixed API proxy is unreachable or refuses the connection. Check the proxy under Settings → Speed & Proxy."],
["Die Prüfung hat nicht genau den neuen Account bestätigt.", "The check did not confirm exactly the new account."], ["Nur lokal gebunden", "Bound locally only"], ["(nur lokal)", "(local only)"], ["Nur lokal", "Local only"],
["Bindet nur an 127.0.0.1. Fernzugriff nur ueber einen Tunnel (z.B. Tailscale/SSH) - die sicherste Variante.", "Binds only to 127.0.0.1. Remote access only through a tunnel (such as Tailscale/SSH) - the safest option."],
["Bindet an 0.0.0.0. Erreichbar im Netzwerk, erfordert eine Allowlist. Nur in vertrauenswuerdigen Netzen/VPN nutzen.", "Binds to 0.0.0.0. Reachable on the network and requires an allowlist. Use only on trusted networks or VPNs."],
@@ -149,7 +149,7 @@ export function getDownloadQueueStatusMetrics(items: readonly DownloadItem[]): {
export function formatRemainingDownloadBytes(summary: { bytes: number; unknownItems: number }): string {
const value = summary.bytes >= 1024 ** 4
? `${(summary.bytes / 1024 ** 4).toFixed(4)} TB`
? `${(summary.bytes / 1024 ** 4).toFixed(5)} TB`
: humanSize(summary.bytes);
if (summary.unknownItems <= 0) return value;
return summary.bytes > 0 ? `${value}` : "Unbekannt";
+10 -3
View File
@@ -10,7 +10,7 @@ export const SETTINGS_SECTIONS: readonly { id: SettingsSection; label: string }[
{ id: "allgemein", label: "Allgemein" },
{ id: "accounts", label: "Accounts" },
{ id: "extract", label: "Entpacken" },
{ id: "speed", label: "Geschwindigkeit" },
{ id: "speed", label: "Geschwindigkeit & Proxy" },
{ id: "cleanup", label: "Bereinigung" },
{ id: "updates", label: "Updates" }
];
@@ -392,8 +392,8 @@ export function buildSettingsFormViewModel({
};
});
return {
title: "Geschwindigkeit",
description: "Tempo, Wiederverbindung und zeitgesteuerte Bandbreitenregeln.",
title: "Geschwindigkeit & Proxy",
description: "Tempo, Proxy, Wiederverbindung und zeitgesteuerte Bandbreitenregeln.",
groups: [
{
id: "speed-limit",
@@ -583,6 +583,13 @@ export function buildSettingsFormViewModel({
title: "Speicherort",
fields: [
{ id: "outputDir", kind: "path", label: "Download-Ordner", value: settings.outputDir, actionLabel: "Wählen", help: "Zielordner für heruntergeladene Dateien." },
{
id: "createWorkDirectoriesOnStartup",
kind: "switch",
label: "Fehlende Arbeitsordner beim Start anlegen",
value: settings.createWorkDirectoriesOnStartup,
help: "Erstellt den Download-Ordner sowie bei aktiver Funktion den Entpack- und Videosammelordner neu. Vorhandene Ordner und Inhalte bleiben unverändert."
},
{ id: "packageName", kind: "text", label: "Paketname (optional)", value: settings.packageName },
{
id: "logStorageLocation",
+2
View File
@@ -184,6 +184,7 @@ export interface AppSettings extends DailyStartSettings, ProxyDownloadSettings {
providerTertiary: DebridFallbackProvider;
autoProviderFallback: boolean;
outputDir: string;
createWorkDirectoriesOnStartup: boolean;
packageName: string;
autoExtract: boolean;
autoRename4sf4sj: boolean;
@@ -319,6 +320,7 @@ export interface RendererSettings extends DailyStartSettings, ProxyDownloadSetti
providerTertiary: DebridFallbackProvider;
autoProviderFallback: boolean;
outputDir: string;
createWorkDirectoriesOnStartup: boolean;
packageName: string;
autoExtract: boolean;
autoRename4sf4sj: boolean;
+1 -1
View File
@@ -39,7 +39,7 @@ describe("account operation errors", () => {
expect(formatAccountOperationError(
"Account konnte nicht gespeichert werden",
new Error("Error invoking remote method: proxy_only_account:proxy_list_missing")
)).toBe("Account konnte nicht gespeichert werden: Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit.");
)).toBe("Account konnte nicht gespeichert werden: Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit & Proxy.");
expect(formatAccountOperationError(
"Prüfung fehlgeschlagen",
"proxy_only_account:proxy_unreachable"
+5 -2
View File
@@ -281,8 +281,11 @@ describe("Download-ETA", () => {
});
describe("verbleibendes Downloadvolumen", () => {
it("shows a fourth TB decimal so fast downloads stay visibly responsive", () => {
expect(formatRemainingDownloadBytes({ bytes: Math.round(1.125 * 1024 ** 4), unknownItems: 0 })).toBe("1.1250 TB");
it("uses TB precision that changes on every normal large-queue live tick", () => {
const before = Math.round(1.125 * 1024 ** 4);
const after = before - 40 * 1024 ** 2;
expect(formatRemainingDownloadBytes({ bytes: before, unknownItems: 0 })).toBe("1.12500 TB");
expect(formatRemainingDownloadBytes({ bytes: after, unknownItems: 0 })).toBe("1.12496 TB");
});
it("summiert nur offene bekannte Restbytes und klemmt überladene Fortschritte bei null", () => {
+6 -4
View File
@@ -18,6 +18,8 @@ describe("renderer localization", () => {
expect(translateUiText("Passwort/Zugang", "en")).toBe("Password/access");
expect(translateUiText("Animationen", "en")).toBe("Animations");
expect(translateUiText("Animations", "de")).toBe("Animationen");
expect(translateUiText("Geschwindigkeit & Proxy", "en")).toBe("Speed & Proxy");
expect(translateUiText("Fehlende Arbeitsordner beim Start anlegen", "en")).toBe("Create missing work folders on startup");
});
it.each([
@@ -29,11 +31,11 @@ describe("renderer localization", () => {
});
it.each([
["Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but no proxy list is configured. Add one under Settings → Speed."],
["Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste kann nicht gelesen werden. Prüfe die Datei unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but the configured proxy list cannot be read. Check the file under Settings → Speed."],
["Proxy-only ist aktiviert, aber es ist keine Proxy-Liste hinterlegt. Hinterlege sie unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but no proxy list is configured. Add one under Settings → Speed & Proxy."],
["Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste kann nicht gelesen werden. Prüfe die Datei unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but the configured proxy list cannot be read. Check the file under Settings → Speed & Proxy."],
["Proxy-only ist aktiviert, aber die hinterlegte Proxy-Liste ist leer oder enthält keine gültigen HTTP-Proxys.", "Proxy-only is enabled, but the configured proxy list is empty or contains no valid HTTP proxies."],
["Proxy-only ist aktiviert, aber der feste API-Proxy ist in der Liste nicht verfügbar. Prüfe den Listeneintrag unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but the fixed API proxy is not available in the list. Check the list entry under Settings → Speed."],
["Proxy-only ist aktiviert, aber der feste API-Proxy ist nicht erreichbar oder lehnt die Verbindung ab. Prüfe den Proxy unter Einstellungen → Geschwindigkeit.", "Proxy-only is enabled, but the fixed API proxy is unreachable or refuses the connection. Check the proxy under Settings → Speed."]
["Proxy-only ist aktiviert, aber der feste API-Proxy ist in der Liste nicht verfügbar. Prüfe den Listeneintrag unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but the fixed API proxy is not available in the list. Check the list entry under Settings → Speed & Proxy."],
["Proxy-only ist aktiviert, aber der feste API-Proxy ist nicht erreichbar oder lehnt die Verbindung ab. Prüfe den Proxy unter Einstellungen → Geschwindigkeit & Proxy.", "Proxy-only is enabled, but the fixed API proxy is unreachable or refuses the connection. Check the proxy under Settings → Speed & Proxy."]
])("translates Proxy-only account guidance %s", (german, english) => {
expect(translateUiText(german, "en")).toBe(english);
expect(translateUiText(english, "de")).toBe(german);
+9
View File
@@ -4,6 +4,15 @@ import { createRendererSettings } from "../src/main/renderer-state";
import { validateRendererSettingsUpdate } from "../src/main/renderer-settings";
describe("renderer settings validation", () => {
it("projects and validates startup work-directory creation", () => {
const current = { ...defaultSettings(), createWorkDirectoriesOnStartup: true };
const projected = createRendererSettings(current);
expect(projected.createWorkDirectoriesOnStartup).toBe(true);
expect(validateRendererSettingsUpdate({ createWorkDirectoriesOnStartup: false }, current))
.toEqual({ createWorkDirectoriesOnStartup: false });
});
it("projects and validates the semantic theme preference independently from the resolved palette", () => {
const current = { ...defaultSettings(), theme: "dark" as const, themePreference: "system" as const };
const projected = createRendererSettings(current);
+19 -2
View File
@@ -391,7 +391,7 @@ describe("settings model", () => {
{ id: "allgemein", label: "Allgemein" },
{ id: "accounts", label: "Accounts" },
{ id: "extract", label: "Entpacken" },
{ id: "speed", label: "Geschwindigkeit" },
{ id: "speed", label: "Geschwindigkeit & Proxy" },
{ id: "cleanup", label: "Bereinigung" },
{ id: "updates", label: "Updates" }
]);
@@ -642,6 +642,23 @@ describe("settings views", () => {
});
});
it("offers the disabled-by-default startup work-directory switch", () => {
const form = buildSettingsFormViewModel({
settings: { ...createRendererSettings(defaultSettings()), archivePasswordList: "", notifyUrl: "" },
section: "allgemein",
speedLimitInput: "0",
scheduleSpeedInputs: {}
});
const field = form.groups.flatMap((group) => group.fields)
.find((entry) => entry.id === "createWorkDirectoriesOnStartup");
expect(field).toEqual(expect.objectContaining({
kind: "switch",
label: "Fehlende Arbeitsordner beim Start anlegen",
value: false
}));
});
it("projects proxy segmentation controls with safe defaults and connection totals", () => {
const form = buildSettingsFormViewModel({
settings: {
@@ -833,7 +850,7 @@ describe("settings views", () => {
const html = renderToStaticMarkup(<SettingsSidebar actions={viewActions()} model={viewModel()} />);
expect(count(html, "data-visual-region=\"settings-sidebar\"")).toBe(1);
for (const section of SETTINGS_SECTIONS) {
expect(html).toContain(section.label);
expect(html).toContain(section.label.replace(/&/g, "&amp;"));
}
expect(html).toContain("aria-current=\"page\"");
});
+101
View File
@@ -0,0 +1,101 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { defaultSettings } from "../src/main/constants";
import { ensureStartupWorkDirectories } from "../src/main/startup-work-directories";
const tempDirs: string[] = [];
function tempRoot(): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-startup-dirs-"));
tempDirs.push(root);
return root;
}
afterEach(() => {
for (const directory of tempDirs.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
describe("startup work directories", () => {
it("does nothing while the setting is disabled", () => {
const root = tempRoot();
const outputDir = path.join(root, "downloads");
const result = ensureStartupWorkDirectories({
...defaultSettings(),
outputDir,
createWorkDirectoriesOnStartup: false
});
expect(result).toEqual({ created: [], existing: [], failures: [] });
expect(fs.existsSync(outputDir)).toBe(false);
});
it("creates only the enabled work directories and preserves existing contents", () => {
const root = tempRoot();
const outputDir = path.join(root, "downloads");
const extractDir = path.join(root, "extracted");
const mkvLibraryDir = path.join(root, "videos");
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(path.join(outputDir, "keep.bin"), "keep");
const result = ensureStartupWorkDirectories({
...defaultSettings(),
outputDir,
extractDir,
mkvLibraryDir,
autoExtract: true,
collectMkvToLibrary: true,
createWorkDirectoriesOnStartup: true
});
expect(result.created.map((entry) => entry.kind)).toEqual(["extract", "video-library"]);
expect(result.existing.map((entry) => entry.kind)).toEqual(["download"]);
expect(result.failures).toEqual([]);
expect(fs.readFileSync(path.join(outputDir, "keep.bin"), "utf8")).toBe("keep");
expect(fs.statSync(extractDir).isDirectory()).toBe(true);
expect(fs.statSync(mkvLibraryDir).isDirectory()).toBe(true);
});
it("skips inactive optional directories", () => {
const root = tempRoot();
const outputDir = path.join(root, "downloads");
const extractDir = path.join(root, "extracted");
const mkvLibraryDir = path.join(root, "videos");
const result = ensureStartupWorkDirectories({
...defaultSettings(),
outputDir,
extractDir,
mkvLibraryDir,
autoExtract: false,
collectMkvToLibrary: false,
createWorkDirectoriesOnStartup: true
});
expect(result.created.map((entry) => entry.kind)).toEqual(["download"]);
expect(fs.existsSync(extractDir)).toBe(false);
expect(fs.existsSync(mkvLibraryDir)).toBe(false);
});
it("reports one invalid target without blocking the other directories", () => {
const root = tempRoot();
const blockedOutput = path.join(root, "blocked");
const extractDir = path.join(root, "extracted");
fs.writeFileSync(blockedOutput, "file");
const result = ensureStartupWorkDirectories({
...defaultSettings(),
outputDir: blockedOutput,
extractDir,
autoExtract: true,
createWorkDirectoriesOnStartup: true
});
expect(result.failures.map((entry) => entry.kind)).toEqual(["download"]);
expect(result.created.map((entry) => entry.kind)).toEqual(["extract"]);
expect(fs.statSync(extractDir).isDirectory()).toBe(true);
});
});
+12
View File
@@ -306,6 +306,18 @@ describe("settings storage", () => {
expect(normalizeSettings({ ...defaultSettings(), language: "fr" as "en" }).language).toBe("en");
});
it("keeps startup work-directory creation opt-in and persists an explicit enable", () => {
const legacy = { ...defaultSettings() } as Partial<AppSettings>;
delete legacy.createWorkDirectoriesOnStartup;
expect(defaultSettings().createWorkDirectoriesOnStartup).toBe(false);
expect(normalizeSettings(legacy as AppSettings).createWorkDirectoriesOnStartup).toBe(false);
expect(normalizeSettings({
...defaultSettings(),
createWorkDirectoriesOnStartup: true
}).createWorkDirectoriesOnStartup).toBe(true);
});
it("migrates a legacy shared Mega-Debrid pool into only the preferred mode", () => {
const legacy = { ...defaultSettings() } as Partial<AppSettings>;
legacy.megaCredentials = "legacy@example.test:legacy-pass";
+1
View File
@@ -92,6 +92,7 @@ function createSettings(): AppSettings {
providerTertiary: "bestdebrid",
autoProviderFallback: true,
outputDir: "C:\\Visual\\Downloads",
createWorkDirectoriesOnStartup: false,
packageName: "",
autoExtract: true,
autoRename4sf4sj: true,