Fix Debrid-Link key rotation cascade failure, case-sensitive rename, and sample filter

- notDebrid (host-level) no longer burns all keys: stops rotation immediately
  with 5min cooldown instead of cycling through all 9 keys pointlessly
- Remove double provider-blockade: debrid_link_cooldown no longer stacks
  recordProviderFailure + applyProviderBusyBackoff on top of key cooldowns
- Detect timeout cascades: 2+ consecutive transport failures trigger 3min
  cooldown instead of burning remaining keys
- Case-sensitive rename: files with different casing (e.g. lowercase scene
  names) now get properly renamed instead of being skipped as "already matching"
- Extended sample filter: detect -s.mkv suffix and \Sample\ subdirectories
  in auto-rename (already worked in MKV-move)
- Add key status display with state pills in Debrid-Link key stats popup
- Add parseDebridLinkTerminalFailure for fast-fail on exhausted keys

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-03-26 13:04:42 +01:00
co-authored by Claude Opus 4.6
parent 1d0b2ee8e3
commit 38179881f5
7 changed files with 288 additions and 31 deletions
+83 -2
View File
@@ -1121,6 +1121,76 @@ function formatDebridLinkCountQuota(info: DebridLinkHostLimitInfo | null | undef
return info.note || "Nicht verfügbar";
}
function getDebridLinkKeyStatusDisplay(
key: DebridLinkAccountKeyEntry,
info: DebridLinkHostLimitInfo | null | undefined
): { label: string; tone: "ok" | "warn" | "bad" | "muted"; title: string } {
if (key.disabled) {
return {
label: "Deaktiviert",
tone: "muted",
title: "Key ist manuell deaktiviert."
};
}
if (key.dailyLimitReached) {
return {
label: "Lokales Limit",
tone: "warn",
title: key.dailyLimitBytes > 0
? `Lokales Tageslimit erreicht (${humanSize(key.dailyUsedBytes)} / ${humanSize(key.dailyLimitBytes)}).`
: "Lokales Tageslimit erreicht."
};
}
if (!info) {
return {
label: "Pruefe...",
tone: "muted",
title: "Live-Status wird geladen."
};
}
const title = [info.stateDetail, info.note, info.hostNote]
.filter((value) => Boolean(String(value || "").trim()))
.join("\n");
if (info.state === "ready") {
if (info.hostState === "down") {
return {
label: "Host offline",
tone: "warn",
title: title || "Der Hoster ist laut Debrid-Link aktuell offline."
};
}
return {
label: "Bereit",
tone: "ok",
title: title || "Key ist nutzbar."
};
}
if (info.state === "invalid" || info.state === "error") {
return {
label: info.stateLabel,
tone: "bad",
title: title || info.stateLabel
};
}
if (info.state === "quota" || info.state === "rate_limit" || info.state === "cooldown") {
return {
label: info.stateLabel,
tone: "warn",
title: title || info.stateLabel
};
}
return {
label: info.stateLabel || "Unbekannt",
tone: "muted",
title: title || info.stateLabel || "Unbekannt"
};
}
interface BandwidthChartProps {
items: Record<string, DownloadItem>;
running: boolean;
@@ -5913,7 +5983,13 @@ export function App(): ReactElement {
const totalUsed = entry.debridLinkKeys.reduce((s, k) => s + k.dailyUsedBytes, 0);
const limitedCount = entry.debridLinkKeys.filter((k) => k.dailyLimitReached).length;
const disabledCount = entry.debridLinkKeys.filter((k) => k.disabled).length;
const keyDiagnostics = entry.debridLinkKeys
.map((k) => debridLinkHostLimits[k.id])
.filter((info): info is DebridLinkHostLimitInfo => Boolean(info));
const loadedQuotaCount = entry.debridLinkKeys.filter((k) => Boolean(debridLinkHostLimits[k.id])).length;
const invalidCount = keyDiagnostics.filter((info) => info.state === "invalid").length;
const cooldownCount = keyDiagnostics.filter((info) => info.state === "cooldown" || info.state === "quota" || info.state === "rate_limit").length;
const hostStatusLabel = keyDiagnostics.find((info) => info.hostState !== "unknown")?.hostStateLabel || "";
return (
<div className="modal-backdrop" onClick={() => setKeyStatsPopup(null)}>
<div className="modal-card key-stats-popup" onClick={(e) => e.stopPropagation()}>
@@ -5924,8 +6000,10 @@ export function App(): ReactElement {
{entry.debridLinkKeys.length} Keys &middot; Heute: {humanSize(totalUsed)}
{limitedCount > 0 && <span className="key-stats-warn"> &middot; {limitedCount} am Limit</span>}
{disabledCount > 0 && <span className="key-stats-warn"> &middot; {disabledCount} deaktiviert</span>}
{invalidCount > 0 && <span className="key-stats-warn"> &middot; {invalidCount} invalid</span>}
{cooldownCount > 0 && <span className="key-stats-warn"> &middot; {cooldownCount} im Cooldown</span>}
{debridLinkHostLimitsLoading && <span> &middot; Rapidgator-Quota wird geladen ({loadedQuotaCount}/{entry.debridLinkKeys.length})</span>}
{!debridLinkHostLimitsLoading && !debridLinkHostLimitsError && <span> &middot; Rapidgator API-Quota</span>}
{!debridLinkHostLimitsLoading && !debridLinkHostLimitsError && <span> &middot; Rapidgator {hostStatusLabel || "Status unbekannt"}</span>}
{debridLinkHostLimitsError && <span className="key-stats-warn"> &middot; API-Quota konnte nicht geladen werden</span>}
</p>
</div>
@@ -5937,14 +6015,16 @@ export function App(): ReactElement {
<span className="col-masked">Key</span>
<span className="col-usage">Heute</span>
<span className="col-limit">Lokal</span>
<span className="col-status">Status</span>
<span className="col-traffic">RG Traffic</span>
<span className="col-links">RG Links</span>
<span className="col-action"></span>
</div>
{entry.debridLinkKeys.map((key, ki) => (
<div key={key.id} className={`account-subkey-table-row${key.dailyLimitReached ? " warning" : ""}${key.disabled ? " disabled" : ""}`}>
<div key={key.id} className={`account-subkey-table-row${key.dailyLimitReached || (debridLinkHostLimits[key.id] && debridLinkHostLimits[key.id].state !== "ready") ? " warning" : ""}${key.disabled ? " disabled" : ""}`}>
{(() => {
const hostInfo = debridLinkHostLimits[key.id];
const statusDisplay = getDebridLinkKeyStatusDisplay(key, hostInfo);
return (
<>
<span className="col-key">{ki + 1}</span>
@@ -5961,6 +6041,7 @@ export function App(): ReactElement {
</span>
<span className="col-usage">{humanSize(key.dailyUsedBytes)}</span>
<span className="col-limit">{key.disabled ? "Deaktiviert" : key.dailyLimitBytes > 0 ? humanSize(key.dailyLimitBytes) : "Kein Limit"}</span>
<span className={`col-status status-pill status-pill-${statusDisplay.tone}`} title={statusDisplay.title}>{statusDisplay.label}</span>
<span className="col-traffic" title={hostInfo?.note || ""}>{formatDebridLinkTraffic(hostInfo)}</span>
<span className="col-links" title={hostInfo?.note || ""}>{formatDebridLinkCountQuota(hostInfo)}</span>
<span className="col-action">
+42 -4
View File
@@ -1562,10 +1562,12 @@ body,
.account-subkey-table-head .col-usage,
.account-subkey-table-head .col-limit,
.account-subkey-table-head .col-status,
.account-subkey-table-head .col-traffic,
.account-subkey-table-head .col-links,
.account-subkey-table-row .col-usage,
.account-subkey-table-row .col-limit,
.account-subkey-table-row .col-status,
.account-subkey-table-row .col-traffic,
.account-subkey-table-row .col-links {
text-align: right;
@@ -1615,6 +1617,10 @@ body,
color: var(--muted);
}
.account-subkey-table-row .col-status {
text-align: center;
}
.account-subkey-table-row .col-traffic,
.account-subkey-table-row .col-links {
text-align: center;
@@ -1630,10 +1636,42 @@ body,
gap: 6px;
}
.account-subkey-table-row .col-action .btn {
padding: 1px 6px;
font-size: 10px;
}
.account-subkey-table-row .col-action .btn {
padding: 1px 6px;
font-size: 10px;
}
.status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 88px;
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.02em;
}
.status-pill-ok {
color: #166534;
background: color-mix(in srgb, #22c55e 16%, transparent);
}
.status-pill-warn {
color: #92400e;
background: color-mix(in srgb, #f59e0b 18%, transparent);
}
.status-pill-bad {
color: #991b1b;
background: color-mix(in srgb, #ef4444 14%, transparent);
}
.status-pill-muted {
color: var(--muted);
background: color-mix(in srgb, var(--field) 80%, transparent);
}
.btn-sm {
padding: 3px 8px;