Skip to content

Commit

Permalink
feat: show quality profile on request (#847)
Browse files Browse the repository at this point in the history
* feat: backend fetch and return quality profile

* feat: show request profile name

* fix: wrong backend types

* feat: i18n keys

* fix: don't display quality profile if not set

* fix: remove development artifact

* fix: reduce parent div padding
  • Loading branch information
myselfolli authored Aug 1, 2024
1 parent 36d98a2 commit 6445332
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 35 deletions.
13 changes: 13 additions & 0 deletions server/interfaces/api/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ interface PageInfo {
export interface PaginatedResponse {
pageInfo: PageInfo;
}

/**
* Get the keys of an object that are not functions
*/
type NonFunctionPropertyNames<T> = {
// 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<T> = Pick<T, NonFunctionPropertyNames<T>>;
5 changes: 3 additions & 2 deletions server/interfaces/api/requestInterfaces.ts
Original file line number Diff line number Diff line change
@@ -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<MediaRequest>[];
}

export type MediaRequestBody = {
Expand All @@ -14,6 +14,7 @@ export type MediaRequestBody = {
is4k?: boolean;
serverId?: number;
profileId?: number;
profileName?: string;
rootFolder?: string;
languageProfileId?: number;
userId?: number;
Expand Down
61 changes: 60 additions & 1 deletion server/routes/request.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import {
MediaRequestStatus,
MediaStatus,
Expand All @@ -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';
Expand Down Expand Up @@ -143,14 +146,70 @@ requestRoutes.get<Record<string, unknown>, 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),
pageSize,
results: requestCount,
page: Math.ceil(skip / pageSize) + 1,
},
results: requests,
results: requestsWithProfileNames,
});
} catch (e) {
next({ status: 500, message: e.message });
Expand Down
28 changes: 16 additions & 12 deletions src/components/RequestCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -58,7 +59,7 @@ const RequestCardPlaceholder = () => {
};

interface RequestCardErrorProps {
requestData?: MediaRequest;
requestData?: NonFunctionProperties<MediaRequest>;
}

const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
Expand Down Expand Up @@ -213,7 +214,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
};

interface RequestCardProps {
request: MediaRequest;
request: NonFunctionProperties<MediaRequest>;
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
}

Expand All @@ -238,16 +239,19 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
data: requestData,
error: requestError,
mutate: revalidate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
fallbackData: request,
refreshInterval: refreshIntervalHelper(
{
downloadStatus: request.media.downloadStatus,
downloadStatus4k: request.media.downloadStatus4k,
},
15000
),
});
} = useSWR<NonFunctionProperties<MediaRequest>>(
`/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,
Expand Down
45 changes: 28 additions & 17 deletions src/components/RequestList/RequestItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -42,14 +43,15 @@ 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 => {
return (movie as MovieDetails).title !== undefined;
};

interface RequestItemErrorProps {
requestData?: MediaRequest;
requestData?: NonFunctionProperties<MediaRequest>;
revalidateList: () => void;
}

Expand Down Expand Up @@ -285,7 +287,7 @@ const RequestItemError = ({
};

interface RequestItemProps {
request: MediaRequest;
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
revalidateList: () => void;
}

Expand All @@ -304,19 +306,18 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
inView ? url : null
);
const { data: requestData, mutate: revalidate } = useSWR<MediaRequest>(
`/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<MediaRequest>
>(`/api/v1/request/${request.id}`, {
fallbackData: request,
refreshInterval: refreshIntervalHelper(
{
downloadStatus: request.media.downloadStatus,
downloadStatus4k: request.media.downloadStatus4k,
},
15000
),
});

const [isRetrying, setRetrying] = useState(false);

Expand Down Expand Up @@ -401,7 +402,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
setShowEditModal(false);
}}
/>
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-2 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
Expand Down Expand Up @@ -482,7 +483,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
)}
</div>
</div>
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center gap-1 overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(globalMessages.status)}
Expand Down Expand Up @@ -632,6 +633,16 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
</span>
</div>
)}
{request.profileName && (
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(messages.profileName)}
</span>
<span className="flex truncate text-sm text-gray-300">
{request.profileName}
</span>
</div>
)}
</div>
</div>
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
Expand Down
3 changes: 2 additions & 1 deletion src/components/RequestModal/MovieRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,7 +39,7 @@ const messages = defineMessages('components.RequestModal', {
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
tmdbId: number;
is4k?: boolean;
editRequest?: MediaRequest;
editRequest?: NonFunctionProperties<MediaRequest>;
onCancel?: () => void;
onComplete?: (newStatus: MediaStatus) => void;
onUpdating?: (isUpdating: boolean) => void;
Expand Down
3 changes: 2 additions & 1 deletion src/components/RequestModal/TvRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -57,7 +58,7 @@ interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
onComplete?: (newStatus: MediaStatus) => void;
onUpdating?: (isUpdating: boolean) => void;
is4k?: boolean;
editRequest?: MediaRequest;
editRequest?: NonFunctionProperties<MediaRequest>;
}

const TvRequestModal = ({
Expand Down
3 changes: 2 additions & 1 deletion src/components/RequestModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MediaRequest>;
onComplete?: (newStatus: MediaStatus) => void;
onCancel?: () => void;
onUpdating?: (isUpdating: boolean) => void;
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand Down

0 comments on commit 6445332

Please sign in to comment.