diff --git a/apps/docs/docs/widgets/media-request-list/index.mdx b/apps/docs/docs/widgets/media-request-list/index.mdx
index 2f52a8e1f..1c491a051 100644
--- a/apps/docs/docs/widgets/media-request-list/index.mdx
+++ b/apps/docs/docs/widgets/media-request-list/index.mdx
@@ -1,41 +1,44 @@
---
-title: 'Media Request List'
-description: 'See a list of all media requests from your integration'
+title: "Media Request List"
+description: "See a list of all media requests from your integration"
hide_title: true
---
-import { WidgetHeader } from '@site/src/components/widgets/header';
-import { AddingWidget } from '@site/src/components/widgets/adding';
-import { WidgetConfig } from '@site/src/components/widgets/configuration';
-import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
+import { WidgetHeader } from "@site/src/components/widgets/header";
+import { AddingWidget } from "@site/src/components/widgets/adding";
+import { WidgetConfig } from "@site/src/components/widgets/configuration";
+import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { IconAd } from "@tabler/icons-react";
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-import { mediaRequestListWidget } from '.';
-import { jellyseerrIntegration } from '@site/docs/integrations/jellyseerr';
-import { overseerrIntegration } from '@site/docs/integrations/overseerr';
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+import { mediaRequestListWidget } from ".";
+import { jellyseerrIntegration } from "@site/docs/integrations/jellyseerr";
+import { overseerrIntegration } from "@site/docs/integrations/overseerr";
-
-
+
Provides a list of all open and recently closed requests. It also provides the functionality to approve or reject requests.
+You can limit the list to specific request statuses (for example, only show `pending`) and restrict it to requests created within the last N days.
+
### Screenshots
-import mediaRequestList from './img/list.png';
+import mediaRequestList from "./img/list.png";
### Supported Integrations
-
+
### Interactions
@@ -47,4 +50,5 @@ import mediaRequestList from './img/list.png';
### Configuration
+
diff --git a/apps/docs/docs/widgets/media-request-list/index.ts b/apps/docs/docs/widgets/media-request-list/index.ts
index 8eb9d8bfb..1732d6fd7 100644
--- a/apps/docs/docs/widgets/media-request-list/index.ts
+++ b/apps/docs/docs/widgets/media-request-list/index.ts
@@ -14,6 +14,18 @@ export const mediaRequestListWidget: WidgetDefinition = {
values: { type: "boolean" },
defaultValue: "yes",
},
+ {
+ name: "Statuses to show",
+ description: "Only requests with these statuses will be shown. Defaults to all statuses.",
+ values: "List of: 'pending', 'approved', 'declined', 'failed', 'completed'",
+ defaultValue: "All statuses",
+ },
+ {
+ name: "Show only recent requests",
+ description: "Limit to requests created within this many days. Set to 0 to disable the time filter.",
+ values: "0-365",
+ defaultValue: "0",
+ },
],
},
};
diff --git a/packages/api/src/router/widgets/media-requests.ts b/packages/api/src/router/widgets/media-requests.ts
index 99c4906e3..fbd192499 100644
--- a/packages/api/src/router/widgets/media-requests.ts
+++ b/packages/api/src/router/widgets/media-requests.ts
@@ -3,7 +3,10 @@ import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import { mediaRequestStatusConfiguration } from "@homarr/integrations/types";
-import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
+import {
+ mediaRequestListInputSchema,
+ mediaRequestListRequestHandler,
+} from "@homarr/request-handler/media-request-list";
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
@@ -16,13 +19,14 @@ export const mediaRequestsRouter = createTRPCRouter({
mcp: {
enabled: true,
description:
- "Get latest media requests from Overseerr/Jellyseerr with their status (pending, approved, declined, available). REQUIRED: integrationIds (array of Overseerr/Jellyseerr integration IDs from integration_all)",
+ "Get latest media requests from Overseerr/Jellyseerr with their status (pending, approved, declined, failed, completed). REQUIRED: integrationIds (array of Overseerr/Jellyseerr integration IDs from integration_all). OPTIONAL: statuses (array of statuses to include, must be non-empty) and recentDays (number 0-365; 0 disables the time filter).",
},
})
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
- .query(async ({ ctx }) => {
+ .input(mediaRequestListInputSchema)
+ .query(async ({ ctx, input }) => {
const results = await settleIntegrationQueries(ctx.integrations, async (integration) => {
- const { data } = await mediaRequestListRequestHandler.handler(integration, {}).getDataAsync();
+ const { data } = await mediaRequestListRequestHandler.handler(integration, input).getDataAsync();
return { integration: { id: integration.id, name: integration.name, kind: integration.kind }, data };
});
return results
diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts
index a4ae66bb0..a52de2b04 100644
--- a/packages/old-import/src/widgets/options.ts
+++ b/packages/old-import/src/widgets/options.ts
@@ -23,6 +23,8 @@ type OptionMapping = {
const optionMapping: OptionMapping = {
"mediaRequests-requestList": {
linksTargetNewTab: (oldOptions) => oldOptions.openInNewTab,
+ statusFilter: () => undefined,
+ recentDays: () => undefined,
},
"mediaRequests-requestStats": {},
bookmarks: {
diff --git a/packages/request-handler/src/media-request-list.ts b/packages/request-handler/src/media-request-list.ts
index 7a99dae76..d8a7c6336 100644
--- a/packages/request-handler/src/media-request-list.ts
+++ b/packages/request-handler/src/media-request-list.ts
@@ -1,17 +1,43 @@
+import { z } from "zod/v4";
+
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
-import type { MediaRequest } from "@homarr/integrations/types";
+import type { MediaRequest, MediaRequestStatus } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
+const mediaRequestStatusValues = [
+ "pending",
+ "approved",
+ "declined",
+ "failed",
+ "completed",
+] as const satisfies readonly MediaRequestStatus[];
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+export const mediaRequestListInputSchema = z.object({
+ statuses: z.array(z.enum(mediaRequestStatusValues)).nonempty(),
+ recentDays: z.number().min(0).max(365),
+});
+
+export type MediaRequestListInput = z.infer;
+
export const mediaRequestListRequestHandler = createIntegrationRequestHandler<
MediaRequest[],
IntegrationKindByCategory<"mediaRequest">,
- Record
+ MediaRequestListInput
>({
- async requestAsync(integration, _input) {
+ async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
- return await integrationInstance.getRequestsAsync();
+ const requests = await integrationInstance.getRequestsAsync();
+ const cutoff = input.recentDays > 0 ? Date.now() - input.recentDays * MS_PER_DAY : null;
+ const statusSet = new Set(input.statuses);
+ return requests.filter((request) => {
+ if (!statusSet.has(request.status)) return false;
+ if (cutoff !== null && request.createdAt.getTime() < cutoff) return false;
+ return true;
+ });
},
cacheTtlMs: 60_000,
fallbackToStaleOnError: true,
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index 86ecfdee2..ba5540adb 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -3201,6 +3201,12 @@
"option": {
"linksTargetNewTab": {
"label": "Open links in new tab"
+ },
+ "statusFilter": {
+ "label": "Statuses to show"
+ },
+ "recentDays": {
+ "label": "Show only recent requests"
}
},
"pending": {
diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx
index bbbd0047e..c5dc5bf06 100644
--- a/packages/widgets/src/media-requests/list/component.tsx
+++ b/packages/widgets/src/media-requests/list/component.tsx
@@ -38,6 +38,11 @@ export default function MediaServerWidget({
}: WidgetComponentProps<"mediaRequests-requestList">) {
const { data: mediaRequests } = clientApi.widget.mediaRequests.getLatestRequests.useQuery({
integrationIds,
+ statuses:
+ options.statusFilter.length > 0
+ ? options.statusFilter
+ : ["pending", "approved", "declined", "failed", "completed"],
+ recentDays: options.recentDays,
});
if (!mediaRequests) return ;
diff --git a/packages/widgets/src/media-requests/list/index.ts b/packages/widgets/src/media-requests/list/index.ts
index d4e0fcd54..1f65cd0c3 100644
--- a/packages/widgets/src/media-requests/list/index.ts
+++ b/packages/widgets/src/media-requests/list/index.ts
@@ -1,4 +1,5 @@
import { IconSearch, IconZoomQuestion } from "@tabler/icons-react";
+import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { openMediaRequestSearch } from "@homarr/spotlight";
@@ -6,6 +7,8 @@ import { openMediaRequestSearch } from "@homarr/spotlight";
import { createWidgetDefinition } from "../../definition";
import { optionsBuilder } from "../../options";
+const mediaRequestStatusValues = ["pending", "approved", "declined", "failed", "completed"] as const;
+
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", {
icon: IconZoomQuestion,
createOptions() {
@@ -13,6 +16,17 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaRequ
linksTargetNewTab: factory.switch({
defaultValue: true,
}),
+ statusFilter: factory.multiSelect({
+ defaultValue: [...mediaRequestStatusValues],
+ options: mediaRequestStatusValues.map((value) => ({
+ value,
+ label: (t) => t(`widget.mediaRequests-requestList.status.${value}`),
+ })),
+ }),
+ recentDays: factory.number({
+ validate: z.number().min(0).max(365),
+ defaultValue: 0,
+ }),
}));
},
contextActions: ({ integrationIds }) => [