Compare commits

...

3 Commits

Author SHA1 Message Date
Sucukdeluxe
9dd5d4eef8 Release v1.6.93 2026-03-06 23:06:35 +01:00
Sucukdeluxe
905b55e7d8 fix: provider fallback on cooldown, hoster routing dirty flag, provider order in DM 2026-03-06 23:06:05 +01:00
Sucukdeluxe
0c9bbb0153 fix: release script idempotent recovery when tag already exists 2026-03-06 22:56:30 +01:00
4 changed files with 81 additions and 28 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.92", "version": "1.6.93",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -260,11 +260,20 @@ async function createOrGetRelease(baseApi, tag, authHeader, notes) {
prerelease: false prerelease: false
}; };
const created = await apiRequest("POST", `${baseApi}/releases`, authHeader, JSON.stringify(payload)); const created = await apiRequest("POST", `${baseApi}/releases`, authHeader, JSON.stringify(payload));
if (!created.ok) { if (created.ok) {
throw new Error(`Failed to create release (${created.status}): ${JSON.stringify(created.body)}`);
}
return created.body; return created.body;
} }
// Gitea can return 409/422/500 UNIQUE when the release was already partially created.
// Retry the GET — it may now exist.
if (created.status === 409 || created.status === 422 || created.status === 500) {
const retry = await apiRequest("GET", `${baseApi}/releases/tags/${encodeURIComponent(tag)}`, authHeader);
if (retry.ok) {
process.stdout.write(`Release already exists, using existing release.\n`);
return retry.body;
}
}
throw new Error(`Failed to create release (${created.status}): ${JSON.stringify(created.body)}`);
}
async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) { async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) {
for (const fileName of files) { for (const fileName of files) {
@ -322,8 +331,12 @@ async function main() {
const releaseNotes = args.notes || `- Release ${tag}`; const releaseNotes = args.notes || `- Release ${tag}`;
const repo = getGiteaRepo(); const repo = getGiteaRepo();
const tagExists = spawnSync("git", ["rev-parse", "--verify", `refs/tags/${tag}`], { cwd: process.cwd(), stdio: "ignore" }).status === 0;
if (tagExists) {
process.stdout.write(`Tag ${tag} already exists locally — skipping version bump and git operations (recovery mode).\n`);
} else {
ensureNoTrackedChanges(); ensureNoTrackedChanges();
ensureTagMissing(tag);
if (args.dryRun) { if (args.dryRun) {
process.stdout.write(`Dry run: would release ${tag}. No changes made.\n`); process.stdout.write(`Dry run: would release ${tag}. No changes made.\n`);
@ -331,16 +344,19 @@ async function main() {
} }
updatePackageVersion(rootDir, version); updatePackageVersion(rootDir, version);
}
process.stdout.write(`Building release artifacts for ${tag}...\n`); process.stdout.write(`Building release artifacts for ${tag}...\n`);
run(NPM_RELEASE_WIN.command, NPM_RELEASE_WIN.args); run(NPM_RELEASE_WIN.command, NPM_RELEASE_WIN.args);
const assets = ensureAssetsExist(rootDir, version); const assets = ensureAssetsExist(rootDir, version);
if (!tagExists) {
run("git", ["add", "package.json"]); run("git", ["add", "package.json"]);
run("git", ["commit", "-m", `Release ${tag}`]); run("git", ["commit", "-m", `Release ${tag}`]);
run("git", ["push", repo.remote, "main"]); run("git", ["push", repo.remote, "main"]);
run("git", ["tag", tag]); run("git", ["tag", tag]);
run("git", ["push", repo.remote, tag]); run("git", ["push", repo.remote, tag]);
}
const authHeader = getAuthHeader(repo.host); const authHeader = getAuthHeader(repo.host);
const baseApi = `${repo.baseUrl}/api/v1/repos/${repo.owner}/${repo.repo}`; const baseApi = `${repo.baseUrl}/api/v1/repos/${repo.owner}/${repo.repo}`;

View File

@ -4412,6 +4412,28 @@ export class DownloadManager extends EventEmitter {
return false; return false;
} }
private getProviderOrder(): DebridProvider[] {
if (this.settings.providerOrder && this.settings.providerOrder.length > 0) {
return this.settings.providerOrder;
}
return [
this.settings.providerPrimary,
this.settings.providerSecondary !== "none" ? this.settings.providerSecondary : null,
this.settings.providerTertiary !== "none" ? this.settings.providerTertiary : null
].filter(Boolean) as DebridProvider[];
}
/** Returns the first configured provider from the order that is NOT in cooldown. */
private findFallbackProviderNotInCooldown(item: DownloadItem): DebridProvider | null {
const hosterKey = extractHosterKey(item.url);
for (const provider of this.getProviderOrder()) {
if (!this.isProviderConfigured(provider)) continue;
const key = hosterKey && provider === "alldebrid" ? `${provider}:${hosterKey}` : provider;
if (this.getProviderCooldownRemaining(key) === 0) return provider;
}
return null;
}
private getExpectedProviderForItem(item: DownloadItem): DebridProvider | null { private getExpectedProviderForItem(item: DownloadItem): DebridProvider | null {
if (item.provider) { if (item.provider) {
return resolveMegaDebridProvider(this.settings, item.provider); return resolveMegaDebridProvider(this.settings, item.provider);
@ -4424,14 +4446,8 @@ export class DownloadManager extends EventEmitter {
return routedProvider; return routedProvider;
} }
const order = [
this.settings.providerPrimary,
this.settings.providerSecondary !== "none" ? this.settings.providerSecondary : null,
this.settings.providerTertiary !== "none" ? this.settings.providerTertiary : null
].filter(Boolean) as DebridProvider[];
const seen = new Set<DebridProvider>(); const seen = new Set<DebridProvider>();
for (const provider of order) { for (const provider of this.getProviderOrder()) {
if (seen.has(provider)) { if (seen.has(provider)) {
continue; continue;
} }
@ -5122,12 +5138,29 @@ export class DownloadManager extends EventEmitter {
const cooldownProvider = this.getProviderFailureKeyForItem(item); const cooldownProvider = this.getProviderFailureKeyForItem(item);
const cooldownMs = this.getProviderCooldownRemaining(cooldownProvider); const cooldownMs = this.getProviderCooldownRemaining(cooldownProvider);
if (cooldownMs > 0) { if (cooldownMs > 0) {
// If autoProviderFallback is enabled and another provider is ready, switch to it
// instead of waiting out the full cooldown.
if (this.settings.autoProviderFallback) {
const fallback = this.findFallbackProviderNotInCooldown(item);
if (fallback) {
logger.info(`Provider-Cooldown: ${cooldownProvider} noch ${Math.ceil(cooldownMs / 1000)}s, wechsle zu ${fallback} für ${item.fileName || item.url}`);
item.provider = null;
// Continue — debrid.ts will attempt providers in order and reach the fallback
} else {
const delayMs = Math.min(cooldownMs + 1000, 310000); const delayMs = Math.min(cooldownMs + 1000, 310000);
this.queueRetry(item, active, delayMs, `Provider-Cooldown (${Math.ceil(delayMs / 1000)}s)`); this.queueRetry(item, active, delayMs, `Provider-Cooldown (${Math.ceil(delayMs / 1000)}s)`);
this.persistSoon(); this.persistSoon();
this.emitState(); this.emitState();
return; return;
} }
} else {
const delayMs = Math.min(cooldownMs + 1000, 310000);
this.queueRetry(item, active, delayMs, `Provider-Cooldown (${Math.ceil(delayMs / 1000)}s)`);
this.persistSoon();
this.emitState();
return;
}
}
if (await this.maybeApplyAllDebridRapidgatorBackoff(item, active)) { if (await this.maybeApplyAllDebridRapidgatorBackoff(item, active)) {
this.persistSoon(); this.persistSoon();
this.emitState(); this.emitState();

View File

@ -3877,6 +3877,10 @@ export function App(): ReactElement {
const availableHosters = KNOWN_HOSTERS.filter((h) => !usedHosters.has(h.id)); const availableHosters = KNOWN_HOSTERS.filter((h) => !usedHosters.has(h.id));
const setRouting = (newRouting: Record<string, DebridProvider>) => { const setRouting = (newRouting: Record<string, DebridProvider>) => {
settingsDraftRevisionRef.current += 1;
panelDirtyRevisionRef.current += 1;
settingsDirtyRef.current = true;
setSettingsDirty(true);
setSettingsDraft((prev) => ({ ...prev, hosterRouting: newRouting })); setSettingsDraft((prev) => ({ ...prev, hosterRouting: newRouting }));
}; };