Restore in-app updater and add Mega web fallback path
Some checks are pending
Build and Release / build (push) Waiting to run

This commit is contained in:
Sucukdeluxe 2026-02-27 05:28:50 +01:00
parent 704826b421
commit 0e898733d6
14 changed files with 344 additions and 20 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.18", "version": "1.1.19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.18", "version": "1.1.19",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.18", "version": "1.1.19",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -1,26 +1,35 @@
import path from "node:path"; import path from "node:path";
import { app } from "electron"; import { app } from "electron";
import { AddLinksPayload, AppSettings, ParsedPackageInput, UiSnapshot, UpdateCheckResult } from "../shared/types"; import { AddLinksPayload, AppSettings, ParsedPackageInput, UiSnapshot, UpdateCheckResult, UpdateInstallResult } from "../shared/types";
import { importDlcContainers } from "./container"; import { importDlcContainers } from "./container";
import { APP_VERSION, defaultSettings } from "./constants"; import { APP_VERSION, defaultSettings } from "./constants";
import { DownloadManager } from "./download-manager"; import { DownloadManager } from "./download-manager";
import { parseCollectorInput } from "./link-parser"; import { parseCollectorInput } from "./link-parser";
import { configureLogger, logger } from "./logger"; import { configureLogger, logger } from "./logger";
import { MegaWebFallback } from "./mega-web-fallback";
import { createStoragePaths, emptySession, loadSession, loadSettings, saveSettings } from "./storage"; import { createStoragePaths, emptySession, loadSession, loadSettings, saveSettings } from "./storage";
import { checkGitHubUpdate } from "./update"; import { checkGitHubUpdate, installLatestUpdate } from "./update";
export class AppController { export class AppController {
private settings: AppSettings; private settings: AppSettings;
private manager: DownloadManager; private manager: DownloadManager;
private megaWebFallback: MegaWebFallback;
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime")); private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
public constructor() { public constructor() {
configureLogger(this.storagePaths.baseDir); configureLogger(this.storagePaths.baseDir);
this.settings = loadSettings(this.storagePaths); this.settings = loadSettings(this.storagePaths);
const session = loadSession(this.storagePaths); const session = loadSession(this.storagePaths);
this.manager = new DownloadManager(this.settings, session, this.storagePaths); this.megaWebFallback = new MegaWebFallback(() => ({
login: this.settings.megaLogin,
password: this.settings.megaPassword
}));
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
megaWebUnrestrict: (link: string) => this.megaWebFallback.unrestrict(link)
});
this.manager.on("state", (snapshot: UiSnapshot) => { this.manager.on("state", (snapshot: UiSnapshot) => {
this.onState?.(snapshot); this.onState?.(snapshot);
}); });
@ -69,6 +78,10 @@ export class AppController {
return checkGitHubUpdate(this.settings.updateRepo); return checkGitHubUpdate(this.settings.updateRepo);
} }
public async installUpdate(): Promise<UpdateInstallResult> {
return installLatestUpdate(this.settings.updateRepo);
}
public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } { public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } {
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName); const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
if (parsed.length === 0) { if (parsed.length === 0) {
@ -110,6 +123,7 @@ export class AppController {
public shutdown(): void { public shutdown(): void {
this.manager.stop(); this.manager.stop();
this.megaWebFallback.dispose();
logger.info("App beendet"); logger.info("App beendet");
} }
} }

View File

@ -3,7 +3,7 @@ import os from "node:os";
import { AppSettings } from "../shared/types"; import { AppSettings } from "../shared/types";
export const APP_NAME = "Debrid Download Manager"; export const APP_NAME = "Debrid Download Manager";
export const APP_VERSION = "1.1.18"; export const APP_VERSION = "1.1.19";
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0"; export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
@ -29,6 +29,8 @@ export function defaultSettings(): AppSettings {
return { return {
token: "", token: "",
megaToken: "", megaToken: "",
megaLogin: "",
megaPassword: "",
bestToken: "", bestToken: "",
allDebridToken: "", allDebridToken: "",
rememberToken: true, rememberToken: true,

View File

@ -20,6 +20,12 @@ interface ProviderUnrestrictedLink extends UnrestrictedLink {
providerLabel: string; providerLabel: string;
} }
export type MegaWebUnrestrictor = (link: string) => Promise<UnrestrictedLink | null>;
interface DebridServiceOptions {
megaWebUnrestrict?: MegaWebUnrestrictor;
}
type BestDebridRequest = { type BestDebridRequest = {
url: string; url: string;
useAuthHeader: boolean; useAuthHeader: boolean;
@ -191,8 +197,11 @@ function buildBestDebridRequests(link: string, token: string): BestDebridRequest
class MegaDebridClient { class MegaDebridClient {
private token: string; private token: string;
public constructor(token: string) { private megaWebUnrestrict?: MegaWebUnrestrictor;
public constructor(token: string, megaWebUnrestrict?: MegaWebUnrestrictor) {
this.token = token; this.token = token;
this.megaWebUnrestrict = megaWebUnrestrict;
} }
private normalizeMegaCandidates(link: string): string[] { private normalizeMegaCandidates(link: string): string[] {
@ -298,6 +307,14 @@ class MegaDebridClient {
if (/token error|vip_end/i.test(lastError)) { if (/token error|vip_end/i.test(lastError)) {
throw new Error(lastError); throw new Error(lastError);
} }
if (/UNRESTRICTING_ERROR_1/i.test(lastError) && this.megaWebUnrestrict) {
const web = await this.megaWebUnrestrict(link);
if (web?.directUrl) {
web.retriesUsed = attempt - 1;
return web;
}
}
} catch (error) { } catch (error) {
lastError = compactErrorText(error); lastError = compactErrorText(error);
if (attempt >= REQUEST_RETRIES) { if (attempt >= REQUEST_RETRIES) {
@ -533,8 +550,11 @@ export class DebridService {
private allDebridClient: AllDebridClient; private allDebridClient: AllDebridClient;
public constructor(settings: AppSettings) { private options: DebridServiceOptions;
public constructor(settings: AppSettings, options: DebridServiceOptions = {}) {
this.settings = settings; this.settings = settings;
this.options = options;
this.realDebridClient = new RealDebridClient(settings.token); this.realDebridClient = new RealDebridClient(settings.token);
this.allDebridClient = new AllDebridClient(settings.allDebridToken); this.allDebridClient = new AllDebridClient(settings.allDebridToken);
} }
@ -634,7 +654,7 @@ export class DebridService {
return this.realDebridClient.unrestrictLink(link); return this.realDebridClient.unrestrictLink(link);
} }
if (provider === "megadebrid") { if (provider === "megadebrid") {
return new MegaDebridClient(token).unrestrictLink(link); return new MegaDebridClient(token, this.options.megaWebUnrestrict).unrestrictLink(link);
} }
if (provider === "alldebrid") { if (provider === "alldebrid") {
return this.allDebridClient.unrestrictLink(link); return this.allDebridClient.unrestrictLink(link);

View File

@ -6,7 +6,7 @@ import { v4 as uuidv4 } from "uuid";
import { AppSettings, DownloadItem, DownloadSummary, DownloadStatus, PackageEntry, ParsedPackageInput, SessionState, UiSnapshot } from "../shared/types"; import { AppSettings, DownloadItem, DownloadSummary, DownloadStatus, PackageEntry, ParsedPackageInput, SessionState, UiSnapshot } from "../shared/types";
import { CHUNK_SIZE, REQUEST_RETRIES } from "./constants"; import { CHUNK_SIZE, REQUEST_RETRIES } from "./constants";
import { cleanupCancelledPackageArtifacts, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup"; import { cleanupCancelledPackageArtifacts, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { DebridService } from "./debrid"; import { DebridService, MegaWebUnrestrictor } from "./debrid";
import { extractPackageArchives } from "./extractor"; import { extractPackageArchives } from "./extractor";
import { validateFileAgainstManifest } from "./integrity"; import { validateFileAgainstManifest } from "./integrity";
import { logger } from "./logger"; import { logger } from "./logger";
@ -23,6 +23,10 @@ type ActiveTask = {
nonResumableCounted: boolean; nonResumableCounted: boolean;
}; };
type DownloadManagerOptions = {
megaWebUnrestrict?: MegaWebUnrestrictor;
};
function cloneSession(session: SessionState): SessionState { function cloneSession(session: SessionState): SessionState {
return JSON.parse(JSON.stringify(session)) as SessionState; return JSON.parse(JSON.stringify(session)) as SessionState;
} }
@ -103,12 +107,12 @@ export class DownloadManager extends EventEmitter {
private speedBytesLastWindow = 0; private speedBytesLastWindow = 0;
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths) { public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
super(); super();
this.settings = settings; this.settings = settings;
this.session = cloneSession(session); this.session = cloneSession(session);
this.storagePaths = storagePaths; this.storagePaths = storagePaths;
this.debridService = new DebridService(settings); this.debridService = new DebridService(settings, { megaWebUnrestrict: options.megaWebUnrestrict });
this.applyOnStartCleanupPolicy(); this.applyOnStartCleanupPolicy();
this.normalizeSessionStatuses(); this.normalizeSessionStatuses();
} }

View File

@ -41,6 +41,15 @@ function registerIpcHandlers(): void {
ipcMain.handle(IPC_CHANNELS.GET_SNAPSHOT, () => controller.getSnapshot()); ipcMain.handle(IPC_CHANNELS.GET_SNAPSHOT, () => controller.getSnapshot());
ipcMain.handle(IPC_CHANNELS.GET_VERSION, () => controller.getVersion()); ipcMain.handle(IPC_CHANNELS.GET_VERSION, () => controller.getVersion());
ipcMain.handle(IPC_CHANNELS.CHECK_UPDATES, async () => controller.checkUpdates()); ipcMain.handle(IPC_CHANNELS.CHECK_UPDATES, async () => controller.checkUpdates());
ipcMain.handle(IPC_CHANNELS.INSTALL_UPDATE, async () => {
const result = await controller.installUpdate();
if (result.started) {
setTimeout(() => {
app.quit();
}, 350);
}
return result;
});
ipcMain.handle(IPC_CHANNELS.OPEN_EXTERNAL, async (_event: IpcMainInvokeEvent, rawUrl: string) => { ipcMain.handle(IPC_CHANNELS.OPEN_EXTERNAL, async (_event: IpcMainInvokeEvent, rawUrl: string) => {
try { try {
const parsed = new URL(String(rawUrl || "").trim()); const parsed = new URL(String(rawUrl || "").trim());

View File

@ -0,0 +1,170 @@
import { BrowserWindow } from "electron";
import { UnrestrictedLink } from "./realdebrid";
import { compactErrorText, filenameFromUrl, sleep } from "./utils";
type MegaCredentials = {
login: string;
password: string;
};
type MegaWebResult = {
directUrl: string;
fileName: string;
};
export class MegaWebFallback {
private browser: BrowserWindow | null = null;
private queue: Promise<unknown> = Promise.resolve();
private getCredentials: () => MegaCredentials;
public constructor(getCredentials: () => MegaCredentials) {
this.getCredentials = getCredentials;
}
public async unrestrict(link: string): Promise<UnrestrictedLink | null> {
return this.runExclusive(async () => {
const creds = this.getCredentials();
if (!creds.login.trim() || !creds.password.trim()) {
return null;
}
const browser = await this.ensureBrowser();
const authOk = await this.login(browser, creds.login, creds.password);
if (!authOk) {
throw new Error("Mega-Web-Login fehlgeschlagen");
}
const data = await this.generateLink(browser, link);
if (!data?.directUrl) {
throw new Error("Mega-Web konnte keinen Downloadlink erzeugen");
}
return {
directUrl: data.directUrl,
fileName: data.fileName || filenameFromUrl(link),
fileSize: null,
retriesUsed: 0
};
});
}
private async runExclusive<T>(job: () => Promise<T>): Promise<T> {
const run = this.queue.then(job, job);
this.queue = run.then(() => undefined, () => undefined);
return run;
}
private async ensureBrowser(): Promise<BrowserWindow> {
if (this.browser && !this.browser.isDestroyed()) {
return this.browser;
}
this.browser = new BrowserWindow({
show: false,
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
partition: "persist:mega-web"
}
});
return this.browser;
}
private async login(browser: BrowserWindow, login: string, password: string): Promise<boolean> {
await browser.loadURL("https://www.mega-debrid.eu/index.php?page=login&lang=en");
await sleep(600);
const result = await browser.webContents.executeJavaScript(`(async () => {
const hasLogout = Boolean(document.querySelector('a[href*="logout"], a[href*="debrideur"], a[href*="debrid"]'));
if (hasLogout) return { ok: true };
const form = document.querySelector('#formulaire_login') || document.querySelector('form[action*="form=login"]') || document.querySelector('form');
if (!form) return { ok: false, reason: 'Login-Form nicht gefunden' };
const loginInput = form.querySelector('input[name="login"], #user_login');
const passInput = form.querySelector('input[name="password"], #user_password');
if (!loginInput || !passInput) return { ok: false, reason: 'Login-Felder fehlen' };
loginInput.value = ${JSON.stringify(login)};
passInput.value = ${JSON.stringify(password)};
const submit = form.querySelector('button[type="submit"], input[type="submit"], #user_submit');
if (submit) { submit.click(); } else { form.submit(); }
return { ok: true };
})();`, true);
if (!result?.ok) {
return false;
}
for (let i = 0; i < 30; i += 1) {
await sleep(350);
const url = browser.webContents.getURL();
if (url.includes("page=debrideur") || url.includes("page=debrid")) {
return true;
}
const logged = await browser.webContents.executeJavaScript(
"Boolean(document.querySelector('a[href*=\"debrideur\"], a[href*=\"debrid\"], a[href*=\"logout\"]'))",
true
).catch(() => false);
if (logged) {
return true;
}
}
return false;
}
private async generateLink(browser: BrowserWindow, link: string): Promise<MegaWebResult | null> {
await browser.loadURL("https://www.mega-debrid.eu/index.php?page=debrideur&lang=de");
await sleep(800);
const start = await browser.webContents.executeJavaScript(`(async () => {
const textarea = document.querySelector('textarea');
if (!textarea) return { ok: false, reason: 'Textarea fehlt' };
textarea.value = ${JSON.stringify(link)};
textarea.dispatchEvent(new Event('input', { bubbles: true }));
const controls = Array.from(document.querySelectorAll('button, input[type="submit"], a.btn'));
const trigger = controls.find((el) => {
const text = (el.textContent || el.value || '').toLowerCase();
return text.includes('erzeugen') || text.includes('generate') || text.includes('générer') || text.includes('downloadlink');
});
if (!trigger) return { ok: false, reason: 'Generate-Button fehlt' };
trigger.click();
return { ok: true };
})();`, true);
if (!start?.ok) {
throw new Error(start?.reason || "Mega-Web konnte Request nicht starten");
}
const linkHash = link.toLowerCase();
for (let i = 0; i < 80; i += 1) {
await sleep(500);
const result = await browser.webContents.executeJavaScript(`(() => {
const cards = Array.from(document.querySelectorAll('.acp-box.card, .acp-box, .card'));
for (const card of cards) {
const title = (card.querySelector('.title')?.textContent || '').trim();
const href = card.querySelector('a[href*="unrestrict.link/download/file/"]')?.getAttribute('href') || '';
const fileName = (card.querySelector('.filename')?.textContent || '').trim();
if (!href) continue;
const lowTitle = title.toLowerCase();
if (lowTitle.includes(${JSON.stringify(linkHash)}) || lowTitle.includes(${JSON.stringify(link.toLowerCase())}) || !title) {
return { directUrl: href, fileName };
}
}
return null;
})();`, true).catch(() => null);
if (result?.directUrl) {
return result;
}
}
return null;
}
public dispose(): void {
if (this.browser && !this.browser.isDestroyed()) {
this.browser.destroy();
}
this.browser = null;
}
}
export function compactMegaWebError(error: unknown): string {
return compactErrorText(error);
}

View File

@ -1,7 +1,13 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { APP_VERSION, DEFAULT_UPDATE_REPO } from "./constants"; import { APP_VERSION, DEFAULT_UPDATE_REPO } from "./constants";
import { UpdateCheckResult } from "../shared/types"; import { UpdateCheckResult, UpdateInstallResult } from "../shared/types";
import { compactErrorText } from "./utils"; import { compactErrorText } from "./utils";
type ReleaseAsset = { name: string; browser_download_url: string };
function parseVersionParts(version: string): number[] { function parseVersionParts(version: string): number[] {
const cleaned = version.replace(/^v/i, "").trim(); const cleaned = version.replace(/^v/i, "").trim();
return cleaned.split(".").map((part) => Number(part.replace(/[^0-9].*$/, "") || "0")); return cleaned.split(".").map((part) => Number(part.replace(/[^0-9].*$/, "") || "0"));
@ -50,13 +56,21 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
const latestTag = String(payload.tag_name || `v${APP_VERSION}`).trim(); const latestTag = String(payload.tag_name || `v${APP_VERSION}`).trim();
const latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION; const latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION;
const releaseUrl = String(payload.html_url || fallback.releaseUrl); const releaseUrl = String(payload.html_url || fallback.releaseUrl);
const assets = Array.isArray(payload.assets) ? payload.assets as Array<Record<string, unknown>> : [];
const setup = assets
.map((asset) => ({
name: String(asset.name || ""),
browser_download_url: String(asset.browser_download_url || "")
}))
.find((asset) => /\.setup\..*\.exe$/i.test(asset.name));
return { return {
updateAvailable: isRemoteNewer(APP_VERSION, latestVersion), updateAvailable: isRemoteNewer(APP_VERSION, latestVersion),
currentVersion: APP_VERSION, currentVersion: APP_VERSION,
latestVersion, latestVersion,
latestTag, latestTag,
releaseUrl releaseUrl,
setupAssetUrl: setup?.browser_download_url || ""
}; };
} catch (error) { } catch (error) {
return { return {
@ -65,3 +79,64 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
}; };
} }
} }
async function downloadFile(url: string, targetPath: string): Promise<void> {
const response = await fetch(url, {
headers: {
"User-Agent": "RD-Node-Downloader/1.1.18"
}
});
if (!response.ok || !response.body) {
throw new Error(`Update Download fehlgeschlagen (HTTP ${response.status})`);
}
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
const stream = fs.createWriteStream(targetPath);
await new Promise<void>((resolve, reject) => {
const reader = response.body!.getReader();
const pump = (): void => {
void reader.read().then(({ done, value }) => {
if (done) {
stream.end(() => resolve());
return;
}
if (value) {
stream.write(Buffer.from(value));
}
pump();
}).catch((error) => {
stream.destroy();
reject(error);
});
};
pump();
});
}
export async function installLatestUpdate(repo: string): Promise<UpdateInstallResult> {
const check = await checkGitHubUpdate(repo);
if (check.error) {
return { started: false, message: check.error };
}
if (!check.updateAvailable) {
return { started: false, message: "Kein neues Update verfügbar" };
}
const downloadUrl = check.setupAssetUrl || check.releaseUrl;
if (!check.setupAssetUrl) {
return { started: false, message: "Setup-Asset nicht gefunden" };
}
const fileName = path.basename(new URL(downloadUrl).pathname || "update.exe") || "update.exe";
const targetPath = path.join(os.tmpdir(), "rd-update", fileName);
try {
await downloadFile(downloadUrl, targetPath);
const child = spawn(targetPath, [], {
detached: true,
stdio: "ignore"
});
child.unref();
return { started: true, message: "Update-Installer gestartet" };
} catch (error) {
return { started: false, message: compactErrorText(error) };
}
}

View File

@ -7,6 +7,7 @@ const api: ElectronApi = {
getSnapshot: (): Promise<UiSnapshot> => ipcRenderer.invoke(IPC_CHANNELS.GET_SNAPSHOT), getSnapshot: (): Promise<UiSnapshot> => ipcRenderer.invoke(IPC_CHANNELS.GET_SNAPSHOT),
getVersion: (): Promise<string> => ipcRenderer.invoke(IPC_CHANNELS.GET_VERSION), getVersion: (): Promise<string> => ipcRenderer.invoke(IPC_CHANNELS.GET_VERSION),
checkUpdates: (): Promise<UpdateCheckResult> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_UPDATES), checkUpdates: (): Promise<UpdateCheckResult> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_UPDATES),
installUpdate: () => ipcRenderer.invoke(IPC_CHANNELS.INSTALL_UPDATE),
openExternal: (url: string): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_EXTERNAL, url), openExternal: (url: string): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_EXTERNAL, url),
updateSettings: (settings: Partial<AppSettings>): Promise<AppSettings> => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_SETTINGS, settings), updateSettings: (settings: Partial<AppSettings>): Promise<AppSettings> => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_SETTINGS, settings),
addLinks: (payload: AddLinksPayload): Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }> => addLinks: (payload: AddLinksPayload): Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }> =>

View File

@ -7,6 +7,8 @@ const emptySnapshot = (): UiSnapshot => ({
settings: { settings: {
token: "", token: "",
megaToken: "", megaToken: "",
megaLogin: "",
megaPassword: "",
bestToken: "", bestToken: "",
allDebridToken: "", allDebridToken: "",
rememberToken: true, rememberToken: true,
@ -122,7 +124,7 @@ export function App(): ReactElement {
} }
const approved = window.confirm( const approved = window.confirm(
`Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})\n\nJetzt Download-Seite öffnen?` `Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})\n\nJetzt automatisch herunterladen und installieren?`
); );
if (!approved) { if (!approved) {
setStatusToast(`Update verfügbar: ${result.latestTag}`); setStatusToast(`Update verfügbar: ${result.latestTag}`);
@ -130,9 +132,15 @@ export function App(): ReactElement {
return; return;
} }
const opened = await window.rd.openExternal(result.releaseUrl); const install = await window.rd.installUpdate();
setStatusToast(opened ? "Download-Seite im Browser geöffnet" : "Konnte Download-Seite nicht öffnen"); if (install.started) {
setTimeout(() => setStatusToast(""), 2600); setStatusToast("Updater gestartet - App wird geschlossen");
setTimeout(() => setStatusToast(""), 2600);
return;
}
setStatusToast(`Auto-Update fehlgeschlagen: ${install.message}`);
setTimeout(() => setStatusToast(""), 3200);
}; };
const onSaveSettings = async (): Promise<void> => { const onSaveSettings = async (): Promise<void> => {
@ -307,6 +315,17 @@ export function App(): ReactElement {
value={settingsDraft.megaToken} value={settingsDraft.megaToken}
onChange={(event) => setText("megaToken", event.target.value)} onChange={(event) => setText("megaToken", event.target.value)}
/> />
<label>Mega-Debrid Login (Web Fallback)</label>
<input
value={settingsDraft.megaLogin}
onChange={(event) => setText("megaLogin", event.target.value)}
/>
<label>Mega-Debrid Passwort (Web Fallback)</label>
<input
type="password"
value={settingsDraft.megaPassword}
onChange={(event) => setText("megaPassword", event.target.value)}
/>
<label>BestDebrid API Token</label> <label>BestDebrid API Token</label>
<input <input
type="password" type="password"

View File

@ -2,6 +2,7 @@ export const IPC_CHANNELS = {
GET_SNAPSHOT: "app:get-snapshot", GET_SNAPSHOT: "app:get-snapshot",
GET_VERSION: "app:get-version", GET_VERSION: "app:get-version",
CHECK_UPDATES: "app:check-updates", CHECK_UPDATES: "app:check-updates",
INSTALL_UPDATE: "app:install-update",
OPEN_EXTERNAL: "app:open-external", OPEN_EXTERNAL: "app:open-external",
UPDATE_SETTINGS: "app:update-settings", UPDATE_SETTINGS: "app:update-settings",
ADD_LINKS: "queue:add-links", ADD_LINKS: "queue:add-links",

View File

@ -1,9 +1,10 @@
import type { AddLinksPayload, AppSettings, UiSnapshot, UpdateCheckResult } from "./types"; import type { AddLinksPayload, AppSettings, UiSnapshot, UpdateCheckResult, UpdateInstallResult } from "./types";
export interface ElectronApi { export interface ElectronApi {
getSnapshot: () => Promise<UiSnapshot>; getSnapshot: () => Promise<UiSnapshot>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
checkUpdates: () => Promise<UpdateCheckResult>; checkUpdates: () => Promise<UpdateCheckResult>;
installUpdate: () => Promise<UpdateInstallResult>;
openExternal: (url: string) => Promise<boolean>; openExternal: (url: string) => Promise<boolean>;
updateSettings: (settings: Partial<AppSettings>) => Promise<AppSettings>; updateSettings: (settings: Partial<AppSettings>) => Promise<AppSettings>;
addLinks: (payload: AddLinksPayload) => Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }>; addLinks: (payload: AddLinksPayload) => Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }>;

View File

@ -19,6 +19,8 @@ export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "allde
export interface AppSettings { export interface AppSettings {
token: string; token: string;
megaToken: string; megaToken: string;
megaLogin: string;
megaPassword: string;
bestToken: string; bestToken: string;
allDebridToken: string; allDebridToken: string;
rememberToken: boolean; rememberToken: boolean;
@ -143,9 +145,15 @@ export interface UpdateCheckResult {
latestVersion: string; latestVersion: string;
latestTag: string; latestTag: string;
releaseUrl: string; releaseUrl: string;
setupAssetUrl?: string;
error?: string; error?: string;
} }
export interface UpdateInstallResult {
started: boolean;
message: string;
}
export interface ParsedHashEntry { export interface ParsedHashEntry {
fileName: string; fileName: string;
algorithm: "crc32" | "md5" | "sha1"; algorithm: "crc32" | "md5" | "sha1";