Compare commits

...

6 Commits

Author SHA1 Message Date
Sucukdeluxe
0a7467a8b0 Release v2.0.0-beta.5 2026-03-08 19:39:07 +01:00
Sucukdeluxe
f25e61573d Fix downloads not starting: reset session.running on startup
Root cause: normalizeSessionStatuses() did not reset session.running
to false on startup. If the app was closed/crashed while downloads
were active, session.json retained running=true. On next launch,
start() checked if (this.session.running) return — silently refusing
to start any downloads.

Also improved normalizeSessionStatuses to match the old DM behavior:
- Reset session.running, paused, reconnectUntil, reconnectReason
- Recover cancelled/Gestoppt items back to queued
- Mark extracting/integrity_check items as completed (already downloaded)
- Handle paused and reconnect_wait items
- Cover all transient package statuses

Updated update tests for beta repo name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:38:22 +01:00
Sucukdeluxe
25b7104580 Release v2.0.0-beta.4 2026-03-08 19:19:32 +01:00
Sucukdeluxe
e0eab43763 Fix auto-updater pointing to stable repo instead of beta repo
DEFAULT_UPDATE_REPO was still set to Administrator/real-debrid-downloader
(the stable repo). Changed to Administrator/beta-real-debrid-downloader
so the beta app finds its own releases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:19:01 +01:00
Sucukdeluxe
4df0d40ece Release v2.0.0-beta.3 2026-03-08 19:16:38 +01:00
Sucukdeluxe
3567cc173c Fix cancelPackage not removing packages from session
cancelPackage only marked packages/items as cancelled but never removed
them from session.packages, session.items, or session.packageOrder.
The old download-manager called removePackageFromSession() which actually
deletes the entries. Now cancelPackage properly removes all items and the
package from the session, cleans up related state, and runs artifact
cleanup in the background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:16:04 +01:00
4 changed files with 61 additions and 16 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader-beta", "name": "real-debrid-downloader-beta",
"version": "2.0.0-beta.2", "version": "2.0.0-beta.5",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -36,7 +36,7 @@ export const MAX_LINK_ARTIFACT_BYTES = 256 * 1024;
export const SPEED_WINDOW_SECONDS = 1; export const SPEED_WINDOW_SECONDS = 1;
export const CLIPBOARD_POLL_INTERVAL_MS = 2000; export const CLIPBOARD_POLL_INTERVAL_MS = 2000;
export const DEFAULT_UPDATE_REPO = "Administrator/real-debrid-downloader"; export const DEFAULT_UPDATE_REPO = "Administrator/beta-real-debrid-downloader";
export function defaultSettings(): AppSettings { export function defaultSettings(): AppSettings {
const baseDir = path.join(os.homedir(), "Downloads", "RealDebrid"); const baseDir = path.join(os.homedir(), "Downloads", "RealDebrid");

View File

@ -523,23 +523,37 @@ export class DownloadManager extends EventEmitter {
pkg.cancelled = true; pkg.cancelled = true;
pkg.status = "cancelled"; pkg.status = "cancelled";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
for (const itemId of pkg.itemIds) { const outputDir = pkg.outputDir;
const itemIds = [...pkg.itemIds];
for (const itemId of itemIds) {
const item = this.session.items[itemId]; const item = this.session.items[itemId];
if (!item) continue; if (!item) continue;
const slot = this.activeTasks.get(itemId); const slot = this.activeTasks.get(itemId);
if (slot) { slot.abortReason = "cancel"; slot.abortController.abort("cancel"); } if (slot) { slot.abortReason = "cancel"; slot.abortController.abort("cancel"); }
if (!isFinishedStatus(item.status)) { if (item.status !== "completed") {
item.status = "cancelled"; this.runOutcomes.set(itemId, "cancelled");
item.fullStatus = "Abgebrochen";
item.speedBps = 0;
item.updatedAt = nowMs();
} }
this.releaseTargetPath(itemId); this.releaseTargetPath(itemId);
this.retryManager.removeItem(itemId); this.retryManager.removeItem(itemId);
this.cachedDirectUrls.delete(itemId);
this.itemContributedBytes.delete(itemId);
delete this.session.items[itemId];
this.itemCount = Math.max(0, this.itemCount - 1);
} }
this.postProcessor.abortPackage(packageId); this.postProcessor.abortPackage(packageId);
this.historyRecordedPackages.delete(packageId);
this.hybridExtractRequeue.delete(packageId);
this.packagePostProcessTasks.delete(packageId);
delete this.session.packages[packageId];
this.session.packageOrder = this.session.packageOrder.filter(id => id !== packageId);
this.persistSoon(); this.persistSoon();
this.emitState(); this.emitState();
// Cleanup artifacts in background
void cleanupCancelledPackageArtifactsAsync(outputDir).catch(() => {});
} }
public resetPackage(packageId: string): void { public resetPackage(packageId: string): void {
@ -1580,16 +1594,47 @@ export class DownloadManager extends EventEmitter {
} }
private normalizeSessionStatuses(): void { private normalizeSessionStatuses(): void {
// Critical: reset session-level running state on startup.
// Without this, if the app crashes while running, session.running stays true
// in the persisted JSON and start() silently returns on next launch.
this.session.running = false;
this.session.paused = false;
this.session.reconnectUntil = 0;
this.session.reconnectReason = "";
for (const item of Object.values(this.session.items)) { for (const item of Object.values(this.session.items)) {
if (item.status === "downloading" || item.status === "validating" || item.status === "integrity_check") { // Items that were stopped mid-run should be re-queued
if (item.status === "cancelled" && item.fullStatus === "Gestoppt") {
item.status = "queued"; item.status = "queued";
item.fullStatus = "Wartet"; item.fullStatus = "Wartet";
item.lastError = "";
item.speedBps = 0;
item.provider = null;
item.updatedAt = nowMs();
continue;
}
// Items that were extracting/checking integrity are already fully downloaded
if (item.status === "extracting" || item.status === "integrity_check") {
item.status = "completed";
item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`;
item.speedBps = 0;
item.updatedAt = nowMs();
continue;
}
// Active/paused/reconnecting items → re-queue
if (item.status === "downloading" || item.status === "validating"
|| item.status === "paused" || item.status === "reconnect_wait") {
item.status = "queued";
const pkg = this.session.packages[item.packageId];
item.fullStatus = (pkg && pkg.enabled === false) ? "Paket gestoppt" : "Wartet";
item.speedBps = 0; item.speedBps = 0;
item.updatedAt = nowMs(); item.updatedAt = nowMs();
} }
} }
for (const pkg of Object.values(this.session.packages)) { for (const pkg of Object.values(this.session.packages)) {
if (pkg.status === "downloading" || pkg.status === "validating" || pkg.status === "extracting") { if (pkg.status === "downloading" || pkg.status === "validating"
|| pkg.status === "extracting" || pkg.status === "integrity_check"
|| pkg.status === "paused" || pkg.status === "reconnect_wait") {
pkg.status = "queued"; pkg.status = "queued";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
} }

View File

@ -46,7 +46,7 @@ afterEach(() => {
describe("update", () => { describe("update", () => {
it("normalizes update repo input", () => { it("normalizes update repo input", () => {
expect(normalizeUpdateRepo("")).toBe("Administrator/real-debrid-downloader"); expect(normalizeUpdateRepo("")).toBe("Administrator/beta-real-debrid-downloader");
expect(normalizeUpdateRepo("owner/repo")).toBe("owner/repo"); expect(normalizeUpdateRepo("owner/repo")).toBe("owner/repo");
expect(normalizeUpdateRepo("https://codeberg.org/owner/repo")).toBe("owner/repo"); expect(normalizeUpdateRepo("https://codeberg.org/owner/repo")).toBe("owner/repo");
expect(normalizeUpdateRepo("https://www.codeberg.org/owner/repo")).toBe("owner/repo"); expect(normalizeUpdateRepo("https://www.codeberg.org/owner/repo")).toBe("owner/repo");
@ -518,14 +518,14 @@ describe("normalizeUpdateRepo extended", () => {
}); });
it("returns default for malformed inputs", () => { it("returns default for malformed inputs", () => {
expect(normalizeUpdateRepo("just-one-part")).toBe("Administrator/real-debrid-downloader"); expect(normalizeUpdateRepo("just-one-part")).toBe("Administrator/beta-real-debrid-downloader");
expect(normalizeUpdateRepo(" ")).toBe("Administrator/real-debrid-downloader"); expect(normalizeUpdateRepo(" ")).toBe("Administrator/beta-real-debrid-downloader");
}); });
it("rejects traversal-like owner or repo segments", () => { it("rejects traversal-like owner or repo segments", () => {
expect(normalizeUpdateRepo("../owner/repo")).toBe("Administrator/real-debrid-downloader"); expect(normalizeUpdateRepo("../owner/repo")).toBe("Administrator/beta-real-debrid-downloader");
expect(normalizeUpdateRepo("owner/../repo")).toBe("Administrator/real-debrid-downloader"); expect(normalizeUpdateRepo("owner/../repo")).toBe("Administrator/beta-real-debrid-downloader");
expect(normalizeUpdateRepo("https://codeberg.org/owner/../../repo")).toBe("Administrator/real-debrid-downloader"); expect(normalizeUpdateRepo("https://codeberg.org/owner/../../repo")).toBe("Administrator/beta-real-debrid-downloader");
}); });
it("handles www prefix", () => { it("handles www prefix", () => {