Restore in-app updater and add Mega web fallback path
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
704826b421
commit
0e898733d6
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.1.18",
|
||||
"version": "1.1.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.1.18",
|
||||
"version": "1.1.19",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.1.18",
|
||||
"version": "1.1.19",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -1,26 +1,35 @@
|
||||
import path from "node:path";
|
||||
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 { APP_VERSION, defaultSettings } from "./constants";
|
||||
import { DownloadManager } from "./download-manager";
|
||||
import { parseCollectorInput } from "./link-parser";
|
||||
import { configureLogger, logger } from "./logger";
|
||||
import { MegaWebFallback } from "./mega-web-fallback";
|
||||
import { createStoragePaths, emptySession, loadSession, loadSettings, saveSettings } from "./storage";
|
||||
import { checkGitHubUpdate } from "./update";
|
||||
import { checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||
|
||||
export class AppController {
|
||||
private settings: AppSettings;
|
||||
|
||||
private manager: DownloadManager;
|
||||
|
||||
private megaWebFallback: MegaWebFallback;
|
||||
|
||||
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
|
||||
|
||||
public constructor() {
|
||||
configureLogger(this.storagePaths.baseDir);
|
||||
this.settings = loadSettings(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.onState?.(snapshot);
|
||||
});
|
||||
@ -69,6 +78,10 @@ export class AppController {
|
||||
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 } {
|
||||
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
|
||||
if (parsed.length === 0) {
|
||||
@ -110,6 +123,7 @@ export class AppController {
|
||||
|
||||
public shutdown(): void {
|
||||
this.manager.stop();
|
||||
this.megaWebFallback.dispose();
|
||||
logger.info("App beendet");
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import os from "node:os";
|
||||
import { AppSettings } from "../shared/types";
|
||||
|
||||
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 DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
||||
@ -29,6 +29,8 @@ export function defaultSettings(): AppSettings {
|
||||
return {
|
||||
token: "",
|
||||
megaToken: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
rememberToken: true,
|
||||
|
||||
@ -20,6 +20,12 @@ interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
||||
providerLabel: string;
|
||||
}
|
||||
|
||||
export type MegaWebUnrestrictor = (link: string) => Promise<UnrestrictedLink | null>;
|
||||
|
||||
interface DebridServiceOptions {
|
||||
megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||
}
|
||||
|
||||
type BestDebridRequest = {
|
||||
url: string;
|
||||
useAuthHeader: boolean;
|
||||
@ -191,8 +197,11 @@ function buildBestDebridRequests(link: string, token: string): BestDebridRequest
|
||||
class MegaDebridClient {
|
||||
private token: string;
|
||||
|
||||
public constructor(token: string) {
|
||||
private megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||
|
||||
public constructor(token: string, megaWebUnrestrict?: MegaWebUnrestrictor) {
|
||||
this.token = token;
|
||||
this.megaWebUnrestrict = megaWebUnrestrict;
|
||||
}
|
||||
|
||||
private normalizeMegaCandidates(link: string): string[] {
|
||||
@ -298,6 +307,14 @@ class MegaDebridClient {
|
||||
if (/token error|vip_end/i.test(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) {
|
||||
lastError = compactErrorText(error);
|
||||
if (attempt >= REQUEST_RETRIES) {
|
||||
@ -533,8 +550,11 @@ export class DebridService {
|
||||
|
||||
private allDebridClient: AllDebridClient;
|
||||
|
||||
public constructor(settings: AppSettings) {
|
||||
private options: DebridServiceOptions;
|
||||
|
||||
public constructor(settings: AppSettings, options: DebridServiceOptions = {}) {
|
||||
this.settings = settings;
|
||||
this.options = options;
|
||||
this.realDebridClient = new RealDebridClient(settings.token);
|
||||
this.allDebridClient = new AllDebridClient(settings.allDebridToken);
|
||||
}
|
||||
@ -634,7 +654,7 @@ export class DebridService {
|
||||
return this.realDebridClient.unrestrictLink(link);
|
||||
}
|
||||
if (provider === "megadebrid") {
|
||||
return new MegaDebridClient(token).unrestrictLink(link);
|
||||
return new MegaDebridClient(token, this.options.megaWebUnrestrict).unrestrictLink(link);
|
||||
}
|
||||
if (provider === "alldebrid") {
|
||||
return this.allDebridClient.unrestrictLink(link);
|
||||
|
||||
@ -6,7 +6,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { AppSettings, DownloadItem, DownloadSummary, DownloadStatus, PackageEntry, ParsedPackageInput, SessionState, UiSnapshot } from "../shared/types";
|
||||
import { CHUNK_SIZE, REQUEST_RETRIES } from "./constants";
|
||||
import { cleanupCancelledPackageArtifacts, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||
import { DebridService } from "./debrid";
|
||||
import { DebridService, MegaWebUnrestrictor } from "./debrid";
|
||||
import { extractPackageArchives } from "./extractor";
|
||||
import { validateFileAgainstManifest } from "./integrity";
|
||||
import { logger } from "./logger";
|
||||
@ -23,6 +23,10 @@ type ActiveTask = {
|
||||
nonResumableCounted: boolean;
|
||||
};
|
||||
|
||||
type DownloadManagerOptions = {
|
||||
megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||
};
|
||||
|
||||
function cloneSession(session: SessionState): SessionState {
|
||||
return JSON.parse(JSON.stringify(session)) as SessionState;
|
||||
}
|
||||
@ -103,12 +107,12 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
private speedBytesLastWindow = 0;
|
||||
|
||||
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths) {
|
||||
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
|
||||
super();
|
||||
this.settings = settings;
|
||||
this.session = cloneSession(session);
|
||||
this.storagePaths = storagePaths;
|
||||
this.debridService = new DebridService(settings);
|
||||
this.debridService = new DebridService(settings, { megaWebUnrestrict: options.megaWebUnrestrict });
|
||||
this.applyOnStartCleanupPolicy();
|
||||
this.normalizeSessionStatuses();
|
||||
}
|
||||
|
||||
@ -41,6 +41,15 @@ function registerIpcHandlers(): void {
|
||||
ipcMain.handle(IPC_CHANNELS.GET_SNAPSHOT, () => controller.getSnapshot());
|
||||
ipcMain.handle(IPC_CHANNELS.GET_VERSION, () => controller.getVersion());
|
||||
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) => {
|
||||
try {
|
||||
const parsed = new URL(String(rawUrl || "").trim());
|
||||
|
||||
170
src/main/mega-web-fallback.ts
Normal file
170
src/main/mega-web-fallback.ts
Normal 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);
|
||||
}
|
||||
@ -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 { UpdateCheckResult } from "../shared/types";
|
||||
import { UpdateCheckResult, UpdateInstallResult } from "../shared/types";
|
||||
import { compactErrorText } from "./utils";
|
||||
|
||||
type ReleaseAsset = { name: string; browser_download_url: string };
|
||||
|
||||
function parseVersionParts(version: string): number[] {
|
||||
const cleaned = version.replace(/^v/i, "").trim();
|
||||
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 latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION;
|
||||
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 {
|
||||
updateAvailable: isRemoteNewer(APP_VERSION, latestVersion),
|
||||
currentVersion: APP_VERSION,
|
||||
latestVersion,
|
||||
latestTag,
|
||||
releaseUrl
|
||||
releaseUrl,
|
||||
setupAssetUrl: setup?.browser_download_url || ""
|
||||
};
|
||||
} catch (error) {
|
||||
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) };
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ const api: ElectronApi = {
|
||||
getSnapshot: (): Promise<UiSnapshot> => ipcRenderer.invoke(IPC_CHANNELS.GET_SNAPSHOT),
|
||||
getVersion: (): Promise<string> => ipcRenderer.invoke(IPC_CHANNELS.GET_VERSION),
|
||||
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),
|
||||
updateSettings: (settings: Partial<AppSettings>): Promise<AppSettings> => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_SETTINGS, settings),
|
||||
addLinks: (payload: AddLinksPayload): Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }> =>
|
||||
|
||||
@ -7,6 +7,8 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
settings: {
|
||||
token: "",
|
||||
megaToken: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
rememberToken: true,
|
||||
@ -122,7 +124,7 @@ export function App(): ReactElement {
|
||||
}
|
||||
|
||||
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) {
|
||||
setStatusToast(`Update verfügbar: ${result.latestTag}`);
|
||||
@ -130,9 +132,15 @@ export function App(): ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const opened = await window.rd.openExternal(result.releaseUrl);
|
||||
setStatusToast(opened ? "Download-Seite im Browser geöffnet" : "Konnte Download-Seite nicht öffnen");
|
||||
const install = await window.rd.installUpdate();
|
||||
if (install.started) {
|
||||
setStatusToast("Updater gestartet - App wird geschlossen");
|
||||
setTimeout(() => setStatusToast(""), 2600);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatusToast(`Auto-Update fehlgeschlagen: ${install.message}`);
|
||||
setTimeout(() => setStatusToast(""), 3200);
|
||||
};
|
||||
|
||||
const onSaveSettings = async (): Promise<void> => {
|
||||
@ -307,6 +315,17 @@ export function App(): ReactElement {
|
||||
value={settingsDraft.megaToken}
|
||||
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>
|
||||
<input
|
||||
type="password"
|
||||
|
||||
@ -2,6 +2,7 @@ export const IPC_CHANNELS = {
|
||||
GET_SNAPSHOT: "app:get-snapshot",
|
||||
GET_VERSION: "app:get-version",
|
||||
CHECK_UPDATES: "app:check-updates",
|
||||
INSTALL_UPDATE: "app:install-update",
|
||||
OPEN_EXTERNAL: "app:open-external",
|
||||
UPDATE_SETTINGS: "app:update-settings",
|
||||
ADD_LINKS: "queue:add-links",
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import type { AddLinksPayload, AppSettings, UiSnapshot, UpdateCheckResult } from "./types";
|
||||
import type { AddLinksPayload, AppSettings, UiSnapshot, UpdateCheckResult, UpdateInstallResult } from "./types";
|
||||
|
||||
export interface ElectronApi {
|
||||
getSnapshot: () => Promise<UiSnapshot>;
|
||||
getVersion: () => Promise<string>;
|
||||
checkUpdates: () => Promise<UpdateCheckResult>;
|
||||
installUpdate: () => Promise<UpdateInstallResult>;
|
||||
openExternal: (url: string) => Promise<boolean>;
|
||||
updateSettings: (settings: Partial<AppSettings>) => Promise<AppSettings>;
|
||||
addLinks: (payload: AddLinksPayload) => Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }>;
|
||||
|
||||
@ -19,6 +19,8 @@ export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "allde
|
||||
export interface AppSettings {
|
||||
token: string;
|
||||
megaToken: string;
|
||||
megaLogin: string;
|
||||
megaPassword: string;
|
||||
bestToken: string;
|
||||
allDebridToken: string;
|
||||
rememberToken: boolean;
|
||||
@ -143,9 +145,15 @@ export interface UpdateCheckResult {
|
||||
latestVersion: string;
|
||||
latestTag: string;
|
||||
releaseUrl: string;
|
||||
setupAssetUrl?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface UpdateInstallResult {
|
||||
started: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ParsedHashEntry {
|
||||
fileName: string;
|
||||
algorithm: "crc32" | "md5" | "sha1";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user