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",
|
||||
"version": "1.4.89",
|
||||
"version": "1.4.90",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -505,6 +505,10 @@ class MegaDebridClient {
|
||||
if (!lastError) {
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
|
||||
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 {
|
||||
const text = String(errorText || "").toLowerCase();
|
||||
return text.includes("unrestrict") || text.includes("mega-web") || text.includes("mega-debrid")
|
||||
@ -3780,6 +3794,21 @@ export class DownloadManager extends EventEmitter {
|
||||
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) {
|
||||
active.unrestrictRetries += 1;
|
||||
item.retries += 1;
|
||||
@ -5252,6 +5281,17 @@ export class DownloadManager extends EventEmitter {
|
||||
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]);
|
||||
}
|
||||
|
||||
@ -5292,6 +5332,16 @@ export class DownloadManager extends EventEmitter {
|
||||
return item != null && item.status !== "completed";
|
||||
});
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,45 @@ function parseSetCookieFromHeaders(headers: Headers): string {
|
||||
.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[] {
|
||||
const entries: CodeEntry[] = [];
|
||||
const cardRegex = /<div[^>]*class=['"][^'"]*acp-box[^'"]*['"][^>]*>[\s\S]*?<\/div>/gi;
|
||||
@ -306,6 +345,14 @@ export class MegaWebFallback {
|
||||
});
|
||||
|
||||
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);
|
||||
if (!code) {
|
||||
return null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user