Add Mega-Debrid account info check (web scraping)

Scrapes the Mega-Debrid profile page to display username, premium status,
remaining days, and loyalty points. New "Account prüfen" button in Settings > Accounts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-03 14:06:19 +01:00
parent ac479bb023
commit e6ec1ed755
10 changed files with 113 additions and 2 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.5.50", "version": "1.5.51",
"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

@ -7,6 +7,7 @@ import {
DuplicatePolicy, DuplicatePolicy,
HistoryEntry, HistoryEntry,
ParsedPackageInput, ParsedPackageInput,
ProviderAccountInfo,
SessionStats, SessionStats,
StartConflictEntry, StartConflictEntry,
StartConflictResolutionResult, StartConflictResolutionResult,
@ -327,6 +328,10 @@ export class AppController {
removeHistoryEntry(this.storagePaths, entryId); removeHistoryEntry(this.storagePaths, entryId);
} }
public async checkMegaAccount(): Promise<ProviderAccountInfo> {
return this.megaWebFallback.getAccountInfo();
}
public addToHistory(entry: HistoryEntry): void { public addToHistory(entry: HistoryEntry): void {
addHistoryEntry(this.storagePaths, entry); addHistoryEntry(this.storagePaths, entry);
} }

View File

@ -384,6 +384,13 @@ function registerIpcHandlers(): void {
const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options); const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options);
return result.canceled ? [] : result.filePaths; return result.canceled ? [] : result.filePaths;
}); });
ipcMain.handle(IPC_CHANNELS.CHECK_ACCOUNT, async (_event: IpcMainInvokeEvent, provider: string) => {
validateString(provider, "provider");
if (provider === "megadebrid") {
return controller.checkMegaAccount();
}
return { provider, username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Nicht unterstützt" };
});
ipcMain.handle(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats()); ipcMain.handle(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats());
ipcMain.handle(IPC_CHANNELS.RESTART, () => { ipcMain.handle(IPC_CHANNELS.RESTART, () => {

View File

@ -1,3 +1,4 @@
import { ProviderAccountInfo } from "../shared/types";
import { UnrestrictedLink } from "./realdebrid"; import { UnrestrictedLink } from "./realdebrid";
import { compactErrorText, filenameFromUrl, sleep } from "./utils"; import { compactErrorText, filenameFromUrl, sleep } from "./utils";
@ -15,6 +16,7 @@ const LOGIN_URL = "https://www.mega-debrid.eu/index.php?form=login";
const DEBRID_URL = "https://www.mega-debrid.eu/index.php?form=debrid"; const DEBRID_URL = "https://www.mega-debrid.eu/index.php?form=debrid";
const DEBRID_AJAX_URL = "https://www.mega-debrid.eu/index.php?ajax=debrid&json"; const DEBRID_AJAX_URL = "https://www.mega-debrid.eu/index.php?ajax=debrid&json";
const DEBRID_REFERER = "https://www.mega-debrid.eu/index.php?page=debrideur&lang=de"; const DEBRID_REFERER = "https://www.mega-debrid.eu/index.php?page=debrideur&lang=de";
const PROFILE_URL = "https://www.mega-debrid.eu/index.php?page=profil";
function normalizeLink(link: string): string { function normalizeLink(link: string): string {
return link.trim().toLowerCase(); return link.trim().toLowerCase();
@ -264,6 +266,51 @@ export class MegaWebFallback {
}, signal); }, signal);
} }
public async getAccountInfo(): Promise<ProviderAccountInfo> {
return this.runExclusive(async () => {
const creds = this.getCredentials();
if (!creds.login.trim() || !creds.password.trim()) {
return { provider: "megadebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Login/Passwort nicht konfiguriert" };
}
try {
if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) {
await this.login(creds.login, creds.password);
}
const res = await fetch(PROFILE_URL, {
method: "GET",
headers: {
"User-Agent": "Mozilla/5.0",
Cookie: this.cookie,
Referer: DEBRID_REFERER
},
signal: AbortSignal.timeout(30000)
});
const html = await res.text();
const usernameMatch = html.match(/<a[^>]*id=["']user_link["'][^>]*><span>([^<]+)<\/span>/i);
const username = usernameMatch?.[1]?.trim() || "";
const typeMatch = html.match(/(Premiumuser|Freeuser)\s*-\s*(\d+)\s*Tag/i);
const accountType = typeMatch?.[1] || "Unbekannt";
const daysRemaining = typeMatch?.[2] ? parseInt(typeMatch[2], 10) : null;
const pointsMatch = html.match(/(\d+)\s*Treuepunkte/i);
const loyaltyPoints = pointsMatch?.[1] ? parseInt(pointsMatch[1], 10) : null;
if (!username && !typeMatch) {
this.cookie = "";
return { provider: "megadebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Profil konnte nicht gelesen werden (Session ungültig?)" };
}
return { provider: "megadebrid", username, accountType, daysRemaining, loyaltyPoints };
} catch (err) {
return { provider: "megadebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: compactErrorText(err) };
}
});
}
public invalidateSession(): void { public invalidateSession(): void {
this.cookie = ""; this.cookie = "";
this.cookieSetAt = 0; this.cookieSetAt = 0;

View File

@ -4,6 +4,7 @@ import {
AppSettings, AppSettings,
DuplicatePolicy, DuplicatePolicy,
HistoryEntry, HistoryEntry,
ProviderAccountInfo,
SessionStats, SessionStats,
StartConflictEntry, StartConflictEntry,
StartConflictResolutionResult, StartConflictResolutionResult,
@ -53,6 +54,7 @@ const api: ElectronApi = {
getHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_HISTORY), getHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_HISTORY),
clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY), clearHistory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId), removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId),
checkAccount: (provider: string): Promise<ProviderAccountInfo> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_ACCOUNT, provider),
onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => { onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => {
const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot); const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot);
ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener); ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener);

View File

@ -10,6 +10,7 @@ import type {
DuplicatePolicy, DuplicatePolicy,
HistoryEntry, HistoryEntry,
PackageEntry, PackageEntry,
ProviderAccountInfo,
StartConflictEntry, StartConflictEntry,
UiSnapshot, UiSnapshot,
UpdateCheckResult, UpdateCheckResult,
@ -442,6 +443,8 @@ export function App(): ReactElement {
const latestStateRef = useRef<UiSnapshot | null>(null); const latestStateRef = useRef<UiSnapshot | null>(null);
const stateFlushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const stateFlushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [megaAccountInfo, setMegaAccountInfo] = useState<ProviderAccountInfo | null>(null);
const [megaAccountLoading, setMegaAccountLoading] = useState(false);
const [dragOver, setDragOver] = useState(false); const [dragOver, setDragOver] = useState(false);
const [editingPackageId, setEditingPackageId] = useState<string | null>(null); const [editingPackageId, setEditingPackageId] = useState<string | null>(null);
const [editingName, setEditingName] = useState(""); const [editingName, setEditingName] = useState("");
@ -2390,6 +2393,23 @@ export function App(): ReactElement {
<input value={settingsDraft.megaLogin} onChange={(e) => setText("megaLogin", e.target.value)} /> <input value={settingsDraft.megaLogin} onChange={(e) => setText("megaLogin", e.target.value)} />
<label>Mega-Debrid Passwort</label> <label>Mega-Debrid Passwort</label>
<input type="password" value={settingsDraft.megaPassword} onChange={(e) => setText("megaPassword", e.target.value)} /> <input type="password" value={settingsDraft.megaPassword} onChange={(e) => setText("megaPassword", e.target.value)} />
<button className="btn" disabled={megaAccountLoading || !settingsDraft.megaLogin.trim() || !settingsDraft.megaPassword.trim()} onClick={() => {
setMegaAccountLoading(true);
setMegaAccountInfo(null);
window.rd.checkAccount("megadebrid").then((info) => {
setMegaAccountInfo(info);
}).catch(() => {
setMegaAccountInfo({ provider: "megadebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Verbindungsfehler" });
}).finally(() => {
setMegaAccountLoading(false);
});
}}>{megaAccountLoading ? "Prüfe…" : "Account prüfen"}</button>
{megaAccountInfo && !megaAccountInfo.error && (
<div className="account-info account-info-success"> {megaAccountInfo.username} {megaAccountInfo.accountType}{megaAccountInfo.daysRemaining !== null ? `${megaAccountInfo.daysRemaining} Tage` : ""}{megaAccountInfo.loyaltyPoints !== null ? `${megaAccountInfo.loyaltyPoints} Treuepunkte` : ""}</div>
)}
{megaAccountInfo?.error && (
<div className="account-info account-info-error"> {megaAccountInfo.error}</div>
)}
<label>BestDebrid API Token</label> <label>BestDebrid API Token</label>
<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>

View File

@ -1465,6 +1465,24 @@ td {
} }
} }
.account-info {
font-size: 13px;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--field);
}
.account-info-success {
color: #4ade80;
border-color: rgba(74, 222, 128, 0.35);
}
.account-info-error {
color: var(--danger);
border-color: rgba(244, 63, 94, 0.35);
}
.schedule-row { .schedule-row {
display: grid; display: grid;
grid-template-columns: 56px auto 56px auto 92px auto auto auto; grid-template-columns: 56px auto 56px auto 92px auto auto auto;

View File

@ -36,5 +36,6 @@ export const IPC_CHANNELS = {
EXTRACT_NOW: "queue:extract-now", EXTRACT_NOW: "queue:extract-now",
GET_HISTORY: "history:get", GET_HISTORY: "history:get",
CLEAR_HISTORY: "history:clear", CLEAR_HISTORY: "history:clear",
REMOVE_HISTORY_ENTRY: "history:remove-entry" REMOVE_HISTORY_ENTRY: "history:remove-entry",
CHECK_ACCOUNT: "app:check-account"
} as const; } as const;

View File

@ -3,6 +3,7 @@ import type {
AppSettings, AppSettings,
DuplicatePolicy, DuplicatePolicy,
HistoryEntry, HistoryEntry,
ProviderAccountInfo,
SessionStats, SessionStats,
StartConflictEntry, StartConflictEntry,
StartConflictResolutionResult, StartConflictResolutionResult,
@ -48,6 +49,7 @@ export interface ElectronApi {
getHistory: () => Promise<HistoryEntry[]>; getHistory: () => Promise<HistoryEntry[]>;
clearHistory: () => Promise<void>; clearHistory: () => Promise<void>;
removeHistoryEntry: (entryId: string) => Promise<void>; removeHistoryEntry: (entryId: string) => Promise<void>;
checkAccount: (provider: string) => Promise<ProviderAccountInfo>;
onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void; onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void;
onClipboardDetected: (callback: (links: string[]) => void) => () => void; onClipboardDetected: (callback: (links: string[]) => void) => () => void;
onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void; onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void;

View File

@ -269,6 +269,15 @@ export interface HistoryEntry {
outputDir: string; outputDir: string;
} }
export interface ProviderAccountInfo {
provider: DebridProvider;
username: string;
accountType: string;
daysRemaining: number | null;
loyaltyPoints: number | null;
error?: string;
}
export interface HistoryState { export interface HistoryState {
entries: HistoryEntry[]; entries: HistoryEntry[];
maxEntries: number; maxEntries: number;