Compare commits

..

2 Commits

Author SHA1 Message Date
Sucukdeluxe
284c5e7aa6 Release v1.6.35
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:07:52 +01:00
Sucukdeluxe
036cd3e066 Add DDownload provider, post-processing status labels, and update changelog
- DDownload (ddownload.com/ddl.to) as new hoster with web login
- Post-processing labels: Entpacken/Renaming/Aufräumen/MKVs
- Release notes shown in update confirmation dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:05:16 +01:00
9 changed files with 249 additions and 14 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.34", "version": "1.6.35",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -104,6 +104,7 @@ export class AppController {
|| (settings.megaLogin.trim() && settings.megaPassword.trim()) || (settings.megaLogin.trim() && settings.megaPassword.trim())
|| settings.bestToken.trim() || settings.bestToken.trim()
|| settings.allDebridToken.trim() || settings.allDebridToken.trim()
|| (settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim())
); );
} }
@ -284,7 +285,7 @@ export class AppController {
public exportBackup(): string { public exportBackup(): string {
const settings = { ...this.settings }; const settings = { ...this.settings };
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken"]; const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken", "ddownloadPassword"];
for (const key of SENSITIVE_KEYS) { for (const key of SENSITIVE_KEYS) {
const val = settings[key]; const val = settings[key];
if (typeof val === "string" && val.length > 0) { if (typeof val === "string" && val.length > 0) {
@ -306,7 +307,7 @@ export class AppController {
return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" }; return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" };
} }
const importedSettings = parsed.settings as AppSettings; const importedSettings = parsed.settings as AppSettings;
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken"]; const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken", "ddownloadPassword"];
for (const key of SENSITIVE_KEYS) { for (const key of SENSITIVE_KEYS) {
const val = (importedSettings as Record<string, unknown>)[key]; const val = (importedSettings as Record<string, unknown>)[key];
if (typeof val === "string" && val.startsWith("***")) { if (typeof val === "string" && val.startsWith("***")) {

View File

@ -45,6 +45,8 @@ export function defaultSettings(): AppSettings {
megaPassword: "", megaPassword: "",
bestToken: "", bestToken: "",
allDebridToken: "", allDebridToken: "",
ddownloadLogin: "",
ddownloadPassword: "",
archivePasswordList: "", archivePasswordList: "",
rememberToken: true, rememberToken: true,
providerPrimary: "realdebrid", providerPrimary: "realdebrid",

View File

@ -15,7 +15,8 @@ const PROVIDER_LABELS: Record<DebridProvider, string> = {
realdebrid: "Real-Debrid", realdebrid: "Real-Debrid",
megadebrid: "Mega-Debrid", megadebrid: "Mega-Debrid",
bestdebrid: "BestDebrid", bestdebrid: "BestDebrid",
alldebrid: "AllDebrid" alldebrid: "AllDebrid",
ddownload: "DDownload"
}; };
interface ProviderUnrestrictedLink extends UnrestrictedLink { interface ProviderUnrestrictedLink extends UnrestrictedLink {
@ -958,6 +959,193 @@ class AllDebridClient {
} }
} }
const DDOWNLOAD_URL_RE = /^https?:\/\/(?:www\.)?(?:ddownload\.com|ddl\.to)\/([a-z0-9]+)/i;
const DDOWNLOAD_WEB_BASE = "https://ddownload.com";
const DDOWNLOAD_WEB_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36";
class DdownloadClient {
private login: string;
private password: string;
private cookies: string = "";
public constructor(login: string, password: string) {
this.login = login;
this.password = password;
}
private async webLogin(signal?: AbortSignal): Promise<void> {
// Step 1: GET login page to extract form token
const loginPageRes = await fetch(`${DDOWNLOAD_WEB_BASE}/login.html`, {
headers: { "User-Agent": DDOWNLOAD_WEB_UA },
redirect: "manual",
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
});
const loginPageHtml = await loginPageRes.text();
const tokenMatch = loginPageHtml.match(/name="token" value="([^"]+)"/);
const pageCookies = (loginPageRes.headers.getSetCookie?.() || []).map((c: string) => c.split(";")[0]).join("; ");
// Step 2: POST login
const body = new URLSearchParams({
op: "login",
token: tokenMatch?.[1] || "",
rand: "",
redirect: "",
login: this.login,
password: this.password
});
const loginRes = await fetch(`${DDOWNLOAD_WEB_BASE}/`, {
method: "POST",
headers: {
"User-Agent": DDOWNLOAD_WEB_UA,
"Content-Type": "application/x-www-form-urlencoded",
...(pageCookies ? { Cookie: pageCookies } : {})
},
body: body.toString(),
redirect: "manual",
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
});
// Drain body
try { await loginRes.text(); } catch { /* ignore */ }
const setCookies = loginRes.headers.getSetCookie?.() || [];
const xfss = setCookies.find((c: string) => c.startsWith("xfss="));
const loginCookie = setCookies.find((c: string) => c.startsWith("login="));
if (!xfss) {
throw new Error("DDownload Login fehlgeschlagen (kein Session-Cookie)");
}
this.cookies = [loginCookie, xfss].filter(Boolean).map((c: string) => c.split(";")[0]).join("; ");
}
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
const match = link.match(DDOWNLOAD_URL_RE);
if (!match) {
throw new Error("Kein DDownload-Link");
}
const fileCode = match[1];
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
try {
if (signal?.aborted) throw new Error("aborted:debrid");
// Login if no session yet
if (!this.cookies) {
await this.webLogin(signal);
}
// Step 1: GET file page to extract form fields
const filePageRes = await fetch(`${DDOWNLOAD_WEB_BASE}/${fileCode}`, {
headers: {
"User-Agent": DDOWNLOAD_WEB_UA,
Cookie: this.cookies
},
redirect: "manual",
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
});
// Premium with direct downloads enabled → redirect immediately
if (filePageRes.status >= 300 && filePageRes.status < 400) {
const directUrl = filePageRes.headers.get("location") || "";
try { await filePageRes.text(); } catch { /* drain */ }
if (directUrl) {
return {
fileName: filenameFromUrl(directUrl) || filenameFromUrl(link),
directUrl,
fileSize: null,
retriesUsed: attempt - 1
};
}
}
const html = await filePageRes.text();
// Check for file not found
if (/File Not Found|file was removed|file was banned/i.test(html)) {
throw new Error("DDownload: Datei nicht gefunden");
}
// Extract form fields
const idVal = html.match(/name="id" value="([^"]+)"/)?.[1] || fileCode;
const randVal = html.match(/name="rand" value="([^"]+)"/)?.[1] || "";
const fileNameMatch = html.match(/class="file-info-name"[^>]*>([^<]+)</);
const fileName = fileNameMatch?.[1]?.trim() || filenameFromUrl(link);
// Step 2: POST download2 for premium download
const dlBody = new URLSearchParams({
op: "download2",
id: idVal,
rand: randVal,
referer: "",
method_premium: "1",
adblock_detected: "0"
});
const dlRes = await fetch(`${DDOWNLOAD_WEB_BASE}/${fileCode}`, {
method: "POST",
headers: {
"User-Agent": DDOWNLOAD_WEB_UA,
"Content-Type": "application/x-www-form-urlencoded",
Cookie: this.cookies,
Referer: `${DDOWNLOAD_WEB_BASE}/${fileCode}`
},
body: dlBody.toString(),
redirect: "manual",
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
});
if (dlRes.status >= 300 && dlRes.status < 400) {
const directUrl = dlRes.headers.get("location") || "";
try { await dlRes.text(); } catch { /* drain */ }
if (directUrl) {
return {
fileName: fileName || filenameFromUrl(directUrl),
directUrl,
fileSize: null,
retriesUsed: attempt - 1
};
}
}
const dlHtml = await dlRes.text();
// Try to find direct URL in response HTML
const directMatch = dlHtml.match(/https?:\/\/[a-z0-9]+\.(?:dstorage\.org|ddownload\.com|ddl\.to|ucdn\.to)[^\s"'<>]+/i);
if (directMatch) {
return {
fileName,
directUrl: directMatch[0],
fileSize: null,
retriesUsed: attempt - 1
};
}
// Check for error messages
const errMatch = dlHtml.match(/class="err"[^>]*>([^<]+)</i);
if (errMatch) {
throw new Error(`DDownload: ${errMatch[1].trim()}`);
}
throw new Error("DDownload: Kein Download-Link erhalten");
} catch (error) {
lastError = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) {
break;
}
// Re-login on auth errors
if (/login|session|cookie/i.test(lastError)) {
this.cookies = "";
}
if (attempt >= REQUEST_RETRIES || !isRetryableErrorText(lastError)) {
break;
}
await sleepWithSignal(retryDelay(attempt), signal);
}
}
throw new Error(String(lastError || "DDownload Unrestrict fehlgeschlagen").replace(/^Error:\s*/i, ""));
}
}
export class DebridService { export class DebridService {
private settings: AppSettings; private settings: AppSettings;
@ -1109,6 +1297,9 @@ export class DebridService {
if (provider === "alldebrid") { if (provider === "alldebrid") {
return Boolean(settings.allDebridToken.trim()); return Boolean(settings.allDebridToken.trim());
} }
if (provider === "ddownload") {
return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim());
}
return Boolean(settings.bestToken.trim()); return Boolean(settings.bestToken.trim());
} }
@ -1122,6 +1313,9 @@ export class DebridService {
if (provider === "alldebrid") { if (provider === "alldebrid") {
return new AllDebridClient(settings.allDebridToken).unrestrictLink(link, signal); return new AllDebridClient(settings.allDebridToken).unrestrictLink(link, signal);
} }
if (provider === "ddownload") {
return new DdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal);
}
return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal); return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal);
} }
} }

View File

@ -6439,6 +6439,8 @@ export class DownloadManager extends EventEmitter {
logger.info(`Hybrid-Extract Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}`); logger.info(`Hybrid-Extract Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}`);
if (result.extracted > 0) { if (result.extracted > 0) {
pkg.postProcessLabel = "Renaming...";
this.emitState();
await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg); await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg);
} }
if (result.failed > 0) { if (result.failed > 0) {
@ -6551,8 +6553,10 @@ export class DownloadManager extends EventEmitter {
const allDone = success + failed + cancelled >= items.length; const allDone = success + failed + cancelled >= items.length;
if (!allDone && this.settings.hybridExtract && this.settings.autoExtract && failed === 0 && success > 0) { if (!allDone && this.settings.hybridExtract && this.settings.autoExtract && failed === 0 && success > 0) {
pkg.postProcessLabel = "Entpacken...";
await this.runHybridExtraction(packageId, pkg, items, signal); await this.runHybridExtraction(packageId, pkg, items, signal);
if (signal?.aborted) { if (signal?.aborted) {
pkg.postProcessLabel = undefined;
pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "queued" : "paused"; pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "queued" : "paused";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
return; return;
@ -6566,6 +6570,7 @@ export class DownloadManager extends EventEmitter {
if (!this.session.packages[packageId]) { if (!this.session.packages[packageId]) {
return; // Package was fully cleaned up return; // Package was fully cleaned up
} }
pkg.postProcessLabel = undefined;
pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "downloading" : "queued"; pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "downloading" : "queued";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
this.emitState(); this.emitState();
@ -6573,6 +6578,7 @@ export class DownloadManager extends EventEmitter {
} }
if (!allDone) { if (!allDone) {
pkg.postProcessLabel = undefined;
pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "downloading" : "queued"; pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "downloading" : "queued";
logger.info(`Post-Processing verschoben: pkg=${pkg.name}, noch offene items`); logger.info(`Post-Processing verschoben: pkg=${pkg.name}, noch offene items`);
return; return;
@ -6582,6 +6588,7 @@ export class DownloadManager extends EventEmitter {
const alreadyMarkedExtracted = completedItems.length > 0 && completedItems.every((item) => isExtractedLabel(item.fullStatus)); const alreadyMarkedExtracted = completedItems.length > 0 && completedItems.every((item) => isExtractedLabel(item.fullStatus));
if (this.settings.autoExtract && failed === 0 && success > 0 && !alreadyMarkedExtracted) { if (this.settings.autoExtract && failed === 0 && success > 0 && !alreadyMarkedExtracted) {
pkg.postProcessLabel = "Entpacken...";
pkg.status = "extracting"; pkg.status = "extracting";
this.emitState(); this.emitState();
const extractionStartMs = nowMs(); const extractionStartMs = nowMs();
@ -6732,6 +6739,8 @@ export class DownloadManager extends EventEmitter {
// Auto-rename even when some archives failed — successfully extracted files still need renaming // Auto-rename even when some archives failed — successfully extracted files still need renaming
if (result.extracted > 0) { if (result.extracted > 0) {
pkg.postProcessLabel = "Renaming...";
this.emitState();
await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg); await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg);
} }
@ -6835,6 +6844,8 @@ export class DownloadManager extends EventEmitter {
} }
if (this.settings.autoExtract && alreadyMarkedExtracted && failed === 0 && success > 0 && this.settings.cleanupMode !== "none") { if (this.settings.autoExtract && alreadyMarkedExtracted && failed === 0 && success > 0 && this.settings.cleanupMode !== "none") {
pkg.postProcessLabel = "Aufräumen...";
this.emitState();
const removedArchives = await this.cleanupRemainingArchiveArtifacts(pkg.outputDir); const removedArchives = await this.cleanupRemainingArchiveArtifacts(pkg.outputDir);
if (removedArchives > 0) { if (removedArchives > 0) {
logger.info(`Hybrid-Post-Cleanup entfernte Archive: pkg=${pkg.name}, entfernt=${removedArchives}`); logger.info(`Hybrid-Post-Cleanup entfernte Archive: pkg=${pkg.name}, entfernt=${removedArchives}`);
@ -6842,6 +6853,8 @@ export class DownloadManager extends EventEmitter {
} }
if (success > 0 && (pkg.status === "completed" || pkg.status === "failed")) { if (success > 0 && (pkg.status === "completed" || pkg.status === "failed")) {
pkg.postProcessLabel = "Verschiebe MKVs...";
this.emitState();
await this.collectMkvFilesToLibrary(packageId, pkg); await this.collectMkvFilesToLibrary(packageId, pkg);
} }
if (this.runPackageIds.has(packageId)) { if (this.runPackageIds.has(packageId)) {
@ -6851,6 +6864,7 @@ export class DownloadManager extends EventEmitter {
this.runCompletedPackages.delete(packageId); this.runCompletedPackages.delete(packageId);
} }
} }
pkg.postProcessLabel = undefined;
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
logger.info(`Post-Processing Ende: pkg=${pkg.name}, status=${pkg.status}`); logger.info(`Post-Processing Ende: pkg=${pkg.name}, status=${pkg.status}`);

View File

@ -5,8 +5,8 @@ import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, Down
import { defaultSettings } from "./constants"; import { defaultSettings } from "./constants";
import { logger } from "./logger"; import { logger } from "./logger";
const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid"]); const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]);
const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid"]); const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]);
const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]); const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]);
const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]); const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]);
const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]); const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]);
@ -17,7 +17,7 @@ const VALID_PACKAGE_PRIORITIES = new Set<string>(["high", "normal", "low"]);
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([ const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
"queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled" "queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled"
]); ]);
const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "bestdebrid", "alldebrid"]); const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]);
const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]); const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]);
function asText(value: unknown): string { function asText(value: unknown): string {
@ -111,6 +111,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
megaPassword: asText(settings.megaPassword), megaPassword: asText(settings.megaPassword),
bestToken: asText(settings.bestToken), bestToken: asText(settings.bestToken),
allDebridToken: asText(settings.allDebridToken), allDebridToken: asText(settings.allDebridToken),
ddownloadLogin: asText(settings.ddownloadLogin),
ddownloadPassword: asText(settings.ddownloadPassword),
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n/g, "\n"), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n/g, "\n"),
rememberToken: Boolean(settings.rememberToken), rememberToken: Boolean(settings.rememberToken),
providerPrimary: settings.providerPrimary, providerPrimary: settings.providerPrimary,
@ -200,7 +202,9 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
megaLogin: "", megaLogin: "",
megaPassword: "", megaPassword: "",
bestToken: "", bestToken: "",
allDebridToken: "" allDebridToken: "",
ddownloadLogin: "",
ddownloadPassword: ""
}; };
} }
@ -442,6 +446,7 @@ export function normalizeLoadedSessionTransientFields(session: SessionState): Se
if (ACTIVE_PKG_STATUSES.has(pkg.status)) { if (ACTIVE_PKG_STATUSES.has(pkg.status)) {
pkg.status = "queued"; pkg.status = "queued";
} }
pkg.postProcessLabel = undefined;
} }
// Clear stale session-level running/paused flags // Clear stale session-level running/paused flags

View File

@ -336,6 +336,8 @@ function parseReleasePayload(payload: Record<string, unknown>, fallback: UpdateC
const releaseUrl = String(payload.html_url || fallback.releaseUrl); const releaseUrl = String(payload.html_url || fallback.releaseUrl);
const setup = pickSetupAsset(readReleaseAssets(payload)); const setup = pickSetupAsset(readReleaseAssets(payload));
const body = typeof payload.body === "string" ? payload.body.trim() : "";
return { return {
updateAvailable: isRemoteNewer(APP_VERSION, latestVersion), updateAvailable: isRemoteNewer(APP_VERSION, latestVersion),
currentVersion: APP_VERSION, currentVersion: APP_VERSION,
@ -344,7 +346,8 @@ function parseReleasePayload(payload: Record<string, unknown>, fallback: UpdateC
releaseUrl, releaseUrl,
setupAssetUrl: setup?.browser_download_url || "", setupAssetUrl: setup?.browser_download_url || "",
setupAssetName: setup?.name || "", setupAssetName: setup?.name || "",
setupAssetDigest: setup?.digest || "" setupAssetDigest: setup?.digest || "",
releaseNotes: body || undefined
}; };
} }

View File

@ -93,7 +93,7 @@ const cleanupLabels: Record<string, string> = {
const AUTO_RENDER_PACKAGE_LIMIT = 260; const AUTO_RENDER_PACKAGE_LIMIT = 260;
const providerLabels: Record<DebridProvider, string> = { const providerLabels: Record<DebridProvider, string> = {
realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid" realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload"
}; };
function formatDateTime(ts: number): string { function formatDateTime(ts: number): string {
@ -928,8 +928,11 @@ export function App(): ReactElement {
if (settingsDraft.allDebridToken.trim()) { if (settingsDraft.allDebridToken.trim()) {
list.push("alldebrid"); list.push("alldebrid");
} }
if (settingsDraft.ddownloadLogin.trim() && settingsDraft.ddownloadPassword.trim()) {
list.push("ddownload");
}
return list; return list;
}, [settingsDraft.token, settingsDraft.megaLogin, settingsDraft.megaPassword, settingsDraft.bestToken, settingsDraft.allDebridToken]); }, [settingsDraft.token, settingsDraft.megaLogin, settingsDraft.megaPassword, settingsDraft.bestToken, settingsDraft.allDebridToken, settingsDraft.ddownloadLogin, settingsDraft.ddownloadPassword]);
const primaryProviderValue: DebridProvider = useMemo(() => { const primaryProviderValue: DebridProvider = useMemo(() => {
if (configuredProviders.includes(settingsDraft.providerPrimary)) { if (configuredProviders.includes(settingsDraft.providerPrimary)) {
@ -990,9 +993,14 @@ export function App(): ReactElement {
if (source === "manual") { showToast(`Kein Update verfügbar (v${result.currentVersion})`, 2000); } if (source === "manual") { showToast(`Kein Update verfügbar (v${result.currentVersion})`, 2000); }
return; return;
} }
let changelogBlock = "";
if (result.releaseNotes) {
const notes = result.releaseNotes.length > 500 ? `${result.releaseNotes.slice(0, 500)}` : result.releaseNotes;
changelogBlock = `\n\n--- Changelog ---\n${notes}`;
}
const approved = await askConfirmPrompt({ const approved = await askConfirmPrompt({
title: "Update verfügbar", title: "Update verfügbar",
message: `${result.latestTag} (aktuell v${result.currentVersion})\n\nJetzt automatisch herunterladen und installieren?`, message: `${result.latestTag} (aktuell v${result.currentVersion})${changelogBlock}\n\nJetzt automatisch herunterladen und installieren?`,
confirmLabel: "Jetzt installieren" confirmLabel: "Jetzt installieren"
}); });
if (!mountedRef.current) { if (!mountedRef.current) {
@ -2711,6 +2719,10 @@ export function App(): ReactElement {
<input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} /> <input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} />
<label>AllDebrid API Key</label> <label>AllDebrid API Key</label>
<input type="password" value={settingsDraft.allDebridToken} onChange={(e) => setText("allDebridToken", e.target.value)} /> <input type="password" value={settingsDraft.allDebridToken} onChange={(e) => setText("allDebridToken", e.target.value)} />
<label>DDownload Login</label>
<input value={settingsDraft.ddownloadLogin} onChange={(e) => setText("ddownloadLogin", e.target.value)} />
<label>DDownload Passwort</label>
<input type="password" value={settingsDraft.ddownloadPassword} onChange={(e) => setText("ddownloadPassword", e.target.value)} />
{configuredProviders.length === 0 && ( {configuredProviders.length === 0 && (
<div className="hint">Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.</div> <div className="hint">Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.</div>
)} )}
@ -3344,7 +3356,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
<span key={col} 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 key={col} 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>
); );
case "status": return ( case "status": return (
<span key={col} className="pkg-col pkg-col-status">[{done}/{total}{done === total && total > 0 ? " - Done" : ""}{failed > 0 ? ` · ${failed} Fehler` : ""}{cancelled > 0 ? ` · ${cancelled} abgebr.` : ""}]</span> <span key={col} className="pkg-col pkg-col-status">[{done}/{total}{done === total && total > 0 ? " - Done" : ""}{failed > 0 ? ` · ${failed} Fehler` : ""}{cancelled > 0 ? ` · ${cancelled} abgebr.` : ""}]{pkg.postProcessLabel ? ` ${pkg.postProcessLabel}` : ""}</span>
); );
case "speed": return ( case "speed": return (
<span key={col} className="pkg-col pkg-col-speed">{packageSpeed > 0 ? formatSpeedMbps(packageSpeed) : ""}</span> <span key={col} className="pkg-col pkg-col-speed">{packageSpeed > 0 ? formatSpeedMbps(packageSpeed) : ""}</span>

View File

@ -14,7 +14,7 @@ export type CleanupMode = "none" | "trash" | "delete";
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask"; export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
export type SpeedMode = "global" | "per_download"; export type SpeedMode = "global" | "per_download";
export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done"; export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done";
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid"; export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload";
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 type PackagePriority = "high" | "normal" | "low";
@ -42,6 +42,8 @@ export interface AppSettings {
megaPassword: string; megaPassword: string;
bestToken: string; bestToken: string;
allDebridToken: string; allDebridToken: string;
ddownloadLogin: string;
ddownloadPassword: string;
archivePasswordList: string; archivePasswordList: string;
rememberToken: boolean; rememberToken: boolean;
providerPrimary: DebridProvider; providerPrimary: DebridProvider;
@ -119,6 +121,7 @@ export interface PackageEntry {
cancelled: boolean; cancelled: boolean;
enabled: boolean; enabled: boolean;
priority: PackagePriority; priority: PackagePriority;
postProcessLabel?: string;
createdAt: number; createdAt: number;
updatedAt: number; updatedAt: number;
} }
@ -219,6 +222,7 @@ export interface UpdateCheckResult {
setupAssetUrl?: string; setupAssetUrl?: string;
setupAssetName?: string; setupAssetName?: string;
setupAssetDigest?: string; setupAssetDigest?: string;
releaseNotes?: string;
error?: string; error?: string;
} }