Chore: merge Overseerr into Seerr, add aliases (#6330)

This commit is contained in:
shamoon
2026-02-15 19:12:15 -08:00
committed by GitHub
parent 597f6ecf16
commit 09bab7637e
11 changed files with 89 additions and 41 deletions

View File

@@ -101,7 +101,6 @@ You can also find a list of all available service widgets in the sidebar navigat
- [OpenMediaVault](openmediavault.md)
- [OpenWRT](openwrt.md)
- [OPNsense](opnsense.md)
- [Overseerr](overseerr.md)
- [PaperlessNGX](paperlessngx.md)
- [Peanut](peanut.md)
- [pfSense](pfsense.md)

View File

@@ -1,17 +0,0 @@
---
title: Overseerr
description: Overseerr Widget Configuration
---
Learn more about [Overseerr](https://github.com/sct/overseerr).
Find your API key under `Settings > General`.
Allowed fields: `["pending", "approved", "available", "processing"]`.
```yaml
widget:
type: overseerr
url: http://overseerr.host.or.ip
key: apikeyapikeyapikeyapikeyapikey
```

View File

@@ -7,9 +7,9 @@ Learn more about [Seerr](https://github.com/seerr-team/seerr).
Find your API key under `Settings > General > API Key`.
_Note that Jellyseerr was merged with Overseerr and renamed Seerr. Use `type: seerr` (legacy `type: jellyseerr` is aliased)._
_Jellyseerr and Overseerr merged into Seerr. Use `type: seerr` (legacy `type: jellyseerr` and `type: overseerr` are aliased)._
Allowed fields: `["pending", "approved", "available", "completed", "issues"]`.
Allowed fields: `["pending", "approved", "available", "completed", "processing", "issues"]`.
Default fields: `["pending", "approved", "completed"]`.
```yaml

View File

@@ -124,7 +124,6 @@ nav:
- widgets/services/openmediavault.md
- widgets/services/opnsense.md
- widgets/services/openwrt.md
- widgets/services/overseerr.md
- widgets/services/pangolin.md
- widgets/services/paperlessngx.md
- widgets/services/peanut.md

View File

@@ -294,13 +294,8 @@
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"issues": "Open Issues"
},
"overseerr": {
"pending": "Pending",
"processing": "Processing",
"approved": "Approved",
"available": "Available"
"issues": "Open Issues"
},
"netalertx": {
"total": "Total",

View File

@@ -9,6 +9,8 @@ import { buildHighlightConfig } from "utils/highlights";
const ALIASED_WIDGETS = {
pialert: "netalertx",
hoarder: "karakeep",
jellyseerr: "seerr",
overseerr: "seerr",
};
export default function Container({ error = false, children, service }) {

View File

@@ -58,6 +58,26 @@ describe("components/services/widget/container", () => {
expect(screen.getByTestId("karakeep.count")).toBeInTheDocument();
});
it("supports seerr aliases when filtering (jellyseerr/overseerr -> seerr)", () => {
renderWithProviders(
<Container service={{ widget: { type: "jellyseerr", fields: ["pending"] } }}>
<Dummy label="seerr.pending" />
</Container>,
{ settings: {} },
);
expect(screen.getByTestId("seerr.pending")).toBeInTheDocument();
renderWithProviders(
<Container service={{ widget: { type: "overseerr", fields: ["processing"] } }}>
<Dummy label="seerr.processing" />
</Container>,
{ settings: {} },
);
expect(screen.getByTestId("seerr.processing")).toBeInTheDocument();
});
it("returns null when errors are hidden via settings.hideErrors", () => {
const { container } = renderWithProviders(
<Container error="nope" service={{ widget: { type: "omada", hide_errors: false } }}>

View File

@@ -97,7 +97,7 @@ const components = {
ombi: dynamic(() => import("./ombi/component")),
opendtu: dynamic(() => import("./opendtu/component")),
opnsense: dynamic(() => import("./opnsense/component")),
overseerr: dynamic(() => import("./overseerr/component")),
overseerr: dynamic(() => import("./seerr/component")),
openmediavault: dynamic(() => import("./openmediavault/component")),
openwrt: dynamic(() => import("./openwrt/component")),
paperlessngx: dynamic(() => import("./paperlessngx/component")),

View File

@@ -8,7 +8,6 @@ const MAX_ALLOWED_FIELDS = 4;
export default function Component({ service }) {
const { widget } = service;
widget.fields = widget?.fields?.length ? widget.fields.slice(0, MAX_ALLOWED_FIELDS) : seerrDefaultFields;
const isIssueEnabled = widget.fields.includes("issues");
@@ -21,11 +20,12 @@ export default function Component({ service }) {
if (!statsData || (isIssueEnabled && !issueData)) {
return (
<Container service={service}>
<Block label="seerr.pending" />
<Block label="seerr.approved" />
<Block label="seerr.available" />
<Block label="seerr.completed" />
<Block label="seerr.issues" />
<Block field="seerr.pending" label="seerr.pending" />
<Block field="seerr.approved" label="seerr.approved" />
<Block field="seerr.available" label="seerr.available" />
<Block field="seerr.completed" label="seerr.completed" />
<Block field="seerr.processing" label="seerr.processing" />
<Block field="seerr.issues" label="seerr.issues" />
</Container>
);
}
@@ -38,11 +38,12 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="seerr.pending" value={statsData.pending} />
<Block label="seerr.approved" value={statsData.approved} />
<Block label="seerr.available" value={statsData.available} />
<Block label="seerr.completed" value={statsData.completed} />
<Block label="seerr.issues" value={`${issueData?.open} / ${issueData?.total}`} />
<Block field="seerr.pending" label="seerr.pending" value={statsData.pending} />
<Block field="seerr.approved" label="seerr.approved" value={statsData.approved} />
<Block field="seerr.available" label="seerr.available" value={statsData.available} />
<Block field="seerr.completed" label="seerr.completed" value={statsData.completed} />
<Block field="seerr.processing" label="seerr.processing" value={statsData.processing} />
<Block field="seerr.issues" label="seerr.issues" value={`${issueData?.open} / ${issueData?.total}`} />
</Container>
);
}

View File

@@ -30,9 +30,59 @@ describe("widgets/seerr/component", () => {
expect(screen.getByText("seerr.approved")).toBeInTheDocument();
expect(screen.getByText("seerr.completed")).toBeInTheDocument();
expect(screen.queryByText("seerr.available")).toBeNull();
expect(screen.queryByText("seerr.processing")).toBeNull();
expect(screen.queryByText("seerr.issues")).toBeNull();
});
it("supports jellyseerr as a legacy alias to seerr", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined }) // request/count
.mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "")
const service = { widget: { type: "jellyseerr", url: "http://x" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(service.widget.fields).toEqual(seerrDefaultFields);
expect(useWidgetAPI.mock.calls[1][1]).toBe("");
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("seerr.pending")).toBeInTheDocument();
expect(screen.getByText("seerr.approved")).toBeInTheDocument();
expect(screen.getByText("seerr.completed")).toBeInTheDocument();
});
it("supports overseerr as a legacy alias with the same default fields", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined }) // request/count
.mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "")
const service = { widget: { type: "overseerr", url: "http://x" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(service.widget.fields).toEqual(seerrDefaultFields);
expect(useWidgetAPI.mock.calls[1][1]).toBe("");
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("seerr.pending")).toBeInTheDocument();
expect(screen.getByText("seerr.approved")).toBeInTheDocument();
expect(screen.getByText("seerr.completed")).toBeInTheDocument();
});
it("keeps processing as a separate optional field", () => {
useWidgetAPI
.mockReturnValueOnce({ data: { pending: 1, processing: 2, approved: 3, available: 4 }, error: undefined })
.mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "")
const service = {
widget: { type: "overseerr", url: "http://x", fields: ["pending", "processing", "approved", "available"] },
};
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(useWidgetAPI.mock.calls[1][1]).toBe("");
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("seerr.processing")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.queryByText("seerr.completed")).toBeNull();
});
it("renders issues when enabled (and calls the issue/count endpoint)", () => {
useWidgetAPI
.mockReturnValueOnce({ data: { pending: 1, approved: 2, available: 3, completed: 4 }, error: undefined })

View File

@@ -90,7 +90,6 @@ import opendtu from "./opendtu/widget";
import openmediavault from "./openmediavault/widget";
import openwrt from "./openwrt/widget";
import opnsense from "./opnsense/widget";
import overseerr from "./overseerr/widget";
import pangolin from "./pangolin/widget";
import paperlessngx from "./paperlessngx/widget";
import peanut from "./peanut/widget";
@@ -244,7 +243,7 @@ const widgets = {
ombi,
opendtu,
opnsense,
overseerr,
overseerr: seerr,
openmediavault,
openwrt,
paperlessngx,