Move provider settings to tab and improve DLC filename resolution
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
02370a40b4
commit
3ef2ee732a
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user