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:
parent
ac479bb023
commit
e6ec1ed755
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, () => {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user