mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-29 07:21:16 -07:00
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
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:
@@ -1,14 +1,37 @@
|
|||||||
import Block from "components/services/widget/block";
|
import Block from "components/services/widget/block";
|
||||||
import Container from "components/services/widget/container";
|
import Container from "components/services/widget/container";
|
||||||
import { useTranslation } from "next-i18next/pages";
|
import { useTranslation } from "next-i18next/pages";
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
import QueueEntry from "../../components/widgets/queue/queueEntry";
|
import QueueEntry from "../../components/widgets/queue/queueEntry";
|
||||||
|
|
||||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
function getProgress(sizeLeft, size) {
|
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 }) {
|
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: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status");
|
||||||
const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details");
|
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) {
|
if (moviesError || queuedError || queueDetailsError) {
|
||||||
const finalError = moviesError ?? queuedError ?? queueDetailsError;
|
const finalError = moviesError ?? queuedError ?? queueDetailsError;
|
||||||
return <Container service={service} error={finalError} />;
|
return <Container service={service} error={finalError} />;
|
||||||
@@ -62,7 +74,7 @@ export default function Component({ service }) {
|
|||||||
progress={getProgress(queueEntry.sizeLeft, queueEntry.size)}
|
progress={getProgress(queueEntry.sizeLeft, queueEntry.size)}
|
||||||
timeLeft={queueEntry.timeLeft}
|
timeLeft={queueEntry.timeLeft}
|
||||||
title={moviesData.all.find((entry) => entry.id === queueEntry.movieId)?.title ?? t("radarr.unknown")}
|
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}`}
|
key={`${queueEntry.movieId}-${queueEntry.sizeLeft}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
|||||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||||
|
|
||||||
vi.mock("../../components/widgets/queue/queueEntry", () => ({
|
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";
|
import Component from "./component";
|
||||||
@@ -37,11 +41,37 @@ describe("widgets/radarr/component", () => {
|
|||||||
it("renders counts and queue entries when enabled", () => {
|
it("renders counts and queue entries when enabled", () => {
|
||||||
useWidgetAPI.mockImplementation((_widget, endpoint) => {
|
useWidgetAPI.mockImplementation((_widget, endpoint) => {
|
||||||
if (endpoint === "movie")
|
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/status") return { data: { totalCount: 1 }, error: undefined };
|
||||||
if (endpoint === "queue/details")
|
if (endpoint === "queue/details")
|
||||||
return {
|
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,
|
error: undefined,
|
||||||
};
|
};
|
||||||
return { data: undefined, error: undefined };
|
return { data: undefined, error: undefined };
|
||||||
@@ -54,6 +84,11 @@ describe("widgets/radarr/component", () => {
|
|||||||
expectBlockValue(container, "radarr.missing", 2);
|
expectBlockValue(container, "radarr.missing", 2);
|
||||||
expectBlockValue(container, "radarr.queued", 1);
|
expectBlockValue(container, "radarr.queued", 1);
|
||||||
expectBlockValue(container, "radarr.movies", 3);
|
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"],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ const widget = {
|
|||||||
status: entry.status,
|
status: entry.status,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const downloadingA = a.trackedDownloadState === "downloading";
|
const downloadingA = (a.status ?? a.trackedDownloadState) === "downloading";
|
||||||
const downloadingB = b.trackedDownloadState === "downloading";
|
const downloadingB = (b.status ?? b.trackedDownloadState) === "downloading";
|
||||||
if (downloadingA && !downloadingB) {
|
if (downloadingA && !downloadingB) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -45,8 +45,8 @@ const widget = {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const percentA = a.sizeLeft / a.size;
|
const percentA = a.size > 0 ? a.sizeLeft / a.size : 1;
|
||||||
const percentB = b.sizeLeft / b.size;
|
const percentB = b.size > 0 ? b.sizeLeft / b.size : 1;
|
||||||
if (percentA < percentB) {
|
if (percentA < percentB) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||||
|
|
||||||
@@ -8,4 +8,29 @@ describe("radarr widget config", () => {
|
|||||||
it("exports a valid widget config", () => {
|
it("exports a valid widget config", () => {
|
||||||
expectWidgetConfigShape(widget);
|
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]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,37 @@
|
|||||||
import Block from "components/services/widget/block";
|
import Block from "components/services/widget/block";
|
||||||
import Container from "components/services/widget/container";
|
import Container from "components/services/widget/container";
|
||||||
import { useTranslation } from "next-i18next/pages";
|
import { useTranslation } from "next-i18next/pages";
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
import QueueEntry from "../../components/widgets/queue/queueEntry";
|
import QueueEntry from "../../components/widgets/queue/queueEntry";
|
||||||
|
|
||||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
function getProgress(sizeLeft, size) {
|
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) {
|
function getTitle(queueEntry, seriesData) {
|
||||||
@@ -30,17 +53,6 @@ export default function Component({ service }) {
|
|||||||
const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series");
|
const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series");
|
||||||
const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details");
|
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) {
|
if (wantedError || queuedError || seriesError || queueDetailsError) {
|
||||||
const finalError = wantedError ?? queuedError ?? seriesError ?? queueDetailsError;
|
const finalError = wantedError ?? queuedError ?? seriesError ?? queueDetailsError;
|
||||||
return <Container service={service} error={finalError} />;
|
return <Container service={service} error={finalError} />;
|
||||||
@@ -71,7 +83,7 @@ export default function Component({ service }) {
|
|||||||
progress={getProgress(queueEntry.sizeLeft, queueEntry.size)}
|
progress={getProgress(queueEntry.sizeLeft, queueEntry.size)}
|
||||||
timeLeft={queueEntry.timeLeft}
|
timeLeft={queueEntry.timeLeft}
|
||||||
title={getTitle(queueEntry, seriesData) ?? t("sonarr.unknown")}
|
title={getTitle(queueEntry, seriesData) ?? t("sonarr.unknown")}
|
||||||
activity={formatDownloadState(queueEntry.trackedDownloadState)}
|
activity={getActivity(queueEntry.status, queueEntry.trackedDownloadState)}
|
||||||
key={`${queueEntry.seriesId}-${queueEntry.episodeId}`}
|
key={`${queueEntry.seriesId}-${queueEntry.episodeId}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
|||||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||||
|
|
||||||
vi.mock("../../components/widgets/queue/queueEntry", () => ({
|
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";
|
import Component from "./component";
|
||||||
@@ -44,10 +48,19 @@ describe("widgets/sonarr/component", () => {
|
|||||||
{
|
{
|
||||||
seriesId: 10,
|
seriesId: 10,
|
||||||
episodeId: 1,
|
episodeId: 1,
|
||||||
episodeTitle: "Ep",
|
episodeTitle: "Queued Ep",
|
||||||
sizeLeft: 50,
|
sizeLeft: 0,
|
||||||
|
size: 0,
|
||||||
|
status: "queued",
|
||||||
|
trackedDownloadState: "downloading",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seriesId: 10,
|
||||||
|
episodeId: 2,
|
||||||
|
episodeTitle: "Imported Ep",
|
||||||
|
sizeLeft: 0,
|
||||||
size: 100,
|
size: 100,
|
||||||
timeLeft: "1m",
|
status: "completed",
|
||||||
trackedDownloadState: "importPending",
|
trackedDownloadState: "importPending",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -63,6 +76,11 @@ describe("widgets/sonarr/component", () => {
|
|||||||
expectBlockValue(container, "sonarr.wanted", 1);
|
expectBlockValue(container, "sonarr.wanted", 1);
|
||||||
expectBlockValue(container, "sonarr.queued", 2);
|
expectBlockValue(container, "sonarr.queued", 2);
|
||||||
expectBlockValue(container, "sonarr.series", 1);
|
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"],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ const widget = {
|
|||||||
status: entry.status,
|
status: entry.status,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const downloadingA = a.trackedDownloadState === "downloading";
|
const downloadingA = (a.status ?? a.trackedDownloadState) === "downloading";
|
||||||
const downloadingB = b.trackedDownloadState === "downloading";
|
const downloadingB = (b.status ?? b.trackedDownloadState) === "downloading";
|
||||||
if (downloadingA && !downloadingB) {
|
if (downloadingA && !downloadingB) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -47,8 +47,8 @@ const widget = {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const percentA = a.sizeLeft / a.size;
|
const percentA = a.size > 0 ? a.sizeLeft / a.size : 1;
|
||||||
const percentB = b.sizeLeft / b.size;
|
const percentB = b.size > 0 ? b.sizeLeft / b.size : 1;
|
||||||
if (percentA < percentB) {
|
if (percentA < percentB) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||||
|
|
||||||
@@ -8,4 +8,29 @@ describe("sonarr widget config", () => {
|
|||||||
it("exports a valid widget config", () => {
|
it("exports a valid widget config", () => {
|
||||||
expectWidgetConfigShape(widget);
|
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]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user