From 6eaf155b649f83b8c42aff0e69b02e1f12cabbe7 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 10 Jun 2023 14:20:42 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Migrate=20media=20reque?= =?UTF-8?q?sts=20to=20tRPC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/api/root.ts | 2 + src/server/api/routers/media-request.ts | 177 ++++++++++++++++++ .../media-requests/media-request-query.tsx | 21 ++- 3 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 src/server/api/routers/media-request.ts diff --git a/src/server/api/root.ts b/src/server/api/root.ts index b91218d03..ad550bbc8 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -7,6 +7,7 @@ import { iconRouter } from './routers/icon'; import { dashDotRouter } from './routers/dash-dot'; import { dnsHoleRouter } from './routers/dns-hole'; import { downloadRouter } from './routers/download'; +import { mediaRequestsRouter } from './routers/media-request'; /** * This is the primary router for your server. @@ -22,6 +23,7 @@ export const rootRouter = createTRPCRouter({ dashDot: dashDotRouter, dnsHole: dnsHoleRouter, download: downloadRouter, + mediaRequest: mediaRequestsRouter, }); // export type definition of API diff --git a/src/server/api/routers/media-request.ts b/src/server/api/routers/media-request.ts new file mode 100644 index 000000000..daf69aa20 --- /dev/null +++ b/src/server/api/routers/media-request.ts @@ -0,0 +1,177 @@ +import Consola from 'consola'; +import { z } from 'zod'; +import { getConfig } from '~/tools/config/getConfig'; +import { MediaRequest } from '~/widgets/media-requests/media-request-types'; +import { createTRPCRouter, publicProcedure } from '../trpc'; +import { MediaRequestListWidget } from '~/widgets/media-requests/MediaRequestListTile'; + +export const mediaRequestsRouter = createTRPCRouter({ + all: publicProcedure + .input( + z.object({ + configName: z.string(), + }) + ) + .query(async ({ input }) => { + const config = getConfig(input.configName); + + const apps = config.apps.filter((app) => + ['overseerr', 'jellyseerr'].includes(app.integration?.type ?? '') + ); + + Consola.log(`Retrieving media requests from ${apps.length} apps`); + + const promises = apps.map((app): Promise => { + const apiKey = + app.integration?.properties.find((prop) => prop.field === 'apiKey')?.value ?? ''; + const headers: HeadersInit = { 'X-Api-Key': apiKey }; + return fetch(`${app.url}/api/v1/request?take=25&skip=0&sort=added`, { + headers, + }) + .then(async (response) => { + const body = (await response.json()) as OverseerrResponse; + const mediaWidget = config.widgets.find((x) => x.type === 'media-requests-list') as + | MediaRequestListWidget + | undefined; + if (!mediaWidget) { + Consola.log('No media-requests-list found'); + return Promise.resolve([]); + } + const appUrl = mediaWidget.properties.replaceLinksWithExternalHost + ? app.behaviour.externalUrl + : app.url; + + const requests = await Promise.all( + body.results.map(async (item): Promise => { + const genericItem = await retrieveDetailsForItem( + app.url, + item.type, + headers, + item.media.tmdbId + ); + return { + appId: app.id, + createdAt: item.createdAt, + id: item.id, + rootFolder: item.rootFolder, + type: item.type, + name: genericItem.name, + userName: item.requestedBy.displayName, + userProfilePicture: constructAvatarUrl(appUrl, item), + userLink: `${appUrl}/users/${item.requestedBy.id}`, + airDate: genericItem.airDate, + status: item.status, + backdropPath: `https://image.tmdb.org/t/p/original/${genericItem.backdropPath}`, + posterPath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${genericItem.posterPath}`, + href: `${appUrl}/movie/${item.media.tmdbId}`, + }; + }) + ); + + return Promise.resolve(requests); + }) + .catch((err) => { + Consola.error(`Failed to request data from Overseerr: ${err}`); + return Promise.resolve([]); + }); + }); + + const mediaRequests = (await Promise.all(promises)).reduce( + (prev, cur) => prev.concat(cur), + [] + ); + + return mediaRequests; + }), +}); + +const constructAvatarUrl = (appUrl: string, item: OverseerrResponseItem) => { + const isAbsolute = + item.requestedBy.avatar.startsWith('http://') || item.requestedBy.avatar.startsWith('https://'); + + if (isAbsolute) { + return item.requestedBy.avatar; + } + + return `${appUrl}/${item.requestedBy.avatar}`; +}; + +const retrieveDetailsForItem = async ( + baseUrl: string, + type: OverseerrResponseItem['type'], + headers: HeadersInit, + id: number +): Promise => { + if (type === 'tv') { + const tvResponse = await fetch(`${baseUrl}/api/v1/tv/${id}`, { + headers, + }); + + const series = (await tvResponse.json()) as OverseerrSeries; + + return { + name: series.name, + airDate: series.firstAirDate, + backdropPath: series.backdropPath, + posterPath: series.backdropPath, + }; + } + + const movieResponse = await fetch(`${baseUrl}/api/v1/movie/${id}`, { + headers, + }); + + const movie = (await movieResponse.json()) as OverseerrMovie; + + return { + name: movie.originalTitle, + airDate: movie.releaseDate, + backdropPath: movie.backdropPath, + posterPath: movie.posterPath, + }; +}; + +type GenericOverseerrItem = { + name: string; + airDate: string; + backdropPath: string; + posterPath: string; +}; + +type OverseerrMovie = { + originalTitle: string; + releaseDate: string; + backdropPath: string; + posterPath: string; +}; + +type OverseerrSeries = { + name: string; + firstAirDate: string; + backdropPath: string; + posterPath: string; +}; + +type OverseerrResponse = { + results: OverseerrResponseItem[]; +}; + +type OverseerrResponseItem = { + id: number; + status: number; + createdAt: string; + type: 'movie' | 'tv'; + rootFolder: string; + requestedBy: OverseerrResponseItemUser; + media: OverseerrResponseItemMedia; +}; + +type OverseerrResponseItemMedia = { + tmdbId: number; +}; + +type OverseerrResponseItemUser = { + id: number; + displayName: string; + avatar: string; +}; diff --git a/src/widgets/media-requests/media-request-query.tsx b/src/widgets/media-requests/media-request-query.tsx index 10ec3541f..012cf6056 100644 --- a/src/widgets/media-requests/media-request-query.tsx +++ b/src/widgets/media-requests/media-request-query.tsx @@ -1,11 +1,12 @@ -import { useQuery } from '@tanstack/react-query'; -import { MediaRequest } from './media-request-types'; +import { useConfigContext } from '~/config/provider'; +import { api } from '~/utils/api'; -export const useMediaRequestQuery = () => useQuery({ - queryKey: ['media-requests'], - queryFn: async () => { - const response = await fetch('/api/modules/media-requests'); - return (await response.json()) as MediaRequest[]; - }, - refetchInterval: 3 * 60 * 1000, -}); +export const useMediaRequestQuery = () => { + const { name: configName } = useConfigContext(); + api.mediaRequest.all.useQuery( + { configName: configName! }, + { + refetchInterval: 3 * 60 * 1000, + } + ); +};