From 56a387d22d1f900656c6ab3d61fb0633e3967354 Mon Sep 17 00:00:00 2001 From: David Ludwig Date: Thu, 15 Apr 2021 15:20:16 -0500 Subject: [PATCH] Added TMDB support --- src/lib/tmdb/index.ts | 23 ++++++++ src/lib/tmdb/request.ts | 126 ++++++++++++++++++++++++++++++++++++++++ src/lib/tmdb/schema.ts | 81 ++++++++++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 src/lib/tmdb/index.ts create mode 100644 src/lib/tmdb/request.ts create mode 100644 src/lib/tmdb/schema.ts diff --git a/src/lib/tmdb/index.ts b/src/lib/tmdb/index.ts new file mode 100644 index 0000000..e764235 --- /dev/null +++ b/src/lib/tmdb/index.ts @@ -0,0 +1,23 @@ +import ApiRequestManager from "./request" +import * as Schema from "./schema"; + +export default class TheMovieDb +{ + protected requestManager!: ApiRequestManager; + + public constructor(apiKey: string) { + this.requestManager = new ApiRequestManager(apiKey); + } + + public async configuration() { + return await this.requestManager.get("/configuration"); + } + + public async searchMovie(query: string, year?: number, page?: number) { + return await this.requestManager.get("/search/movie", { query, year }); + } + + public async movie(id: number) { + return await this.requestManager.get(`/movie/${id}`); + } +} diff --git a/src/lib/tmdb/request.ts b/src/lib/tmdb/request.ts new file mode 100644 index 0000000..cbabd1a --- /dev/null +++ b/src/lib/tmdb/request.ts @@ -0,0 +1,126 @@ +import https, { RequestOptions } from "https"; + +/** + * The API URL + */ +const API_URL = "https://api.themoviedb.org/3"; + +/** + * A status error is used to indicate responses with non-200 status codes + */ +export class StatusError extends Error +{ + /** + * The resulting body of a response + */ + public readonly response: T; + + /** + * The resulting status code of a response + */ + public readonly statusCode?: number; + + /** + * Create a new error indicating non-200 status + */ + public constructor(response: T, statusCode: number) { + super(); + Object.setPrototypeOf(this, StatusError.prototype); + this.response = response; + this.statusCode = statusCode; + } +} + +/** + * A request manager with atomic/persistent request options + */ +export default class ApiRequestManager +{ + private __api_key: string; + + /** + * Store additional request options + */ + protected options: RequestOptions; + + /** + * Create a new API request manager + * + * @param options Additional request options + */ + public constructor(apiKey: string, options: RequestOptions = {}) { + this.__api_key = apiKey; + this.options = options; + } + + /** + * Perform a generic HTTPS request + * + * @param method The HTTP method + * @param url The URL to request + * @param apiKey An optional bearer token + * @param params Optional parameters + * @param body Optional body + */ + protected request(method: string, url: string, params?: any, body?: string) + { + return new Promise((resolve, reject) => { + // Create request options + let options = Object.assign({ method, headers: {} }, this.options); + if (body) { + options.headers["Content-Type"] = "application/json"; + options.headers["Content-Length"] = body.length; + } + + // Add search parameters if necessary + let requestUrl = new URL(url); + requestUrl.searchParams.set("api_key", this.__api_key); + if (params) { + Object.keys(params).forEach((key) => { + if (params[key] !== undefined) { + requestUrl.searchParams.set(key, params[key]); + } + }); + } + + // Create the request + let request = https.request(requestUrl, options, (res) => { + let rawData: string = ""; + res.setEncoding("utf8"); + res.on("data", chunk => {rawData += chunk}); + res.on("error", reject); + res.on("end", () => { + let response: T = JSON.parse(rawData); + if (res.statusCode == 200) { + resolve(response) + } else { + reject(new StatusError(response, res.statusCode)); + } + }); + }) + .on("error", reject) + .on("timeout", () => reject("timeout")); + if (body) { + request.write(body); + } + request.end(); + }); + } + + /** + * Perform a generic GET request + */ + public async get(path: string, params?: any) { + return await this.request("GET", `${API_URL}${path}`, params); + } + + /** + * Perform a generic POST request + */ + public async post(path: string, params?: any, body?: any) { + if (body !== undefined) { + body = JSON.stringify(body); + } + return await this.request("POST", `${API_URL}${path}`, params, body); + } +} diff --git a/src/lib/tmdb/schema.ts b/src/lib/tmdb/schema.ts new file mode 100644 index 0000000..c1a9cb2 --- /dev/null +++ b/src/lib/tmdb/schema.ts @@ -0,0 +1,81 @@ +export enum Status { + Rumored = "Rumored", + Planned = "Planned", + InProduction = "InProduction", + PostProduction = "PostProduction", + Released = "Released", + Canceled = "Canceled" +} + +export interface IGenre { + id : number, + name: string +} + +export interface ILanguage { + iso_639_1: string, + name: string +} + +export interface IMovieSearchResult { + adult : boolean, + backdrop_path : string | null, + genre_ids : number[], + id : number, + original_string: string, + original_title : string, + overview : string, + popularity : number, + poster_path : string | null, + release_date : string, + title : string, + video : boolean, + vote_average : number + vote_count : number, +} + +export interface IMovieDetails { + adult : boolean, + backdrop_path : string | null, + belongs_to_collection: any, + budget : number, + genres : IGenre[] + imdb_id : string | null, + original_language : string, + original_title : string, + overview : string | null, + popularity : number, + poster_path : string | null, + production_companies : IProductionCompany[], + production_countries : IProductionCountry[], + release_date : string, + revenue : number, + runtime : number | null, + spoken_languages : ILanguage[], + status : Status, + tagline : string | null, + title : string, + video : boolean, + vote_average : number, + vote_count : number +} + +export interface IProductionCompany { + name : string, + id : number, + logo_path : string | null, + origin_country: string +} + +export interface IProductionCountry { + release_date: string, + revenue : number, + runtime : number | null +} + +export interface IPaginatedResponse { + page : number, + results : T[], + total_results: number, + total_pages : number +}