diff --git a/overseerr-api.yml b/overseerr-api.yml index b16efc27f..4b92a9eef 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -904,6 +904,8 @@ components: type: string character: type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' CreditCrew: type: object properties: @@ -958,6 +960,8 @@ components: type: string job: type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' securitySchemes: cookieAuth: type: apiKey diff --git a/server/interfaces/api/personInterfaces.ts b/server/interfaces/api/personInterfaces.ts new file mode 100644 index 000000000..19d3468ce --- /dev/null +++ b/server/interfaces/api/personInterfaces.ts @@ -0,0 +1,7 @@ +import { PersonCreditCast, PersonCreditCrew } from '../../models/Person'; + +export interface PersonCombinedCreditsResponse { + id: number; + cast: PersonCreditCast[]; + crew: PersonCreditCrew[]; +} diff --git a/server/models/Person.ts b/server/models/Person.ts index c8b24a6a0..575e40cc6 100644 --- a/server/models/Person.ts +++ b/server/models/Person.ts @@ -3,6 +3,7 @@ import { TmdbPersonCreditCrew, TmdbPersonDetail, } from '../api/themoviedb'; +import Media from '../entity/Media'; export interface PersonDetail { id: number; @@ -42,6 +43,7 @@ export interface PersonCredit { title: string; adult: boolean; releaseDate: string; + mediaInfo?: Media; } export interface PersonCreditCast extends PersonCredit { @@ -76,7 +78,8 @@ export const mapPersonDetails = (person: TmdbPersonDetail): PersonDetail => ({ }); export const mapCastCredits = ( - cast: TmdbPersonCreditCast + cast: TmdbPersonCreditCast, + media?: Media ): PersonCreditCast => ({ id: cast.id, originalLanguage: cast.original_language, @@ -100,10 +103,12 @@ export const mapCastCredits = ( adult: cast.adult, releaseDate: cast.release_date, character: cast.character, + mediaInfo: media, }); export const mapCrewCredits = ( - crew: TmdbPersonCreditCrew + crew: TmdbPersonCreditCrew, + media?: Media ): PersonCreditCrew => ({ id: crew.id, originalLanguage: crew.original_language, @@ -128,4 +133,5 @@ export const mapCrewCredits = ( releaseDate: crew.release_date, department: crew.department, job: crew.job, + mediaInfo: media, }); diff --git a/server/routes/person.ts b/server/routes/person.ts index 9ee1be91e..dfdd4ce24 100644 --- a/server/routes/person.ts +++ b/server/routes/person.ts @@ -1,5 +1,6 @@ import { Router } from 'express'; import TheMovieDb from '../api/themoviedb'; +import Media from '../entity/Media'; import logger from '../logger'; import { mapCastCredits, @@ -32,9 +33,27 @@ personRoutes.get('/:id/combined_credits', async (req, res) => { language: req.query.language as string, }); + const castMedia = await Media.getRelatedMedia( + combinedCredits.cast.map((result) => result.id) + ); + + const crewMedia = await Media.getRelatedMedia( + combinedCredits.crew.map((result) => result.id) + ); + return res.status(200).json({ - cast: combinedCredits.cast.map((result) => mapCastCredits(result)), - crew: combinedCredits.crew.map((result) => mapCrewCredits(result)), + cast: combinedCredits.cast.map((result) => + mapCastCredits( + result, + castMedia.find((med) => med.tmdbId === result.id) + ) + ), + crew: combinedCredits.crew.map((result) => + mapCrewCredits( + result, + crewMedia.find((med) => med.tmdbId === result.id) + ) + ), id: combinedCredits.id, }); }); diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 77f26f790..57e4779db 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -527,6 +527,7 @@ const MovieDetails: React.FC = ({ movie }) => { items={data?.credits.cast.slice(0, 20).map((person) => ( = ({ + personId, name, subName, profilePath, canExpand = false, }) => { return ( -
-
-
- {profilePath && ( -
- )} - {!profilePath && ( - - - - )} -
{name}
- {subName && ( - + + ); }; diff --git a/src/components/PersonDetails/index.tsx b/src/components/PersonDetails/index.tsx new file mode 100644 index 000000000..9928dda4c --- /dev/null +++ b/src/components/PersonDetails/index.tsx @@ -0,0 +1,120 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; +import useSWR from 'swr'; +import type { PersonDetail } from '../../../server/models/Person'; +import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces'; +import Error from '../../pages/_error'; +import LoadingSpinner from '../Common/LoadingSpinner'; +import Slider from '../Slider'; +import TitleCard from '../TitleCard'; +import { defineMessages, useIntl } from 'react-intl'; +import { LanguageContext } from '../../context/LanguageContext'; + +const messages = defineMessages({ + appearsin: 'Appears in', + ascharacter: 'as {character}', + nobiography: 'No biography available.', +}); + +const PersonDetails: React.FC = () => { + const intl = useIntl(); + const { locale } = useContext(LanguageContext); + const router = useRouter(); + const { data, error } = useSWR( + `/api/v1/person/${router.query.personId}` + ); + + const { + data: combinedCredits, + error: errorCombinedCredits, + } = useSWR( + `/api/v1/person/${router.query.personId}/combined_credits?language=${locale}` + ); + + if (!data && !error) { + return ; + } + + if (!data) { + return ; + } + + const sortedCast = combinedCredits?.cast.sort((a, b) => { + const aDate = + a.mediaType === 'movie' + ? a.releaseDate?.slice(0, 4) ?? 0 + : a.firstAirDate?.slice(0, 4) ?? 0; + const bDate = + b.mediaType === 'movie' + ? b.releaseDate?.slice(0, 4) ?? 0 + : b.firstAirDate?.slice(0, 4) ?? 0; + if (aDate > bDate) { + return -1; + } + return 1; + }); + + return ( + <> +
+ {data.profilePath && ( +
+ )} +
+

{data.name}

+
+ {data.biography + ? data.biography + : intl.formatMessage(messages.nobiography)} +
+
+
+
+
+
+ {intl.formatMessage(messages.appearsin)} +
+
+
+ { + return ( +
+ + {media.character && ( +
+ {intl.formatMessage(messages.ascharacter, { + character: media.character, + })} +
+ )} +
+ ); + })} + /> + + ); +}; + +export default PersonDetails; diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index f9a0bdb02..85fec8981 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -478,6 +478,7 @@ const TvDetails: React.FC = ({ tv }) => { items={data?.credits.cast.slice(0, 20).map((person) => ( { + return ; +}; + +export default MoviePage;