feat(api): tmdb api wrapper / multi search route (#62)

Adds a "The Movie DB" api wrapper for some basic requests (search/get movie details/get tv details).
Also adds a search endpoint to our api and mappers to convert the tmdb results
pull/64/head
sct 4 years ago committed by GitHub
parent eb35339eb4
commit c702c17cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,242 @@
import axios, { AxiosInstance } from 'axios';
interface SearchOptions {
query: string;
page?: number;
includeAdult?: boolean;
}
interface TmdbMediaResult {
id: number;
media_type: string;
popularity: number;
poster_path?: string;
backdrop_path?: string;
vote_count: number;
vote_average: number;
genre_ids: number[];
overview: string;
original_language: string;
}
export interface TmdbMovieResult extends TmdbMediaResult {
media_type: 'movie';
title: string;
original_title: string;
release_date: string;
adult: boolean;
video: boolean;
}
export interface TmdbTvResult extends TmdbMediaResult {
media_type: 'tv';
name: string;
original_name: string;
origin_country: string[];
first_air_Date: string;
}
export interface TmdbPersonResult {
id: number;
name: string;
popularity: number;
profile_path?: string;
adult: boolean;
media_type: 'person';
known_for: (TmdbMovieResult | TmdbTvResult)[];
}
interface TmdbSearchMultiResponse {
page: number;
total_results: number;
total_pages: number;
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
}
interface TmdbMovieDetails {
id: number;
imdb_id?: string;
adult: boolean;
backdrop_path?: string;
poster_path?: string;
budget: number;
genres: {
id: number;
name: string;
}[];
homepage?: string;
original_language: string;
original_title: string;
overview?: string;
popularity: number;
production_companies: {
id: number;
name: string;
logo_path?: string;
origin_country: string;
}[];
production_countries: {
iso_3166_1: string;
name: string;
}[];
release_date: string;
revenue: number;
runtime?: number;
spoken_languages: {
iso_639_1: string;
name: string;
}[];
status: string;
tagline?: string;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}
interface TmdbTvEpisodeDetails {
id: number;
air_date: string;
episode_number: number;
name: string;
overview: string;
production_code: string;
season_number: number;
show_id: number;
still_path: string;
vote_average: number;
vote_cuont: number;
}
interface TmdbTvDetails {
id: number;
backdrop_path?: string;
created_by: {
id: number;
credit_id: string;
name: string;
gender: number;
profile_path?: string;
}[];
episode_run_time: number[];
first_air_date: string;
genres: {
id: number;
name: string;
}[];
homepage: string;
in_production: boolean;
languages: string[];
last_air_date: string;
last_episode_to_air?: TmdbTvEpisodeDetails;
name: string;
next_episode_to_air?: TmdbTvEpisodeDetails;
networks: {
id: number;
name: string;
logo_path: string;
origin_country: string;
}[];
number_of_episodes: number;
number_of_seasons: number;
origin_country: string[];
original_language: string;
original_name: string;
overview: string;
popularity: number;
poster_path?: string;
production_companies: {
id: number;
logo_path?: string;
name: string;
origin_country: string;
}[];
seasons: {
id: number;
air_date: string;
episode_count: number;
name: string;
overview: string;
poster_path: string;
season_number: number;
}[];
status: string;
type: string;
vote_average: number;
vote_count: number;
}
class TheMovieDb {
private apiKey = 'db55323b8d3e4154498498a75642b381';
private axios: AxiosInstance;
constructor() {
this.axios = axios.create({
baseURL: 'https://api.themoviedb.org/3',
params: {
api_key: this.apiKey,
},
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
}
public searchMulti = async ({
query,
page = 1,
includeAdult = false,
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
try {
const response = await this.axios.get('/search/multi', {
params: { query, page, include_adult: includeAdult },
});
return response.data;
} catch (e) {
throw new Error(`[TMDB] Failed to search multi: ${e.message}`);
}
};
public getMovie = async ({
movieId,
language = 'en-US',
}: {
movieId: number;
language?: string;
}): Promise<TmdbMovieDetails> => {
try {
const response = await this.axios.get<TmdbMovieDetails>(
`/movie/${movieId}`,
{
params: { language },
}
);
return response.data;
} catch (e) {
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
}
};
public getTvShow = async ({
tvId,
language = 'en-US',
}: {
tvId: number;
language?: string;
}): Promise<TmdbTvDetails> => {
try {
const response = await this.axios.get<TmdbTvDetails>(`/tv/${tvId}`, {
params: { language },
});
return response.data;
} catch (e) {
throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
}
};
}
export default TheMovieDb;

@ -0,0 +1,116 @@
import type {
TmdbMovieResult,
TmdbPersonResult,
TmdbTvResult,
} from '../api/themoviedb';
export type MediaType = 'tv' | 'movie' | 'person';
interface SearchResult {
id: number;
mediaType: MediaType;
popularity: number;
posterPath?: string;
backdropPath?: string;
voteCount: number;
voteAverage: number;
genreIds: number[];
overview: string;
originalLanguage: string;
}
export interface MovieResult extends SearchResult {
mediaType: 'movie';
title: string;
originalTitle: string;
releaseDate: string;
adult: boolean;
video: boolean;
}
export interface TvResult extends SearchResult {
mediaType: 'tv';
name: string;
originalName: string;
originCountry: string[];
firstAirDate: string;
}
export interface PersonResult {
id: number;
name: string;
popularity: number;
profilePath?: string;
adult: boolean;
mediaType: 'person';
knownFor: (MovieResult | TvResult)[];
}
export type Results = MovieResult | TvResult | PersonResult;
export const mapMovieResult = (movieResult: TmdbMovieResult): MovieResult => ({
id: movieResult.id,
mediaType: 'movie',
adult: movieResult.adult,
genreIds: movieResult.genre_ids,
originalLanguage: movieResult.original_language,
originalTitle: movieResult.original_title,
overview: movieResult.overview,
popularity: movieResult.popularity,
releaseDate: movieResult.release_date,
title: movieResult.title,
video: movieResult.video,
voteAverage: movieResult.vote_average,
voteCount: movieResult.vote_count,
backdropPath: movieResult.backdrop_path,
posterPath: movieResult.poster_path,
});
export const mapTvResult = (tvResult: TmdbTvResult): TvResult => ({
id: tvResult.id,
firstAirDate: tvResult.first_air_Date,
genreIds: tvResult.genre_ids,
mediaType: tvResult.media_type,
name: tvResult.name,
originCountry: tvResult.origin_country,
originalLanguage: tvResult.original_language,
originalName: tvResult.original_name,
overview: tvResult.overview,
popularity: tvResult.popularity,
voteAverage: tvResult.vote_average,
voteCount: tvResult.vote_count,
backdropPath: tvResult.backdrop_path,
posterPath: tvResult.poster_path,
});
export const mapPersonResult = (
personResult: TmdbPersonResult
): PersonResult => ({
id: personResult.id,
name: personResult.name,
popularity: personResult.popularity,
adult: personResult.adult,
mediaType: personResult.media_type,
profilePath: personResult.profile_path,
knownFor: personResult.known_for.map((result) => {
if (result.media_type === 'movie') {
return mapMovieResult(result);
}
return mapTvResult(result);
}),
});
export const mapSearchResults = (
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[]
): Results[] =>
results.map((result) => {
switch (result.media_type) {
case 'movie':
return mapMovieResult(result);
case 'tv':
return mapTvResult(result);
default:
return mapPersonResult(result);
}
});

@ -213,6 +213,114 @@ components:
- radarr
- sonarr
- public
MovieResult:
type: object
required:
- id
- mediaType
- title
properties:
id:
type: number
example: 1234
mediaType:
type: string
popularity:
type: number
example: 10
posterPath:
type: string
backdropPath:
type: string
voteCount:
type: number
voteAverage:
type: number
genreIds:
type: array
items:
type: number
overview:
type: string
example: 'Overview of the movie'
originalLanguage:
type: string
example: 'en'
title:
type: string
example: Movie Title
originalTitle:
type: string
example: Original Movie Title
releaseDate:
type: string
adult:
type: boolean
example: false
video:
type: boolean
example: false
TvResult:
type: object
properties:
id:
type: number
example: 1234
mediaType:
type: string
popularity:
type: number
example: 10
posterPath:
type: string
backdropPath:
type: string
voteCount:
type: number
voteAverage:
type: number
genreIds:
type: array
items:
type: number
overview:
type: string
example: 'Overview of the movie'
originalLanguage:
type: string
example: 'en'
name:
type: string
example: TV Show Name
originalName:
type: string
example: Original TV Show Name
originCountry:
type: array
items:
type: string
firstAirDate:
type: string
PersonResult:
type: object
properties:
id:
type: number
example: 12345
profilePath:
type: string
adult:
type: boolean
example: false
mediaType:
type: string
default: 'person'
knownFor:
type: array
items:
oneOf:
- $ref: '#/components/schemas/MovieResult'
- $ref: '#/components/schemas/TvResult'
securitySchemes:
cookieAuth:
@ -611,6 +719,43 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/User'
/search:
get:
summary: Search for movies/tv shows/people
description: Returns a list of movies/tv shows/people in JSON format
tags:
- search
parameters:
- in: query
name: query
required: true
schema:
type: string
example: 'Mulan'
responses:
'200':
description: Results
content:
application/json:
schema:
type: object
properties:
page:
type: number
example: 1
totalPages:
type: number
example: 20
totalResults:
type: number
example: 200
results:
type: array
items:
anyOf:
- $ref: '#/components/schemas/MovieResult'
- $ref: '#/components/schemas/TvResult'
- $ref: '#/components/schemas/PersonResult'
security:
- cookieAuth: []

@ -5,6 +5,7 @@ import { checkUser, isAuthenticated } from '../middleware/auth';
import settingsRoutes from './settings';
import { Permission } from '../lib/permissions';
import { getSettings } from '../lib/settings';
import searchRoutes from './search';
const router = Router();
@ -15,6 +16,7 @@ router.use(
isAuthenticated(Permission.MANAGE_SETTINGS),
settingsRoutes
);
router.use('/search', isAuthenticated(), searchRoutes);
router.use('/auth', authRoutes);
router.get('/settings/public', (_req, res) => {

@ -0,0 +1,21 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapSearchResults } from '../models/Search';
const searchRoutes = Router();
searchRoutes.get('/', async (req, res) => {
const tmdb = new TheMovieDb();
const results = await tmdb.searchMulti({ query: req.query.query as string });
const megaResults = mapSearchResults(results.results);
console.log(megaResults);
return res.status(200).json({
page: results.page,
totalPages: results.total_pages,
totalResults: results.total_results,
results: mapSearchResults(results.results),
});
});
export default searchRoutes;
Loading…
Cancel
Save