diff --git a/packages/integrations/src/interfaces/media-requests/media-request-types.ts b/packages/integrations/src/interfaces/media-requests/media-request-types.ts index 4d7b094ff..e72f692a7 100644 --- a/packages/integrations/src/interfaces/media-requests/media-request-types.ts +++ b/packages/integrations/src/interfaces/media-requests/media-request-types.ts @@ -12,12 +12,14 @@ interface SeriesInformation { overview: string; seasons: SerieSeason[]; posterPath: string; + requestedSeasons: number[]; } interface MovieInformation { id: number; overview: string; posterPath: string; + requestedSeasons: number[]; } export type MediaInformation = SeriesInformation | MovieInformation; diff --git a/packages/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts index 619af2541..eaab2a421 100644 --- a/packages/integrations/src/overseerr/overseerr-integration.ts +++ b/packages/integrations/src/overseerr/overseerr-integration.ts @@ -60,6 +60,9 @@ export class OverseerrIntegration text: "overview" in result ? result.overview : undefined, type: result.mediaType, inLibrary: result.mediaInfo !== undefined, + availability: result.mediaInfo + ? this.mapAvailability(result.mediaInfo.status as UpstreamMediaAvailability, false) + : undefined, })); } @@ -70,7 +73,14 @@ export class OverseerrIntegration "X-Api-Key": this.getSecretValue("apiKey"), }, }); - return await mediaInformationSchema.parseAsync(await response.json()); + const data = await mediaInformationSchema.parseAsync(await response.json()); + const requestedSeasons = [ + ...new Set( + data.mediaInfo?.requests?.flatMap((req) => req.seasons.map((s) => s.seasonNumber)) ?? [], + ), + ]; + const { mediaInfo: _strip, ...rest } = data; + return { ...rest, requestedSeasons }; } /** @@ -341,6 +351,14 @@ interface MovieInformation { releaseDate: string; } +const mediaInfoRequestsSchema = z.object({ + requests: z.array(z.object({ + seasons: z.array(z.object({ + seasonNumber: z.number(), + })), + })).optional(), +}).optional(); + const mediaInformationSchema = z.union([ z.object({ id: z.number(), @@ -355,14 +373,20 @@ const mediaInformationSchema = z.union([ ), numberOfSeasons: z.number(), posterPath: z.string().startsWith("/"), + mediaInfo: mediaInfoRequestsSchema, }), z.object({ id: z.number(), overview: z.string(), posterPath: z.string().startsWith("/"), + mediaInfo: mediaInfoRequestsSchema, }), ]); +const searchMediaInfoSchema = z.object({ + status: z.number(), +}).optional(); + const searchSchema = z.object({ results: z .array( @@ -373,7 +397,7 @@ const searchSchema = z.object({ name: z.string(), posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(), overview: z.string(), - mediaInfo: z.object({}).optional(), + mediaInfo: searchMediaInfoSchema, }), z.object({ id: z.number(), @@ -381,14 +405,14 @@ const searchSchema = z.object({ title: z.string(), posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(), overview: z.string(), - mediaInfo: z.object({}).optional(), + mediaInfo: searchMediaInfoSchema, }), z.object({ id: z.number(), mediaType: z.literal("person"), name: z.string(), profilePath: z.string().startsWith("/").endsWith(".jpg").nullable(), - mediaInfo: z.object({}).optional(), + mediaInfo: searchMediaInfoSchema, }), ]), ) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index b43b6e32c..34890b159 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -131,7 +131,9 @@ export const mediaRequestsChildrenOptions = createChildrenOptions option.result.inLibrary || option.integration.permissions?.hasInteractAccess === false, + hide: (option) => + (option.result.type === "movie" && option.result.inLibrary) || + option.integration.permissions?.hasInteractAccess === false, Component(option) { const t = useScopedI18n("search.mode.media"); return ( diff --git a/packages/spotlight/src/modes/media/media-request-search-group.tsx b/packages/spotlight/src/modes/media/media-request-search-group.tsx index 56a6ba399..fc27792be 100644 --- a/packages/spotlight/src/modes/media/media-request-search-group.tsx +++ b/packages/spotlight/src/modes/media/media-request-search-group.tsx @@ -1,4 +1,4 @@ -import { Group, Image, Stack, Text } from "@mantine/core"; +import { Badge, Group, Image, Stack, Text } from "@mantine/core"; import { useDebouncedValue } from "@mantine/hooks"; import { IconMovie, IconSearch } from "@tabler/icons-react"; import { keepPreviousData } from "@tanstack/react-query"; @@ -7,12 +7,22 @@ import { useAtomValue } from "jotai"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { getIntegrationName } from "@homarr/definitions"; +import type { MediaAvailability } from "@homarr/integrations/types"; +import { mediaAvailabilityConfiguration } from "@homarr/integrations/types"; import { useScopedI18n } from "@homarr/translation/client"; import { createGroup } from "../../lib/group"; import { mediaRequestSearchScopeAtom } from "../../spotlight-store"; import { useMediaRequestSearchInteraction } from "../external/search-engines-search-group"; +const availabilityLabels: Partial> = { + available: "Available", + partiallyAvailable: "Partial", + processing: "Processing", + requested: "Requested", + pending: "Requested", +}; + type MediaRequestSearchResult = RouterOutputs["integration"]["searchMediaRequests"][number]; type MediaRequestSearchOption = @@ -47,6 +57,11 @@ export const mediaRequestSearchGroup = createGroup({ } const { result } = option; + const badgeLabel = result.availability ? availabilityLabels[result.availability as MediaAvailability] : undefined; + const badgeColor = result.availability + ? mediaAvailabilityConfiguration[result.availability as MediaAvailability]?.color + : undefined; + return ( {result.image ? ( @@ -54,8 +69,11 @@ export const mediaRequestSearchGroup = createGroup({ ) : ( )} - - {result.name} + + + {result.name} + {badgeLabel && {badgeLabel}} + {result.text ?? getIntegrationName(result.integration.kind)}