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);
}
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]);
});
});