From 0e898733d61e7cf3c61e37a9ae8ac4d21d179c71 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 05:28:50 +0100 Subject: [PATCH] Restore in-app updater and add Mega web fallback path --- package-lock.json | 4 +- package.json | 2 +- src/main/app-controller.ts | 20 +++- src/main/constants.ts | 4 +- src/main/debrid.ts | 26 +++++- src/main/download-manager.ts | 10 +- src/main/main.ts | 9 ++ src/main/mega-web-fallback.ts | 170 ++++++++++++++++++++++++++++++++++ src/main/update.ts | 79 +++++++++++++++- src/preload/preload.ts | 1 + src/renderer/App.tsx | 27 +++++- src/shared/ipc.ts | 1 + src/shared/preload-api.ts | 3 +- src/shared/types.ts | 8 ++ 14 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 src/main/mega-web-fallback.ts diff --git a/package-lock.json b/package-lock.json index 08861a1..1ea49bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bed5ce5..7e08803 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 364e7b3..556851a 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -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 { + 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"); } } diff --git a/src/main/constants.ts b/src/main/constants.ts index fe57b43..d8fada8 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -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, diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 8e6574f..d378ae9 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -20,6 +20,12 @@ interface ProviderUnrestrictedLink extends UnrestrictedLink { providerLabel: string; } +export type MegaWebUnrestrictor = (link: string) => Promise; + +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); diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index a2f7111..001ce2b 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -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(); } diff --git a/src/main/main.ts b/src/main/main.ts index cee7560..1ec11ec 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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()); diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts new file mode 100644 index 0000000..9b5fed5 --- /dev/null +++ b/src/main/mega-web-fallback.ts @@ -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 = Promise.resolve(); + + private getCredentials: () => MegaCredentials; + + public constructor(getCredentials: () => MegaCredentials) { + this.getCredentials = getCredentials; + } + + public async unrestrict(link: string): Promise { + 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(job: () => Promise): Promise { + const run = this.queue.then(job, job); + this.queue = run.then(() => undefined, () => undefined); + return run; + } + + private async ensureBrowser(): Promise { + 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 { + 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 { + 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); +} diff --git a/src/main/update.ts b/src/main/update.ts index 9f0cff9..6933523 100644 --- a/src/main/update.ts +++ b/src/main/update.ts @@ -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> : []; + 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 { + 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((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 { + 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) }; + } +} diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 0c63b9a..894e400 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -7,6 +7,7 @@ const api: ElectronApi = { getSnapshot: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_SNAPSHOT), getVersion: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_VERSION), checkUpdates: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_UPDATES), + installUpdate: () => ipcRenderer.invoke(IPC_CHANNELS.INSTALL_UPDATE), openExternal: (url: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.OPEN_EXTERNAL, url), updateSettings: (settings: Partial): Promise => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_SETTINGS, settings), addLinks: (payload: AddLinksPayload): Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }> => diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 1d61571..0e1eca5 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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"); - setTimeout(() => setStatusToast(""), 2600); + 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 => { @@ -307,6 +315,17 @@ export function App(): ReactElement { value={settingsDraft.megaToken} onChange={(event) => setText("megaToken", event.target.value)} /> + + setText("megaLogin", event.target.value)} + /> + + setText("megaPassword", event.target.value)} + /> Promise; getVersion: () => Promise; checkUpdates: () => Promise; + installUpdate: () => Promise; openExternal: (url: string) => Promise; updateSettings: (settings: Partial) => Promise; addLinks: (payload: AddLinksPayload) => Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }>; diff --git a/src/shared/types.ts b/src/shared/types.ts index a2bf5e1..b4ccf18 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -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";