diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx index 2df6381da..4f6f12480 100644 --- a/src/widgets/radarr/component.jsx +++ b/src/widgets/radarr/component.jsx @@ -1,14 +1,37 @@ import Block from "components/services/widget/block"; import Container from "components/services/widget/container"; import { useTranslation } from "next-i18next/pages"; -import { useCallback } from "react"; import QueueEntry from "../../components/widgets/queue/queueEntry"; import useWidgetAPI from "utils/proxy/use-widget-api"; function getProgress(sizeLeft, size) { - return sizeLeft === 0 ? 100 : (1 - sizeLeft / size) * 100; + if (!Number.isFinite(size) || size <= 0) return 0; + return Math.min(100, Math.max(0, (1 - sizeLeft / size) * 100)); +} + +function formatDownloadState(downloadState) { + switch (downloadState) { + case "importBlocked": + return "import blocked"; + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } +} + +function getActivity(status, trackedDownloadState) { + const completedStates = ["importBlocked", "importPending", "importing", "failedPending"]; + const downloadState = + status === "completed" && completedStates.includes(trackedDownloadState) + ? trackedDownloadState + : (status ?? trackedDownloadState); + + return formatDownloadState(downloadState); } export default function Component({ service }) { @@ -19,17 +42,6 @@ export default function Component({ service }) { const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status"); const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - const formatDownloadState = useCallback((downloadState) => { - switch (downloadState) { - case "importPending": - return "import pending"; - case "failedPending": - return "failed pending"; - default: - return downloadState; - } - }, []); - if (moviesError || queuedError || queueDetailsError) { const finalError = moviesError ?? queuedError ?? queueDetailsError; return ; @@ -62,7 +74,7 @@ export default function Component({ service }) { progress={getProgress(queueEntry.sizeLeft, queueEntry.size)} timeLeft={queueEntry.timeLeft} title={moviesData.all.find((entry) => entry.id === queueEntry.movieId)?.title ?? t("radarr.unknown")} - activity={formatDownloadState(queueEntry.trackedDownloadState)} + activity={getActivity(queueEntry.status, queueEntry.trackedDownloadState)} key={`${queueEntry.movieId}-${queueEntry.sizeLeft}`} /> ))} diff --git a/src/widgets/radarr/component.test.jsx b/src/widgets/radarr/component.test.jsx index 09a5c34b2..43873207c 100644 --- a/src/widgets/radarr/component.test.jsx +++ b/src/widgets/radarr/component.test.jsx @@ -10,7 +10,11 @@ const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); vi.mock("../../components/widgets/queue/queueEntry", () => ({ - default: ({ title }) =>
{title}
, + default: ({ title, activity, progress }) => ( +
+ {title} +
+ ), })); import Component from "./component"; @@ -37,11 +41,37 @@ describe("widgets/radarr/component", () => { it("renders counts and queue entries when enabled", () => { useWidgetAPI.mockImplementation((_widget, endpoint) => { if (endpoint === "movie") - return { data: { wanted: 1, missing: 2, have: 3, all: [{ id: 10, title: "Movie" }] }, error: undefined }; + return { + data: { + wanted: 1, + missing: 2, + have: 3, + all: [ + { id: 10, title: "Queued Movie" }, + { id: 11, title: "Imported Movie" }, + ], + }, + error: undefined, + }; if (endpoint === "queue/status") return { data: { totalCount: 1 }, error: undefined }; if (endpoint === "queue/details") return { - data: [{ movieId: 10, sizeLeft: 50, size: 100, timeLeft: "1m", trackedDownloadState: "importPending" }], + data: [ + { + movieId: 10, + sizeLeft: 0, + size: 0, + status: "queued", + trackedDownloadState: "downloading", + }, + { + movieId: 11, + sizeLeft: 0, + size: 100, + status: "completed", + trackedDownloadState: "importPending", + }, + ], error: undefined, }; return { data: undefined, error: undefined }; @@ -54,6 +84,11 @@ describe("widgets/radarr/component", () => { expectBlockValue(container, "radarr.missing", 2); expectBlockValue(container, "radarr.queued", 1); expectBlockValue(container, "radarr.movies", 3); - expect(screen.getAllByTestId("queue-entry").map((el) => el.textContent)).toEqual(["Movie"]); + const queueEntries = screen.getAllByTestId("queue-entry"); + expect(queueEntries.map((el) => el.textContent)).toEqual(["Queued Movie", "Imported Movie"]); + expect(queueEntries.map((el) => [el.dataset.activity, el.dataset.progress])).toEqual([ + ["queued", "0"], + ["import pending", "100"], + ]); }); }); diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js index 4f71b8d96..5170325d6 100644 --- a/src/widgets/radarr/widget.js +++ b/src/widgets/radarr/widget.js @@ -36,8 +36,8 @@ const widget = { status: entry.status, })) .sort((a, b) => { - const downloadingA = a.trackedDownloadState === "downloading"; - const downloadingB = b.trackedDownloadState === "downloading"; + const downloadingA = (a.status ?? a.trackedDownloadState) === "downloading"; + const downloadingB = (b.status ?? b.trackedDownloadState) === "downloading"; if (downloadingA && !downloadingB) { return -1; } @@ -45,8 +45,8 @@ const widget = { return 1; } - const percentA = a.sizeLeft / a.size; - const percentB = b.sizeLeft / b.size; + const percentA = a.size > 0 ? a.sizeLeft / a.size : 1; + const percentB = b.size > 0 ? b.sizeLeft / b.size : 1; if (percentA < percentB) { return -1; } diff --git a/src/widgets/radarr/widget.test.js b/src/widgets/radarr/widget.test.js index d111bef27..c7a19f39b 100644 --- a/src/widgets/radarr/widget.test.js +++ b/src/widgets/radarr/widget.test.js @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { expectWidgetConfigShape } from "test-utils/widget-config"; @@ -8,4 +8,29 @@ describe("radarr widget config", () => { it("exports a valid widget config", () => { expectWidgetConfigShape(widget); }); + + it("sorts active downloads ahead of queued downloads", () => { + const queue = widget.mappings["queue/details"].map( + Buffer.from( + JSON.stringify([ + { + movieId: 1, + status: "queued", + trackedDownloadState: "downloading", + size: 0, + sizeleft: 0, + }, + { + movieId: 2, + status: "downloading", + trackedDownloadState: "downloading", + size: 100, + sizeleft: 50, + }, + ]), + ), + ); + + expect(queue.map((entry) => entry.movieId)).toEqual([2, 1]); + }); }); diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx index 61451e89b..fc3f9db7a 100644 --- a/src/widgets/sonarr/component.jsx +++ b/src/widgets/sonarr/component.jsx @@ -1,14 +1,37 @@ import Block from "components/services/widget/block"; import Container from "components/services/widget/container"; import { useTranslation } from "next-i18next/pages"; -import { useCallback } from "react"; import QueueEntry from "../../components/widgets/queue/queueEntry"; import useWidgetAPI from "utils/proxy/use-widget-api"; function getProgress(sizeLeft, size) { - return sizeLeft === 0 ? 100 : (1 - sizeLeft / size) * 100; + if (!Number.isFinite(size) || size <= 0) return 0; + return Math.min(100, Math.max(0, (1 - sizeLeft / size) * 100)); +} + +function formatDownloadState(downloadState) { + switch (downloadState) { + case "importBlocked": + return "import blocked"; + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } +} + +function getActivity(status, trackedDownloadState) { + const completedStates = ["importBlocked", "importPending", "importing", "failedPending"]; + const downloadState = + status === "completed" && completedStates.includes(trackedDownloadState) + ? trackedDownloadState + : (status ?? trackedDownloadState); + + return formatDownloadState(downloadState); } function getTitle(queueEntry, seriesData) { @@ -30,17 +53,6 @@ export default function Component({ service }) { const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series"); const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - const formatDownloadState = useCallback((downloadState) => { - switch (downloadState) { - case "importPending": - return "import pending"; - case "failedPending": - return "failed pending"; - default: - return downloadState; - } - }, []); - if (wantedError || queuedError || seriesError || queueDetailsError) { const finalError = wantedError ?? queuedError ?? seriesError ?? queueDetailsError; return ; @@ -71,7 +83,7 @@ export default function Component({ service }) { progress={getProgress(queueEntry.sizeLeft, queueEntry.size)} timeLeft={queueEntry.timeLeft} title={getTitle(queueEntry, seriesData) ?? t("sonarr.unknown")} - activity={formatDownloadState(queueEntry.trackedDownloadState)} + activity={getActivity(queueEntry.status, queueEntry.trackedDownloadState)} key={`${queueEntry.seriesId}-${queueEntry.episodeId}`} /> ))} diff --git a/src/widgets/sonarr/component.test.jsx b/src/widgets/sonarr/component.test.jsx index 71dd84bb3..d4a56e698 100644 --- a/src/widgets/sonarr/component.test.jsx +++ b/src/widgets/sonarr/component.test.jsx @@ -10,7 +10,11 @@ const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); vi.mock("../../components/widgets/queue/queueEntry", () => ({ - default: ({ title }) =>
{title}
, + default: ({ title, activity, progress }) => ( +
+ {title} +
+ ), })); import Component from "./component"; @@ -44,10 +48,19 @@ describe("widgets/sonarr/component", () => { { seriesId: 10, episodeId: 1, - episodeTitle: "Ep", - sizeLeft: 50, + episodeTitle: "Queued Ep", + sizeLeft: 0, + size: 0, + status: "queued", + trackedDownloadState: "downloading", + }, + { + seriesId: 10, + episodeId: 2, + episodeTitle: "Imported Ep", + sizeLeft: 0, size: 100, - timeLeft: "1m", + status: "completed", trackedDownloadState: "importPending", }, ], @@ -63,6 +76,11 @@ describe("widgets/sonarr/component", () => { expectBlockValue(container, "sonarr.wanted", 1); expectBlockValue(container, "sonarr.queued", 2); expectBlockValue(container, "sonarr.series", 1); - expect(screen.getAllByTestId("queue-entry").map((el) => el.textContent)).toEqual(["Show: Ep"]); + const queueEntries = screen.getAllByTestId("queue-entry"); + expect(queueEntries.map((el) => el.textContent)).toEqual(["Show: Queued Ep", "Show: Imported Ep"]); + expect(queueEntries.map((el) => [el.dataset.activity, el.dataset.progress])).toEqual([ + ["queued", "0"], + ["import pending", "100"], + ]); }); }); diff --git a/src/widgets/sonarr/widget.js b/src/widgets/sonarr/widget.js index 1fcef8eb5..ad12bd567 100644 --- a/src/widgets/sonarr/widget.js +++ b/src/widgets/sonarr/widget.js @@ -38,8 +38,8 @@ const widget = { status: entry.status, })) .sort((a, b) => { - const downloadingA = a.trackedDownloadState === "downloading"; - const downloadingB = b.trackedDownloadState === "downloading"; + const downloadingA = (a.status ?? a.trackedDownloadState) === "downloading"; + const downloadingB = (b.status ?? b.trackedDownloadState) === "downloading"; if (downloadingA && !downloadingB) { return -1; } @@ -47,8 +47,8 @@ const widget = { return 1; } - const percentA = a.sizeLeft / a.size; - const percentB = b.sizeLeft / b.size; + const percentA = a.size > 0 ? a.sizeLeft / a.size : 1; + const percentB = b.size > 0 ? b.sizeLeft / b.size : 1; if (percentA < percentB) { return -1; } diff --git a/src/widgets/sonarr/widget.test.js b/src/widgets/sonarr/widget.test.js index a09895874..458535cfa 100644 --- a/src/widgets/sonarr/widget.test.js +++ b/src/widgets/sonarr/widget.test.js @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { expectWidgetConfigShape } from "test-utils/widget-config"; @@ -8,4 +8,29 @@ describe("sonarr widget config", () => { it("exports a valid widget config", () => { expectWidgetConfigShape(widget); }); + + it("sorts active downloads ahead of queued downloads", () => { + const queue = widget.mappings["queue/details"].map( + Buffer.from( + JSON.stringify([ + { + episodeId: 1, + status: "queued", + trackedDownloadState: "downloading", + size: 0, + sizeleft: 0, + }, + { + episodeId: 2, + status: "downloading", + trackedDownloadState: "downloading", + size: 100, + sizeleft: 50, + }, + ]), + ), + ); + + expect(queue.map((entry) => entry.episodeId)).toEqual([2, 1]); + }); });