Change: use status field as primary queue detail (#6859)
Docker CI / Docker Build & Push (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled

This commit is contained in:
shamoon
2026-07-10 22:48:22 -07:00
committed by GitHub
parent 4755388112
commit 9b1726be9c
8 changed files with 174 additions and 47 deletions
+26 -14
View File
@@ -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 <Container service={service} error={finalError} />;
@@ -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}`}
/>
))}
+39 -4
View File
@@ -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 }) => <div data-testid="queue-entry">{title}</div>,
default: ({ title, activity, progress }) => (
<div data-testid="queue-entry" data-activity={activity} data-progress={progress}>
{title}
</div>
),
}));
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"],
]);
});
});
+4 -4
View File
@@ -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;
}
+26 -1
View File
@@ -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]);
});
});
+26 -14
View File
@@ -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 <Container service={service} error={finalError} />;
@@ -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}`}
/>
))}
+23 -5
View File
@@ -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 }) => <div data-testid="queue-entry">{title}</div>,
default: ({ title, activity, progress }) => (
<div data-testid="queue-entry" data-activity={activity} data-progress={progress}>
{title}
</div>
),
}));
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"],
]);
});
});
+4 -4
View File
@@ -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;
}
+26 -1
View File
@@ -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]);
});
});