Detect dead links as permanent errors, fix last-episode extraction race
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
Dead link detection: - Mega-Web: parse hoster error messages (hosterNotAvailable, etc.) from HTML and throw specific error instead of returning null - MegaDebridClient: stop retrying on permanent hoster errors - download-manager: isPermanentLinkError() immediately fails items with dead links instead of retrying forever Extraction race condition: - package_done cleanup policy checked if all items were "completed" (downloaded) but not if they were "extracted" — removing the package before the last episode could be extracted - Both applyCompletedCleanupPolicy and applyPackageDoneCleanup now guard against premature removal when autoExtract is enabled Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3ed31b7994
commit
09bc354c18
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.89",
|
"version": "1.4.90",
|
||||||
"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",
|
||||||
|
|||||||
@ -505,6 +505,10 @@ class MegaDebridClient {
|
|||||||
if (!lastError) {
|
if (!lastError) {
|
||||||
lastError = web ? "Mega-Web Antwort ohne Download-Link" : "Mega-Web Antwort leer";
|
lastError = web ? "Mega-Web Antwort ohne Download-Link" : "Mega-Web Antwort leer";
|
||||||
}
|
}
|
||||||
|
// Don't retry permanent hoster errors (dead link, file removed, etc.)
|
||||||
|
if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(lastError)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (attempt < REQUEST_RETRIES) {
|
if (attempt < REQUEST_RETRIES) {
|
||||||
await sleepWithSignal(retryDelay(attempt), signal);
|
await sleepWithSignal(retryDelay(attempt), signal);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -213,6 +213,20 @@ function isFetchFailure(errorText: string): boolean {
|
|||||||
return text.includes("fetch failed") || text.includes("socket hang up") || text.includes("econnreset") || text.includes("network error");
|
return text.includes("fetch failed") || text.includes("socket hang up") || text.includes("econnreset") || text.includes("network error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPermanentLinkError(errorText: string): boolean {
|
||||||
|
const text = String(errorText || "").toLowerCase();
|
||||||
|
return text.includes("permanent ungültig")
|
||||||
|
|| text.includes("hosternotavailable")
|
||||||
|
|| /file.?not.?found/.test(text)
|
||||||
|
|| /file.?unavailable/.test(text)
|
||||||
|
|| /link.?is.?dead/.test(text)
|
||||||
|
|| text.includes("file has been removed")
|
||||||
|
|| text.includes("file has been deleted")
|
||||||
|
|| text.includes("file is no longer available")
|
||||||
|
|| text.includes("file was removed")
|
||||||
|
|| text.includes("file was deleted");
|
||||||
|
}
|
||||||
|
|
||||||
function isUnrestrictFailure(errorText: string): boolean {
|
function isUnrestrictFailure(errorText: string): boolean {
|
||||||
const text = String(errorText || "").toLowerCase();
|
const text = String(errorText || "").toLowerCase();
|
||||||
return text.includes("unrestrict") || text.includes("mega-web") || text.includes("mega-debrid")
|
return text.includes("unrestrict") || text.includes("mega-web") || text.includes("mega-debrid")
|
||||||
@ -3780,6 +3794,21 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permanent link errors (dead link, file removed, hoster unavailable) → fail immediately
|
||||||
|
if (isPermanentLinkError(errorText)) {
|
||||||
|
logger.error(`Link permanent ungültig: item=${item.fileName || item.id}, error=${errorText}, link=${item.url.slice(0, 80)}`);
|
||||||
|
item.status = "failed";
|
||||||
|
this.recordRunOutcome(item.id, "failed");
|
||||||
|
item.lastError = errorText;
|
||||||
|
item.fullStatus = `Link ungültig: ${errorText}`;
|
||||||
|
item.speedBps = 0;
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
this.retryStateByItem.delete(item.id);
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
active.unrestrictRetries += 1;
|
active.unrestrictRetries += 1;
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
@ -5252,6 +5281,17 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With autoExtract: only remove once ALL items are extracted, not just downloaded
|
||||||
|
if (this.settings.autoExtract) {
|
||||||
|
const allExtracted = pkg.itemIds.every((itemId) => {
|
||||||
|
const item = this.session.items[itemId];
|
||||||
|
return !item || isExtractedLabel(item.fullStatus || "");
|
||||||
|
});
|
||||||
|
if (!allExtracted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.removePackageFromSession(packageId, [...pkg.itemIds]);
|
this.removePackageFromSession(packageId, [...pkg.itemIds]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5292,6 +5332,16 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return item != null && item.status !== "completed";
|
return item != null && item.status !== "completed";
|
||||||
});
|
});
|
||||||
if (!hasOpen) {
|
if (!hasOpen) {
|
||||||
|
// With autoExtract: only remove once ALL items are extracted, not just downloaded
|
||||||
|
if (this.settings.autoExtract) {
|
||||||
|
const allExtracted = pkg.itemIds.every((id) => {
|
||||||
|
const item = this.session.items[id];
|
||||||
|
return !item || isExtractedLabel(item.fullStatus || "");
|
||||||
|
});
|
||||||
|
if (!allExtracted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.removePackageFromSession(packageId, [...pkg.itemIds]);
|
this.removePackageFromSession(packageId, [...pkg.itemIds]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,45 @@ function parseSetCookieFromHeaders(headers: Headers): string {
|
|||||||
.join("; ");
|
.join("; ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PERMANENT_HOSTER_ERRORS = [
|
||||||
|
"hosternotavailable",
|
||||||
|
"filenotfound",
|
||||||
|
"file_unavailable",
|
||||||
|
"file not found",
|
||||||
|
"link is dead",
|
||||||
|
"file has been removed",
|
||||||
|
"file has been deleted",
|
||||||
|
"file was deleted",
|
||||||
|
"file was removed",
|
||||||
|
"not available",
|
||||||
|
"file is no longer available"
|
||||||
|
];
|
||||||
|
|
||||||
|
function parsePageErrors(html: string): string[] {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const errorRegex = /class=["'][^"']*\berror\b[^"']*["'][^>]*>([^<]+)</gi;
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = errorRegex.exec(html)) !== null) {
|
||||||
|
const text = m[1].replace(/^Fehler:\s*/i, "").trim();
|
||||||
|
if (text) {
|
||||||
|
errors.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPermanentHosterError(errors: string[]): string | null {
|
||||||
|
for (const err of errors) {
|
||||||
|
const lower = err.toLowerCase();
|
||||||
|
for (const pattern of PERMANENT_HOSTER_ERRORS) {
|
||||||
|
if (lower.includes(pattern)) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function parseCodes(html: string): CodeEntry[] {
|
function parseCodes(html: string): CodeEntry[] {
|
||||||
const entries: CodeEntry[] = [];
|
const entries: CodeEntry[] = [];
|
||||||
const cardRegex = /<div[^>]*class=['"][^'"]*acp-box[^'"]*['"][^>]*>[\s\S]*?<\/div>/gi;
|
const cardRegex = /<div[^>]*class=['"][^'"]*acp-box[^'"]*['"][^>]*>[\s\S]*?<\/div>/gi;
|
||||||
@ -306,6 +345,14 @@ export class MegaWebFallback {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const html = await page.text();
|
const html = await page.text();
|
||||||
|
|
||||||
|
// Check for permanent hoster errors before looking for debrid codes
|
||||||
|
const pageErrors = parsePageErrors(html);
|
||||||
|
const permanentError = isPermanentHosterError(pageErrors);
|
||||||
|
if (permanentError) {
|
||||||
|
throw new Error(`Mega-Web: Link permanent ungültig (${permanentError})`);
|
||||||
|
}
|
||||||
|
|
||||||
const code = pickCode(parseCodes(html), link);
|
const code = pickCode(parseCodes(html), link);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user