Move provider settings to tab and improve DLC filename resolution
Some checks are pending
Build and Release / build (push) Waiting to run

This commit is contained in:
Sucukdeluxe 2026-02-27 04:40:21 +01:00
parent 02370a40b4
commit 3ef2ee732a
6 changed files with 197 additions and 38 deletions

4
package-lock.json generated
View File

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

View File

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

View File

@ -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.15";
export const APP_VERSION = "1.1.16";
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";

View File

@ -24,6 +24,16 @@ type BestDebridRequest = {
useAuthHeader: boolean;
};
function canonicalLink(link: string): string {
try {
const parsed = new URL(link);
const query = parsed.searchParams.toString();
return `${parsed.hostname}${parsed.pathname}${query ? `?${query}` : ""}`.toLowerCase();
} catch {
return link.trim().toLowerCase();
}
}
function shouldRetryStatus(status: number): boolean {
return status === 429 || status >= 500;
}
@ -262,6 +272,79 @@ class AllDebridClient {
this.token = token;
}
public async getLinkInfos(links: string[]): Promise<Map<string, string>> {
const result = new Map<string, string>();
const canonicalToInput = new Map<string, string>();
const uniqueLinks: string[] = [];
for (const link of links) {
const trimmed = link.trim();
if (!trimmed) {
continue;
}
const canonical = canonicalLink(trimmed);
if (canonicalToInput.has(canonical)) {
continue;
}
canonicalToInput.set(canonical, trimmed);
uniqueLinks.push(trimmed);
}
for (let index = 0; index < uniqueLinks.length; index += 32) {
const chunk = uniqueLinks.slice(index, index + 32);
const body = new URLSearchParams();
for (const link of chunk) {
body.append("link[]", link);
}
const response = await fetch(`${ALL_DEBRID_API_BASE}/link/infos`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "RD-Node-Downloader/1.1.15"
},
body
});
const text = await response.text();
const payload = asRecord(parseJson(text));
if (!response.ok) {
throw new Error(parseError(response.status, text, payload));
}
const status = pickString(payload, ["status"]);
if (status && status.toLowerCase() === "error") {
const errorObj = asRecord(payload?.error);
throw new Error(pickString(errorObj, ["message", "code"]) || "AllDebrid API error");
}
const data = asRecord(payload?.data);
const infos = Array.isArray(data?.infos) ? data.infos : [];
for (let i = 0; i < infos.length; i += 1) {
const info = asRecord(infos[i]);
if (!info) {
continue;
}
const fileName = pickString(info, ["filename", "fileName"]);
if (!fileName) {
continue;
}
const responseLink = pickString(info, ["link"]);
const byResponse = canonicalToInput.get(canonicalLink(responseLink));
const byIndex = chunk[i] || "";
const original = byResponse || byIndex;
if (!original) {
continue;
}
result.set(original, fileName);
}
}
return result;
}
public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
@ -337,6 +420,31 @@ export class DebridService {
this.allDebridClient = new AllDebridClient(next.allDebridToken);
}
public async resolveFilenames(links: string[]): Promise<Map<string, string>> {
const unresolved = links.filter((link) => filenameFromUrl(link) === "download.bin");
if (unresolved.length === 0) {
return new Map<string, string>();
}
const token = this.settings.allDebridToken.trim();
if (!token) {
return new Map<string, string>();
}
try {
const infos = await this.allDebridClient.getLinkInfos(unresolved);
const clean = new Map<string, string>();
for (const [link, fileName] of infos.entries()) {
if (fileName.trim() && fileName.trim().toLowerCase() !== "download.bin") {
clean.set(link, fileName.trim());
}
}
return clean;
} catch {
return new Map<string, string>();
}
}
public async unrestrictLink(link: string): Promise<ProviderUnrestrictedLink> {
const order = uniqueProviderOrder([
this.settings.providerPrimary,

View File

@ -165,6 +165,7 @@ export class DownloadManager extends EventEmitter {
public addPackages(packages: ParsedPackageInput[]): { addedPackages: number; addedLinks: number } {
let addedPackages = 0;
let addedLinks = 0;
const unresolvedByLink = new Map<string, string[]>();
for (const pkg of packages) {
const links = pkg.links.filter((link) => !!link.trim());
if (links.length === 0) {
@ -211,6 +212,11 @@ export class DownloadManager extends EventEmitter {
};
packageEntry.itemIds.push(itemId);
this.session.items[itemId] = item;
if (fileName === "download.bin") {
const existing = unresolvedByLink.get(link) ?? [];
existing.push(itemId);
unresolvedByLink.set(link, existing);
}
addedLinks += 1;
}
@ -221,9 +227,54 @@ export class DownloadManager extends EventEmitter {
this.persistSoon();
this.emitState();
if (unresolvedByLink.size > 0) {
void this.resolveQueuedFilenames(unresolvedByLink);
}
return { addedPackages, addedLinks };
}
private async resolveQueuedFilenames(unresolvedByLink: Map<string, string[]>): Promise<void> {
try {
const resolved = await this.debridService.resolveFilenames(Array.from(unresolvedByLink.keys()));
if (resolved.size === 0) {
return;
}
let changed = false;
for (const [link, itemIds] of unresolvedByLink.entries()) {
const fileName = resolved.get(link);
if (!fileName || fileName.toLowerCase() === "download.bin") {
continue;
}
const normalized = sanitizeFilename(fileName);
if (!normalized || normalized.toLowerCase() === "download.bin") {
continue;
}
for (const itemId of itemIds) {
const item = this.session.items[itemId];
if (!item) {
continue;
}
if (item.fileName !== "download.bin") {
continue;
}
item.fileName = normalized;
item.targetPath = path.join(this.session.packages[item.packageId]?.outputDir || this.settings.outputDir, normalized);
item.updatedAt = nowMs();
changed = true;
}
}
if (changed) {
this.persistSoon();
this.emitState();
}
} catch (error) {
logger.warn(`Dateinamen-Resolve fehlgeschlagen: ${compactErrorText(error)}`);
}
}
public cancelPackage(packageId: string): void {
const pkg = this.session.packages[packageId];
if (!pkg) {

View File

@ -235,6 +235,40 @@ export function App(): ReactElement {
<main className="tab-content">
{tab === "collector" && (
<section className="grid-two">
<article className="card wide">
<h3>Linksammler</h3>
<div className="link-actions">
<button className="btn" onClick={onImportDlc}>DLC import</button>
<button className="btn accent" onClick={onAddLinks}>Zur Queue hinzufügen</button>
</div>
<textarea
value={linksRaw}
onChange={(event) => setLinksRaw(event.target.value)}
onDragOver={(event) => event.preventDefault()}
onDrop={onDrop}
placeholder="# package: Release-Name\nhttps://...\nhttps://..."
/>
<p className="hint">.dlc einfach auf das Feld ziehen oder per Button importieren.</p>
</article>
</section>
)}
{tab === "downloads" && (
<section className="downloads-view">
{packages.length === 0 && <div className="empty">Noch keine Pakete in der Queue.</div>}
{packages.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
items={pkg.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean)}
onCancel={() => window.rd.cancelPackage(pkg.id)}
/>
))}
</section>
)}
{tab === "settings" && (
<section className="grid-two settings-grid">
<article className="card">
<h3>Debrid Provider</h3>
<label>Real-Debrid API Token</label>
@ -341,40 +375,6 @@ export function App(): ReactElement {
<label><input type="checkbox" checked={settingsDraft.hybridExtract} onChange={(event) => setBool("hybridExtract", event.target.checked)} /> Hybrid-Extract</label>
</article>
<article className="card wide">
<h3>Linksammler</h3>
<div className="link-actions">
<button className="btn" onClick={onImportDlc}>DLC import</button>
<button className="btn accent" onClick={onAddLinks}>Zur Queue hinzufügen</button>
</div>
<textarea
value={linksRaw}
onChange={(event) => setLinksRaw(event.target.value)}
onDragOver={(event) => event.preventDefault()}
onDrop={onDrop}
placeholder="# package: Release-Name\nhttps://...\nhttps://..."
/>
<p className="hint">.dlc einfach auf das Feld ziehen oder per Button importieren.</p>
</article>
</section>
)}
{tab === "downloads" && (
<section className="downloads-view">
{packages.length === 0 && <div className="empty">Noch keine Pakete in der Queue.</div>}
{packages.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
items={pkg.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean)}
onCancel={() => window.rd.cancelPackage(pkg.id)}
/>
))}
</section>
)}
{tab === "settings" && (
<section className="grid-two settings-grid">
<article className="card">
<h3>Queue & Reconnect</h3>
<label>Max. gleichzeitige Downloads</label>