feat: add Deepbrid, daily scheduling, and notification center
Add encrypted Deepbrid API accounts with account validation, provider routing, fallback, usage tracking, safe error handling, and verified 1Fichier downloads. Restore persistent recurring daily starts with local-calendar deduplication and legacy schedule compatibility. Add durable Discord package, run, remaining-volume, stall, and recovery notifications with privacy-safe telemetry and disk-failure recovery.
This commit is contained in:
+118
-7
@@ -133,10 +133,19 @@ describe("history model", () => {
|
||||
|
||||
for (const [filter, ids] of Object.entries(expected) as Array<[HistoryFilter, string[]]>) {
|
||||
expect(filterHistoryRows(entries, filter, "", now).map((row) => row.id)).toEqual(ids);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses local calendar midnights for the six previous days across both daylight-saving transitions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("labels partial and cancelled package results", () => {
|
||||
const rows = filterHistoryRows([
|
||||
entry({ id: "partial", name: "Teilweise", status: "partial" }),
|
||||
entry({ id: "cancelled", name: "Abgebrochen", status: "cancelled" })
|
||||
], "all", "", now);
|
||||
|
||||
expect(rows.map((row) => row.statusLabel)).toEqual(["Teilweise", "Abgebrochen"]);
|
||||
});
|
||||
|
||||
it("uses local calendar midnights for the six previous days across both daylight-saving transitions", () => {
|
||||
const springNow = new Date(2026, 2, 30, 12, 0, 0, 0).getTime();
|
||||
const springBoundary = new Date(2026, 2, 24, 0, 0, 0, 0).getTime();
|
||||
const autumnNow = new Date(2026, 9, 26, 12, 0, 0, 0).getTime();
|
||||
@@ -176,7 +185,7 @@ describe("history model", () => {
|
||||
expect(filterHistoryRows(searchable, "all", "d", now).map((row) => row.id)).toEqual(["new", "old"]);
|
||||
});
|
||||
|
||||
it("derives hosters only from valid URL hostnames and clamps the calculated start time", () => {
|
||||
it("derives hosters only from valid URL hostnames and clamps the calculated start time", () => {
|
||||
expect(deriveHistoryHoster(["https://rapidgator.net/a", "https://rapidgator.net/b", "https://ddownload.com/c", "not a url"])).toBe("rapidgator.net, ddownload.com");
|
||||
expect(deriveHistoryHoster([])).toBe("—");
|
||||
expect(deriveHistoryHoster(undefined)).toBe("—");
|
||||
@@ -185,8 +194,22 @@ describe("history model", () => {
|
||||
|
||||
const row = filterHistoryRows([entry({ id: "provider", name: "Provider", provider: "realdebrid", urls: [] })], "all", "", now)[0];
|
||||
expect(row.hoster).toBe("—");
|
||||
expect(row.providerLabel).toBe("Real-Debrid");
|
||||
});
|
||||
expect(row.providerLabel).toBe("Real-Debrid");
|
||||
});
|
||||
|
||||
it("uses the authoritative lifecycle start instead of reconstructing it from completion", () => {
|
||||
const lifecycleStartedAt = todayStart - 45_000;
|
||||
const lifecycle = entry({
|
||||
id: "lifecycle-start",
|
||||
name: "Lifecycle",
|
||||
startedAt: lifecycleStartedAt,
|
||||
completedAt: todayStart + 60_000,
|
||||
durationSeconds: 5
|
||||
});
|
||||
|
||||
expect(deriveHistoryStartAt(lifecycle)).toBe(lifecycleStartedAt);
|
||||
expect(filterHistoryRows([lifecycle], "all", "", now)[0].startAt).toBe(lifecycleStartedAt);
|
||||
});
|
||||
|
||||
it("prunes removed ids and preserves the original set instance when every id survives", () => {
|
||||
const stable = new Set(["today", "week"]);
|
||||
@@ -588,6 +611,94 @@ describe("HistoryView", () => {
|
||||
expect(html).toContain("https://rapidgator.net/file/test");
|
||||
});
|
||||
|
||||
it("renders authoritative lifecycle timings, counts, failure phase and operation details", () => {
|
||||
const structured = entry({
|
||||
id: "structured",
|
||||
name: "Strukturiert",
|
||||
status: "partial",
|
||||
startedAt: todayStart,
|
||||
downloadEndedAt: todayStart + 60_000,
|
||||
postProcessStartedAt: todayStart + 65_000,
|
||||
completedAt: todayStart + 90_000,
|
||||
downloadDurationSeconds: 60,
|
||||
extractionDurationSeconds: 12,
|
||||
remuxDurationSeconds: 8,
|
||||
postProcessDurationSeconds: 25,
|
||||
totalDurationSeconds: 90,
|
||||
successfulFiles: 3,
|
||||
failedFiles: 1,
|
||||
cancelledFiles: 0,
|
||||
archiveCount: 1,
|
||||
partCount: 16,
|
||||
outputCount: 10,
|
||||
failurePhase: "remux",
|
||||
archiveOperations: [{
|
||||
id: "archive-1",
|
||||
name: "show.part01.rar",
|
||||
itemIds: ["item-1"],
|
||||
partCount: 16,
|
||||
startedAt: todayStart + 65_000,
|
||||
completedAt: todayStart + 77_000,
|
||||
durationMs: 12_000,
|
||||
status: "completed",
|
||||
errorCategory: ""
|
||||
}],
|
||||
remuxOperations: [{
|
||||
id: "remux-1",
|
||||
fileName: "episode.mkv",
|
||||
startedAt: todayStart + 77_000,
|
||||
completedAt: todayStart + 85_000,
|
||||
durationMs: 8_000,
|
||||
status: "failed",
|
||||
errorCategory: "ffmpeg"
|
||||
}]
|
||||
});
|
||||
const html = renderToStaticMarkup(
|
||||
<HistoryView
|
||||
actions={createActions()}
|
||||
model={buildHistoryViewModel([structured], "all", "", [], [structured.id], false, "", now)}
|
||||
/>
|
||||
);
|
||||
|
||||
for (const label of [
|
||||
"Download gestartet",
|
||||
"Download beendet",
|
||||
"Nachbearbeitung gestartet",
|
||||
"Abgeschlossen",
|
||||
"Downloaddauer",
|
||||
"Entpackdauer",
|
||||
"Remuxdauer",
|
||||
"Nachbearbeitungsdauer",
|
||||
"Gesamtdauer",
|
||||
"Erfolgreich / Fehlgeschlagen / Abgebrochen",
|
||||
"Archive / Parts / Ausgaben",
|
||||
"Fehlerphase",
|
||||
"Archivvorgänge",
|
||||
"Remuxvorgänge"
|
||||
]) {
|
||||
expect(html).toContain(label);
|
||||
}
|
||||
expect(html).toContain("show.part01.rar");
|
||||
expect(html).toContain("16 Parts");
|
||||
expect(html).toContain("episode.mkv");
|
||||
expect(html).toContain("ffmpeg");
|
||||
expect(html).not.toContain("Downloaddauer (Altbestand)");
|
||||
});
|
||||
|
||||
it("labels durationSeconds honestly for legacy entries", () => {
|
||||
const legacy = entry({ id: "legacy", name: "Altbestand" });
|
||||
const html = renderToStaticMarkup(
|
||||
<HistoryView
|
||||
actions={createActions()}
|
||||
model={buildHistoryViewModel([legacy], "all", "", [], [legacy.id], false, "", now)}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("Downloaddauer (Altbestand)");
|
||||
expect(html).not.toContain("Download beendet");
|
||||
expect(html).not.toContain("Nachbearbeitung gestartet");
|
||||
});
|
||||
|
||||
it("uses the global animation setting for the history disclosure surface", () => {
|
||||
const animated = buildHistoryViewModel(entries.slice(0, 1), "all", "", [], ["today"], false, "", now, true);
|
||||
const immediate = buildHistoryViewModel(entries.slice(0, 1), "all", "", [], ["today"], false, "", now, false);
|
||||
|
||||
Reference in New Issue
Block a user