mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
🏗️ Migrate media requests to tRPC
This commit is contained in:
@@ -7,6 +7,7 @@ import { iconRouter } from './routers/icon';
|
|||||||
import { dashDotRouter } from './routers/dash-dot';
|
import { dashDotRouter } from './routers/dash-dot';
|
||||||
import { dnsHoleRouter } from './routers/dns-hole';
|
import { dnsHoleRouter } from './routers/dns-hole';
|
||||||
import { downloadRouter } from './routers/download';
|
import { downloadRouter } from './routers/download';
|
||||||
|
import { mediaRequestsRouter } from './routers/media-request';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -22,6 +23,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
dashDot: dashDotRouter,
|
dashDot: dashDotRouter,
|
||||||
dnsHole: dnsHoleRouter,
|
dnsHole: dnsHoleRouter,
|
||||||
download: downloadRouter,
|
download: downloadRouter,
|
||||||
|
mediaRequest: mediaRequestsRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
177
src/server/api/routers/media-request.ts
Normal file
177
src/server/api/routers/media-request.ts
Normal file
@@ -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<MediaRequest[]> => {
|
||||||
|
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<MediaRequest> => {
|
||||||
|
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<GenericOverseerrItem> => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useConfigContext } from '~/config/provider';
|
||||||
import { MediaRequest } from './media-request-types';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
export const useMediaRequestQuery = () => useQuery({
|
export const useMediaRequestQuery = () => {
|
||||||
queryKey: ['media-requests'],
|
const { name: configName } = useConfigContext();
|
||||||
queryFn: async () => {
|
api.mediaRequest.all.useQuery(
|
||||||
const response = await fetch('/api/modules/media-requests');
|
{ configName: configName! },
|
||||||
return (await response.json()) as MediaRequest[];
|
{
|
||||||
},
|
refetchInterval: 3 * 60 * 1000,
|
||||||
refetchInterval: 3 * 60 * 1000,
|
}
|
||||||
});
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user