diff --git a/server/interfaces/api/common.ts b/server/interfaces/api/common.ts index 9ae25939b..d9e9490b4 100644 --- a/server/interfaces/api/common.ts +++ b/server/interfaces/api/common.ts @@ -8,3 +8,16 @@ interface PageInfo { export interface PaginatedResponse { pageInfo: PageInfo; } + +/** + * Get the keys of an object that are not functions + */ +type NonFunctionPropertyNames = { + // eslint-disable-next-line @typescript-eslint/ban-types + [K in keyof T]: T[K] extends Function ? never : K; +}[keyof T]; + +/** + * Get the properties of an object that are not functions + */ +export type NonFunctionProperties = Pick>; diff --git a/server/interfaces/api/requestInterfaces.ts b/server/interfaces/api/requestInterfaces.ts index 89863cb04..88b1201de 100644 --- a/server/interfaces/api/requestInterfaces.ts +++ b/server/interfaces/api/requestInterfaces.ts @@ -1,9 +1,9 @@ import type { MediaType } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; -import type { PaginatedResponse } from './common'; +import type { NonFunctionProperties, PaginatedResponse } from './common'; export interface RequestResultsResponse extends PaginatedResponse { - results: MediaRequest[]; + results: NonFunctionProperties[]; } export type MediaRequestBody = { @@ -14,6 +14,7 @@ export type MediaRequestBody = { is4k?: boolean; serverId?: number; profileId?: number; + profileName?: string; rootFolder?: string; languageProfileId?: number; userId?: number; diff --git a/server/routes/request.ts b/server/routes/request.ts index 83c05b485..94ae8384a 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -1,3 +1,5 @@ +import RadarrAPI from '@server/api/servarr/radarr'; +import SonarrAPI from '@server/api/servarr/sonarr'; import { MediaRequestStatus, MediaStatus, @@ -19,6 +21,7 @@ import type { RequestResultsResponse, } from '@server/interfaces/api/requestInterfaces'; import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; @@ -143,6 +146,62 @@ requestRoutes.get, RequestResultsResponse>( .skip(skip) .getManyAndCount(); + const settings = getSettings(); + + // get all quality profiles for every configured sonarr server + const sonarrServers = await Promise.all( + settings.sonarr.map(async (sonarrSetting) => { + const sonarr = new SonarrAPI({ + apiKey: sonarrSetting.apiKey, + url: SonarrAPI.buildUrl(sonarrSetting, '/api/v3'), + }); + + return { + id: sonarrSetting.id, + profiles: await sonarr.getProfiles(), + }; + }) + ); + + // get all quality profiles for every configured radarr server + const radarrServers = await Promise.all( + settings.radarr.map(async (radarrSetting) => { + const radarr = new RadarrAPI({ + apiKey: radarrSetting.apiKey, + url: RadarrAPI.buildUrl(radarrSetting, '/api/v3'), + }); + + return { + id: radarrSetting.id, + profiles: await radarr.getProfiles(), + }; + }) + ); + + // add profile names to the media requests, with undefined if not found + const requestsWithProfileNames = requests.map((r) => { + switch (r.type) { + case MediaType.MOVIE: { + const profileName = radarrServers + .find((serverr) => serverr.id === r.serverId) + ?.profiles.find((profile) => profile.id === r.profileId)?.name; + + return { + ...r, + profileName, + }; + } + case MediaType.TV: { + return { + ...r, + profileName: sonarrServers + .find((serverr) => serverr.id === r.serverId) + ?.profiles.find((profile) => profile.id === r.profileId)?.name, + }; + } + } + }); + return res.status(200).json({ pageInfo: { pages: Math.ceil(requestCount / pageSize), @@ -150,7 +209,7 @@ requestRoutes.get, RequestResultsResponse>( results: requestCount, page: Math.ceil(skip / pageSize) + 1, }, - results: requests, + results: requestsWithProfileNames, }); } catch (e) { next({ status: 500, message: e.message }); diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 8b75f7439..eb78806f3 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -19,6 +19,7 @@ import { } from '@heroicons/react/24/solid'; import { MediaRequestStatus } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import Image from 'next/image'; @@ -58,7 +59,7 @@ const RequestCardPlaceholder = () => { }; interface RequestCardErrorProps { - requestData?: MediaRequest; + requestData?: NonFunctionProperties; } const RequestCardError = ({ requestData }: RequestCardErrorProps) => { @@ -213,7 +214,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => { }; interface RequestCardProps { - request: MediaRequest; + request: NonFunctionProperties; onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void; } @@ -238,16 +239,19 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { data: requestData, error: requestError, mutate: revalidate, - } = useSWR(`/api/v1/request/${request.id}`, { - fallbackData: request, - refreshInterval: refreshIntervalHelper( - { - downloadStatus: request.media.downloadStatus, - downloadStatus4k: request.media.downloadStatus4k, - }, - 15000 - ), - }); + } = useSWR>( + `/api/v1/request/${request.id}`, + { + fallbackData: request, + refreshInterval: refreshIntervalHelper( + { + downloadStatus: request.media.downloadStatus, + downloadStatus4k: request.media.downloadStatus4k, + }, + 15000 + ), + } + ); const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({ mediaUrl: requestData?.media?.mediaUrl, diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 4694bec00..5d687b518 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -18,6 +18,7 @@ import { } from '@heroicons/react/24/solid'; import { MediaRequestStatus } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import Image from 'next/image'; @@ -42,6 +43,7 @@ const messages = defineMessages('components.RequestList.RequestItem', { tmdbid: 'TMDB ID', tvdbid: 'TheTVDB ID', unknowntitle: 'Unknown Title', + profileName: 'Profile', }); const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { @@ -49,7 +51,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { }; interface RequestItemErrorProps { - requestData?: MediaRequest; + requestData?: NonFunctionProperties; revalidateList: () => void; } @@ -285,7 +287,7 @@ const RequestItemError = ({ }; interface RequestItemProps { - request: MediaRequest; + request: NonFunctionProperties & { profileName?: string }; revalidateList: () => void; } @@ -304,19 +306,18 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { const { data: title, error } = useSWR( inView ? url : null ); - const { data: requestData, mutate: revalidate } = useSWR( - `/api/v1/request/${request.id}`, - { - fallbackData: request, - refreshInterval: refreshIntervalHelper( - { - downloadStatus: request.media.downloadStatus, - downloadStatus4k: request.media.downloadStatus4k, - }, - 15000 - ), - } - ); + const { data: requestData, mutate: revalidate } = useSWR< + NonFunctionProperties + >(`/api/v1/request/${request.id}`, { + fallbackData: request, + refreshInterval: refreshIntervalHelper( + { + downloadStatus: request.media.downloadStatus, + downloadStatus4k: request.media.downloadStatus4k, + }, + 15000 + ), + }); const [isRetrying, setRetrying] = useState(false); @@ -401,7 +402,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { setShowEditModal(false); }} /> -
+
{title.backdropPath && (
{ )}
-
+
{intl.formatMessage(globalMessages.status)} @@ -632,6 +633,16 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
)} + {request.profileName && ( +
+ + {intl.formatMessage(messages.profileName)} + + + {request.profileName} + +
+ )}
diff --git a/src/components/RequestModal/MovieRequestModal.tsx b/src/components/RequestModal/MovieRequestModal.tsx index 3a00e30a1..85af7aef4 100644 --- a/src/components/RequestModal/MovieRequestModal.tsx +++ b/src/components/RequestModal/MovieRequestModal.tsx @@ -8,6 +8,7 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { MediaStatus } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; import { Permission } from '@server/lib/permissions'; import type { MovieDetails } from '@server/models/Movie'; @@ -38,7 +39,7 @@ const messages = defineMessages('components.RequestModal', { interface RequestModalProps extends React.HTMLAttributes { tmdbId: number; is4k?: boolean; - editRequest?: MediaRequest; + editRequest?: NonFunctionProperties; onCancel?: () => void; onComplete?: (newStatus: MediaStatus) => void; onUpdating?: (isUpdating: boolean) => void; diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 4e847294b..71750678c 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -13,6 +13,7 @@ import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; import type SeasonRequest from '@server/entity/SeasonRequest'; +import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; import { Permission } from '@server/lib/permissions'; import type { TvDetails } from '@server/models/Tv'; @@ -57,7 +58,7 @@ interface RequestModalProps extends React.HTMLAttributes { onComplete?: (newStatus: MediaStatus) => void; onUpdating?: (isUpdating: boolean) => void; is4k?: boolean; - editRequest?: MediaRequest; + editRequest?: NonFunctionProperties; } const TvRequestModal = ({ diff --git a/src/components/RequestModal/index.tsx b/src/components/RequestModal/index.tsx index 9ef6b4057..198741793 100644 --- a/src/components/RequestModal/index.tsx +++ b/src/components/RequestModal/index.tsx @@ -4,13 +4,14 @@ import TvRequestModal from '@app/components/RequestModal/TvRequestModal'; import { Transition } from '@headlessui/react'; import type { MediaStatus } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { NonFunctionProperties } from '@server/interfaces/api/common'; interface RequestModalProps { show: boolean; type: 'movie' | 'tv' | 'collection'; tmdbId: number; is4k?: boolean; - editRequest?: MediaRequest; + editRequest?: NonFunctionProperties; onComplete?: (newStatus: MediaStatus) => void; onCancel?: () => void; onUpdating?: (isUpdating: boolean) => void; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 3dc25214b..a7bf780c6 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -465,6 +465,7 @@ "components.RequestList.RequestItem.mediaerror": "{mediaType} Not Found", "components.RequestList.RequestItem.modified": "Modified", "components.RequestList.RequestItem.modifieduserdate": "{date} by {user}", + "components.RequestList.RequestItem.profileName": "Profile", "components.RequestList.RequestItem.requested": "Requested", "components.RequestList.RequestItem.requesteddate": "Requested", "components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",