Add Rapidgator link online/offline check when links are added
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
545043e1d6
commit
662c903bf3
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.5.82",
|
"version": "1.5.83",
|
||||||
"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",
|
||||||
|
|||||||
@ -226,7 +226,9 @@ function isRapidgatorLink(link: string): boolean {
|
|||||||
return hostname === "rapidgator.net"
|
return hostname === "rapidgator.net"
|
||||||
|| hostname.endsWith(".rapidgator.net")
|
|| hostname.endsWith(".rapidgator.net")
|
||||||
|| hostname === "rg.to"
|
|| hostname === "rg.to"
|
||||||
|| hostname.endsWith(".rg.to");
|
|| hostname.endsWith(".rg.to")
|
||||||
|
|| hostname === "rapidgator.asia"
|
||||||
|
|| hostname.endsWith(".rapidgator.asia");
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -460,6 +462,89 @@ async function resolveRapidgatorFilename(link: string, signal?: AbortSignal): Pr
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RapidgatorCheckResult {
|
||||||
|
online: boolean;
|
||||||
|
fileName: string;
|
||||||
|
fileSize: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RG_FILE_ID_RE = /\/file\/([a-z0-9]{32}|\d+)/i;
|
||||||
|
const RG_FILE_NOT_FOUND_RE = />\s*404\s*File not found/i;
|
||||||
|
const RG_FILESIZE_RE = /File\s*size:\s*<strong>([^<>"]+)<\/strong>/i;
|
||||||
|
|
||||||
|
export async function checkRapidgatorOnline(
|
||||||
|
link: string,
|
||||||
|
signal?: AbortSignal
|
||||||
|
): Promise<RapidgatorCheckResult | null> {
|
||||||
|
if (!isRapidgatorLink(link)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileIdMatch = link.match(RG_FILE_ID_RE);
|
||||||
|
if (!fileIdMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fileId = fileIdMatch[1];
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= REQUEST_RETRIES + 1; attempt += 1) {
|
||||||
|
try {
|
||||||
|
if (signal?.aborted) throw new Error("aborted:debrid");
|
||||||
|
|
||||||
|
const response = await fetch(link, {
|
||||||
|
method: "GET",
|
||||||
|
redirect: "follow",
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||||
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9,de;q=0.8"
|
||||||
|
},
|
||||||
|
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return { online: false, fileName: "", fileSize: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (shouldRetryStatus(response.status) && attempt <= REQUEST_RETRIES) {
|
||||||
|
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalUrl = response.url || link;
|
||||||
|
if (!finalUrl.includes(fileId)) {
|
||||||
|
return { online: false, fileName: "", fileSize: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await readResponseTextLimited(response, RAPIDGATOR_SCAN_MAX_BYTES, signal);
|
||||||
|
|
||||||
|
if (RG_FILE_NOT_FOUND_RE.test(html)) {
|
||||||
|
return { online: false, fileName: "", fileSize: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = extractRapidgatorFilenameFromHtml(html) || filenameFromRapidgatorUrlPath(link);
|
||||||
|
const sizeMatch = html.match(RG_FILESIZE_RE);
|
||||||
|
const fileSize = sizeMatch ? sizeMatch[1].trim() : null;
|
||||||
|
|
||||||
|
return { online: true, fileName, fileSize };
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = compactErrorText(error);
|
||||||
|
if (/aborted/i.test(errorText)) throw error;
|
||||||
|
if (attempt > REQUEST_RETRIES || !isRetryableErrorText(errorText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt <= REQUEST_RETRIES) {
|
||||||
|
await sleepWithSignal(retryDelay(attempt), signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function buildBestDebridRequests(link: string, token: string): BestDebridRequest[] {
|
function buildBestDebridRequests(link: string, token: string): BestDebridRequest[] {
|
||||||
const linkParam = encodeURIComponent(link);
|
const linkParam = encodeURIComponent(link);
|
||||||
const safeToken = String(token || "").trim();
|
const safeToken = String(token || "").trim();
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
} from "../shared/types";
|
} from "../shared/types";
|
||||||
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS } from "./constants";
|
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS } from "./constants";
|
||||||
import { cleanupCancelledPackageArtifactsAsync } from "./cleanup";
|
import { cleanupCancelledPackageArtifactsAsync } from "./cleanup";
|
||||||
import { DebridService, MegaWebUnrestrictor } from "./debrid";
|
import { DebridService, MegaWebUnrestrictor, checkRapidgatorOnline } from "./debrid";
|
||||||
import { collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor";
|
import { collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor";
|
||||||
import { validateFileAgainstManifest } from "./integrity";
|
import { validateFileAgainstManifest } from "./integrity";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
@ -1190,6 +1190,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
let addedPackages = 0;
|
let addedPackages = 0;
|
||||||
let addedLinks = 0;
|
let addedLinks = 0;
|
||||||
const unresolvedByLink = new Map<string, string[]>();
|
const unresolvedByLink = new Map<string, string[]>();
|
||||||
|
const newItemIds: 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) {
|
||||||
@ -1250,6 +1251,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
existing.push(itemId);
|
existing.push(itemId);
|
||||||
unresolvedByLink.set(link, existing);
|
unresolvedByLink.set(link, existing);
|
||||||
}
|
}
|
||||||
|
newItemIds.push(itemId);
|
||||||
addedLinks += 1;
|
addedLinks += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1263,6 +1265,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (unresolvedByLink.size > 0) {
|
if (unresolvedByLink.size > 0) {
|
||||||
void this.resolveQueuedFilenames(unresolvedByLink).catch((err) => logger.warn(`resolveQueuedFilenames Fehler (addPackages): ${compactErrorText(err)}`));
|
void this.resolveQueuedFilenames(unresolvedByLink).catch((err) => logger.warn(`resolveQueuedFilenames Fehler (addPackages): ${compactErrorText(err)}`));
|
||||||
}
|
}
|
||||||
|
if (newItemIds.length > 0) {
|
||||||
|
void this.checkRapidgatorLinks(newItemIds).catch((err) => logger.warn(`checkRapidgatorLinks Fehler: ${compactErrorText(err)}`));
|
||||||
|
}
|
||||||
return { addedPackages, addedLinks };
|
return { addedPackages, addedLinks };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1515,6 +1520,62 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkRapidgatorLinks(itemIds: string[]): Promise<void> {
|
||||||
|
const checked = new Map<string, Awaited<ReturnType<typeof checkRapidgatorOnline>>>();
|
||||||
|
const itemsToCheck: Array<{ itemId: string; url: string }> = [];
|
||||||
|
|
||||||
|
for (const itemId of itemIds) {
|
||||||
|
const item = this.session.items[itemId];
|
||||||
|
if (!item || item.status !== "queued") continue;
|
||||||
|
itemsToCheck.push({ itemId, url: item.url });
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueUrls = [...new Set(itemsToCheck.map(i => i.url))];
|
||||||
|
const concurrency = 4;
|
||||||
|
const queue = [...uniqueUrls];
|
||||||
|
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const url = queue.shift()!;
|
||||||
|
const result = await checkRapidgatorOnline(url);
|
||||||
|
if (result !== null) checked.set(url, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(workers);
|
||||||
|
|
||||||
|
if (checked.size === 0) return;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
for (const { itemId, url } of itemsToCheck) {
|
||||||
|
const result = checked.get(url);
|
||||||
|
if (!result) continue;
|
||||||
|
const item = this.session.items[itemId];
|
||||||
|
if (!item || item.status !== "queued") continue;
|
||||||
|
|
||||||
|
if (!result.online) {
|
||||||
|
item.status = "failed";
|
||||||
|
item.fullStatus = "Offline";
|
||||||
|
item.lastError = "Datei nicht gefunden auf Rapidgator";
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
if (result.fileName && looksLikeOpaqueFilename(item.fileName)) {
|
||||||
|
item.fileName = sanitizeFilename(result.fileName);
|
||||||
|
this.assignItemTargetPath(item, path.join(this.session.packages[item.packageId]?.outputDir || this.settings.outputDir, item.fileName));
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
item.fullStatus = "Online";
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private resolveExistingQueuedOpaqueFilenames(): void {
|
private resolveExistingQueuedOpaqueFilenames(): void {
|
||||||
const unresolvedByLink = new Map<string, string[]>();
|
const unresolvedByLink = new Map<string, string[]>();
|
||||||
for (const item of Object.values(this.session.items)) {
|
for (const item of Object.values(this.session.items)) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user