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"; media-request-list ### 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 }) => [