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",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.1.15",
|
"version": "1.1.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.1.15",
|
"version": "1.1.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.1.15",
|
"version": "1.1.16",
|
||||||
"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",
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import os from "node:os";
|
|||||||
import { AppSettings } from "../shared/types";
|
import { AppSettings } from "../shared/types";
|
||||||
|
|
||||||
export const APP_NAME = "Debrid Download Manager";
|
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 API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
||||||
|
|
||||||
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
||||||
|
|||||||
@ -24,6 +24,16 @@ type BestDebridRequest = {
|
|||||||
useAuthHeader: boolean;
|
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 {
|
function shouldRetryStatus(status: number): boolean {
|
||||||
return status === 429 || status >= 500;
|
return status === 429 || status >= 500;
|
||||||
}
|
}
|
||||||
@ -262,6 +272,79 @@ class AllDebridClient {
|
|||||||
this.token = token;
|
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> {
|
public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
|
||||||
let lastError = "";
|
let lastError = "";
|
||||||
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
|
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
|
||||||
@ -337,6 +420,31 @@ export class DebridService {
|
|||||||
this.allDebridClient = new AllDebridClient(next.allDebridToken);
|
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> {
|
public async unrestrictLink(link: string): Promise<ProviderUnrestrictedLink> {
|
||||||
const order = uniqueProviderOrder([
|
const order = uniqueProviderOrder([
|
||||||
this.settings.providerPrimary,
|
this.settings.providerPrimary,
|
||||||
|
|||||||
@ -165,6 +165,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
public addPackages(packages: ParsedPackageInput[]): { addedPackages: number; addedLinks: number } {
|
public addPackages(packages: ParsedPackageInput[]): { addedPackages: number; addedLinks: number } {
|
||||||
let addedPackages = 0;
|
let addedPackages = 0;
|
||||||
let addedLinks = 0;
|
let addedLinks = 0;
|
||||||
|
const unresolvedByLink = new Map<string, string[]>();
|
||||||
for (const pkg of packages) {
|
for (const pkg of packages) {
|
||||||
const links = pkg.links.filter((link) => !!link.trim());
|
const links = pkg.links.filter((link) => !!link.trim());
|
||||||
if (links.length === 0) {
|
if (links.length === 0) {
|
||||||
@ -211,6 +212,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
};
|
};
|
||||||
packageEntry.itemIds.push(itemId);
|
packageEntry.itemIds.push(itemId);
|
||||||
this.session.items[itemId] = item;
|
this.session.items[itemId] = item;
|
||||||
|
if (fileName === "download.bin") {
|
||||||
|
const existing = unresolvedByLink.get(link) ?? [];
|
||||||
|
existing.push(itemId);
|
||||||
|
unresolvedByLink.set(link, existing);
|
||||||
|
}
|
||||||
addedLinks += 1;
|
addedLinks += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,9 +227,54 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
|
if (unresolvedByLink.size > 0) {
|
||||||
|
void this.resolveQueuedFilenames(unresolvedByLink);
|
||||||
|
}
|
||||||
return { addedPackages, addedLinks };
|
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 {
|
public cancelPackage(packageId: string): void {
|
||||||
const pkg = this.session.packages[packageId];
|
const pkg = this.session.packages[packageId];
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
|
|||||||
@ -235,6 +235,40 @@ export function App(): ReactElement {
|
|||||||
<main className="tab-content">
|
<main className="tab-content">
|
||||||
{tab === "collector" && (
|
{tab === "collector" && (
|
||||||
<section className="grid-two">
|
<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">
|
<article className="card">
|
||||||
<h3>Debrid Provider</h3>
|
<h3>Debrid Provider</h3>
|
||||||
<label>Real-Debrid API Token</label>
|
<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>
|
<label><input type="checkbox" checked={settingsDraft.hybridExtract} onChange={(event) => setBool("hybridExtract", event.target.checked)} /> Hybrid-Extract</label>
|
||||||
</article>
|
</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">
|
<article className="card">
|
||||||
<h3>Queue & Reconnect</h3>
|
<h3>Queue & Reconnect</h3>
|
||||||
<label>Max. gleichzeitige Downloads</label>
|
<label>Max. gleichzeitige Downloads</label>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user