import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import { mediaRequestStatusConfiguration } from "@homarr/integrations/types"; import type { MediaRequest } from "@homarr/integrations/types"; import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; export const mediaRequestsRouter = createTRPCRouter({ getLatestRequests: publicProcedure .meta({ 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)", }, }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) .query(async ({ ctx }) => { const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false, }); return { integration: { id: integration.id, name: integration.name, kind: integration.kind, }, data, }; }), ); return results .flatMap(({ data, integration }) => data.map((request) => ({ ...request, integrationId: integration.id, })), ) .toSorted((dataA, dataB) => { if (dataA.status === dataB.status) { return dataB.createdAt.getTime() - dataA.createdAt.getTime(); } return ( mediaRequestStatusConfiguration[dataA.status].position - mediaRequestStatusConfiguration[dataB.status].position ); }); }), subscribeToLatestRequests: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) .subscription(({ ctx }) => { return observable<{ integrationId: string; requests: MediaRequest[]; }>((emit) => { const unsubscribes: (() => void)[] = []; for (const integrationWithSecrets of ctx.integrations) { const { decryptedSecrets: _, ...integration } = integrationWithSecrets; const innerHandler = mediaRequestListRequestHandler.handler(integrationWithSecrets, {}); const unsubscribe = innerHandler.subscribe((requests) => { emit.next({ integrationId: integration.id, requests, }); }); unsubscribes.push(unsubscribe); } return () => { unsubscribes.forEach((unsubscribe) => { unsubscribe(); }); }; }); }), getStats: publicProcedure .meta({ mcp: { enabled: true, description: "Get media request statistics including total counts and top requesters. REQUIRED: integrationIds (array of Overseerr/Jellyseerr integration IDs from integration_all)", }, }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) .query(async ({ ctx }) => { const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = mediaRequestStatsRequestHandler.handler(integration, {}); const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false, }); return { integration: { id: integration.id, name: integration.name, kind: integration.kind, }, data, }; }), ); return { stats: results.flatMap((result) => result.data.stats), users: results .map((result) => result.data.users.map((user) => ({ ...user, integration: result.integration, })), ) .flat() .toSorted(({ requestCount: countA }, { requestCount: countB }) => countB - countA), integrations: results.map((result) => result.integration), }; }), answerRequest: protectedProcedure .meta({ mcp: { enabled: true, description: "Approve or decline a pending media request. REQUIRED: integrationId (single Overseerr/Jellyseerr integration ID), requestId (number from getLatestRequests), answer ('approve' or 'decline')", }, }) .concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest"))) .input( z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]), }), ) .mutation(async ({ ctx: { integration }, input }) => { const integrationInstance = await createIntegrationAsync(integration); const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); const answerActions = { approve: (id: number) => integrationInstance.approveRequestAsync(id), decline: (id: number) => integrationInstance.declineRequestAsync(id), } as const; await answerActions[input.answer](input.requestId); await innerHandler.invalidateAsync(); }), });