diff --git a/overseerr-api.yml b/overseerr-api.yml index 667f45087..398b5025b 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -803,6 +803,144 @@ components: type: string nullable: true + PersonDetail: + type: object + properties: + id: + type: number + example: 1 + name: + type: string + deathday: + type: string + knownForDepartment: + type: string + alsoKnownAs: + type: array + items: + type: string + gender: + type: string + biography: + type: string + popularity: + type: string + placeOfBirth: + type: string + profilePath: + type: string + adult: + type: boolean + imdbId: + type: string + homepage: + type: string + CreditCast: + type: object + properties: + id: + type: number + example: 1 + originalLanguage: + type: string + episodeCount: + type: number + overview: + type: string + originCountry: + type: array + items: + type: string + originalName: + type: string + voteCount: + type: number + name: + type: string + mediaType: + type: string + popularity: + type: number + creditId: + type: string + backdropPath: + type: string + firstAirDate: + type: string + voteAverage: + type: number + genreIds: + type: array + items: + type: number + posterPath: + type: string + originalTitle: + type: string + video: + type: boolean + title: + type: string + adult: + type: boolean + releaseDate: + type: string + character: + type: string + CreditCrew: + type: object + properties: + id: + type: number + example: 1 + originalLanguage: + type: string + episodeCount: + type: number + overview: + type: string + originCountry: + type: array + items: + type: string + originalName: + type: string + voteCount: + type: number + name: + type: string + mediaType: + type: string + popularity: + type: number + creditId: + type: string + backdropPath: + type: string + firstAirDate: + type: string + voteAverage: + type: number + genreIds: + type: array + items: + type: number + posterPath: + type: string + originalTitle: + type: string + video: + type: boolean + title: + type: string + adult: + type: boolean + releaseDate: + type: string + department: + type: string + job: + type: string securitySchemes: cookieAuth: type: apiKey @@ -2177,6 +2315,68 @@ paths: criticsRating: type: string enum: ['Rotten', 'Fresh'] + /person/{personId}: + get: + summary: Request person details + description: Returns details of the person based on provided person ID in JSON format + tags: + - person + parameters: + - in: path + name: personId + required: true + schema: + type: number + example: 287 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Returned person + content: + application/json: + schema: + $ref: '#/components/schemas/PersonDetail' + + /person/{personId}/combined_credits: + get: + summary: Request combined credits of person + description: Returns the combined credits of the person based on the provided person ID in JSON format + tags: + - person + parameters: + - in: path + name: personId + required: true + schema: + type: number + example: 287 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Returned combined credts + content: + application/json: + schema: + type: object + properties: + cast: + type: array + items: + $ref: '#/components/schemas/CreditCast' + crew: + type: array + items: + $ref: "#/components/schemas/CreditCrew" + id: + type: number /media: get: summary: Return all media diff --git a/server/api/themoviedb.ts b/server/api/themoviedb.ts index f83829c49..2aad437c3 100644 --- a/server/api/themoviedb.ts +++ b/server/api/themoviedb.ts @@ -1,5 +1,4 @@ import axios, { AxiosInstance } from 'axios'; -import { number } from 'yup'; interface SearchOptions { query: string; @@ -271,6 +270,60 @@ export interface TmdbTvDetails { external_ids: TmdbExternalIds; } +export interface TmdbPersonDetail { + id: number; + name: string; + deathday: string; + known_for_department: string; + also_known_as?: string[]; + gender: number; + biography: string; + popularity: string; + place_of_birth?: string; + profile_path?: string; + adult: boolean; + imdb_id?: string; + homepage?: string; +} + +export interface TmdbPersonCredit { + id: number; + original_language: string; + episode_count: number; + overview: string; + origin_country: string[]; + original_name: string; + vote_count: number; + name: string; + media_type?: string; + popularity: number; + credit_id: string; + backdrop_path?: string; + first_air_date: string; + vote_average: number; + genre_ids?: number[]; + poster_path?: string; + original_title: string; + video?: boolean; + title: string; + adult: boolean; + release_date: string; +} +export interface TmdbPersonCreditCast extends TmdbPersonCredit { + character: string; +} + +export interface TmdbPersonCreditCrew extends TmdbPersonCredit { + department: string; + job: string; +} + +export interface TmdbPersonCombinedCredits { + id: number; + cast: TmdbPersonCreditCast[]; + crew: TmdbPersonCreditCrew[]; +} + export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult { episodes: TmdbTvEpisodeResult[]; external_ids: TmdbExternalIds; @@ -310,6 +363,50 @@ class TheMovieDb { } }; + public getPerson = async ({ + personId, + language = 'en-US', + }: { + personId: number; + language?: string; + }): Promise => { + try { + const response = await this.axios.get( + `/person/${personId}`, + { + params: { language }, + } + ); + + return response.data; + } catch (e) { + throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`); + } + }; + + public getPersonCombinedCredits = async ({ + personId, + language = 'en-US', + }: { + personId: number; + language?: string; + }): Promise => { + try { + const response = await this.axios.get( + `/person/${personId}/combined_credits`, + { + params: { language }, + } + ); + + return response.data; + } catch (e) { + throw new Error( + `[TMDB] Failed to fetch person combined credits: ${e.message}` + ); + } + }; + public getMovie = async ({ movieId, language = 'en-US', diff --git a/server/models/Person.ts b/server/models/Person.ts new file mode 100644 index 000000000..c8b24a6a0 --- /dev/null +++ b/server/models/Person.ts @@ -0,0 +1,131 @@ +import { + TmdbPersonCreditCast, + TmdbPersonCreditCrew, + TmdbPersonDetail, +} from '../api/themoviedb'; + +export interface PersonDetail { + id: number; + name: string; + deathday: string; + knownForDepartment: string; + alsoKnownAs?: string[]; + gender: number; + biography: string; + popularity: string; + placeOfBirth?: string; + profilePath?: string; + adult: boolean; + imdbId?: string; + homepage?: string; +} + +export interface PersonCredit { + id: number; + originalLanguage: string; + episodeCount: number; + overview: string; + originCountry: string[]; + originalName: string; + voteCount: number; + name: string; + mediaType?: string; + popularity: number; + creditId: string; + backdropPath?: string; + firstAirDate: string; + voteAverage: number; + genreIds?: number[]; + posterPath?: string; + originalTitle: string; + video?: boolean; + title: string; + adult: boolean; + releaseDate: string; +} + +export interface PersonCreditCast extends PersonCredit { + character: string; +} + +export interface PersonCreditCrew extends PersonCredit { + department: string; + job: string; +} + +export interface CombinedCredit { + id: number; + cast: PersonCreditCast[]; + crew: PersonCreditCrew[]; +} + +export const mapPersonDetails = (person: TmdbPersonDetail): PersonDetail => ({ + id: person.id, + name: person.name, + deathday: person.deathday, + knownForDepartment: person.known_for_department, + alsoKnownAs: person.also_known_as, + gender: person.gender, + biography: person.biography, + popularity: person.popularity, + placeOfBirth: person.place_of_birth, + profilePath: person.profile_path, + adult: person.adult, + imdbId: person.imdb_id, + homepage: person.homepage, +}); + +export const mapCastCredits = ( + cast: TmdbPersonCreditCast +): PersonCreditCast => ({ + id: cast.id, + originalLanguage: cast.original_language, + episodeCount: cast.episode_count, + overview: cast.overview, + originCountry: cast.origin_country, + originalName: cast.original_name, + voteCount: cast.vote_count, + name: cast.name, + mediaType: cast.media_type, + popularity: cast.popularity, + creditId: cast.credit_id, + backdropPath: cast.backdrop_path, + firstAirDate: cast.first_air_date, + voteAverage: cast.vote_average, + genreIds: cast.genre_ids, + posterPath: cast.poster_path, + originalTitle: cast.original_title, + video: cast.video, + title: cast.title, + adult: cast.adult, + releaseDate: cast.release_date, + character: cast.character, +}); + +export const mapCrewCredits = ( + crew: TmdbPersonCreditCrew +): PersonCreditCrew => ({ + id: crew.id, + originalLanguage: crew.original_language, + episodeCount: crew.episode_count, + overview: crew.overview, + originCountry: crew.origin_country, + originalName: crew.original_name, + voteCount: crew.vote_count, + name: crew.name, + mediaType: crew.media_type, + popularity: crew.popularity, + creditId: crew.credit_id, + backdropPath: crew.backdrop_path, + firstAirDate: crew.first_air_date, + voteAverage: crew.vote_average, + genreIds: crew.genre_ids, + posterPath: crew.poster_path, + originalTitle: crew.original_title, + video: crew.video, + title: crew.title, + adult: crew.adult, + releaseDate: crew.release_date, + department: crew.department, + job: crew.job, +}); diff --git a/server/routes/index.ts b/server/routes/index.ts index b57bec524..eda282daa 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -11,6 +11,7 @@ import requestRoutes from './request'; import movieRoutes from './movie'; import tvRoutes from './tv'; import mediaRoutes from './media'; +import personRoutes from './person'; const router = Router(); @@ -32,6 +33,7 @@ router.use('/request', isAuthenticated(), requestRoutes); router.use('/movie', isAuthenticated(), movieRoutes); router.use('/tv', isAuthenticated(), tvRoutes); router.use('/media', isAuthenticated(), mediaRoutes); +router.use('/person', isAuthenticated(), personRoutes); router.use('/auth', authRoutes); router.get('/', (_req, res) => { diff --git a/server/routes/person.ts b/server/routes/person.ts new file mode 100644 index 000000000..7a6305ae8 --- /dev/null +++ b/server/routes/person.ts @@ -0,0 +1,43 @@ +import { Router } from 'express'; +import next from 'next'; +import TheMovieDb from '../api/themoviedb'; +import logger from '../logger'; +import { + mapCastCredits, + mapCrewCredits, + mapPersonDetails, +} from '../models/Person'; + +const personRoutes = Router(); + +personRoutes.get('/:id', async (req, res, next) => { + const tmdb = new TheMovieDb(); + + try { + const person = await tmdb.getPerson({ + personId: Number(req.params.id), + language: req.query.language as string, + }); + return res.status(200).json(mapPersonDetails(person)); + } catch (e) { + logger.error(e.message); + next({ status: 404, message: 'Person not found' }); + } +}); + +personRoutes.get('/:id/combined_credits', async (req, res) => { + const tmdb = new TheMovieDb(); + + const combinedCredits = await tmdb.getPersonCombinedCredits({ + personId: Number(req.params.id), + language: req.query.language as string, + }); + + return res.status(200).json({ + cast: combinedCredits.cast.map((result) => mapCastCredits(result)), + crew: combinedCredits.crew.map((result) => mapCrewCredits(result)), + id: combinedCredits.id, + }); +}); + +export default personRoutes;