UNPKG

@vo1x/tmdb

Version:

Unofficial TypeScript/JavaScript SDK for The Movie Database (TMDb) API v3.

410 lines (398 loc) 11.3 kB
'use strict'; // src/core/errors.ts var TMDBError = class _TMDBError extends Error { status; code; body; constructor(message, options) { super(message); this.name = "TMDBError"; this.status = options?.status; this.code = options?.code; this.body = options?.body; if (Error.captureStackTrace) { Error.captureStackTrace(this, _TMDBError); } } get isRateLimit() { return this.status === 429; } get isAuthError() { return this.status === 401 || this.code === 7; } get isNotFound() { return this.status === 404 || this.code === 34; } get friendlyMessage() { if (this.isRateLimit) { return "Rate limit exceeded. Please wait before making more requests."; } if (this.isAuthError) { return "Invalid API key. Please check your TMDB API credentials."; } if (this.isNotFound) { return "The requested content was not found."; } return this.message; } }; // src/shared/parser.ts function snakeToCamel(str) { return str.replace( /([-_][a-z])/g, (group) => group.toUpperCase().replace("-", "").replace("_", "") ); } function toCamelCase(obj) { if (Array.isArray(obj)) { return obj.map((v) => toCamelCase(v)); } else if (obj !== null && typeof obj === "object" && !(obj instanceof Date)) { return Object.keys(obj).reduce((acc, key) => { const camelKey = snakeToCamel(key); const value = obj[key]; acc[camelKey] = toCamelCase(value); return acc; }, {}); } return obj; } // src/core/http.ts var DEFAULT_BASE_URL = "https://api.themoviedb.org/3"; var HttpClient = class { baseUrl; apiKey; language; region; constructor(options) { this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL; this.apiKey = options.apiKey; this.language = options.language; this.region = options.region; } /** * Make a GET request to the TMDB API. * * @param path - API endpoint path (e.g., "/movie/550") * @param params - Query parameters to include in the request * @returns Promise resolving to the parsed JSON response * @throws {TMDBError} When the request fails or returns an error status */ async get(path, params) { const queryParams = this.buildQueryParams(params); const url = this.baseUrl + path + this.buildQueryString(queryParams); try { const response = await fetch(url, { method: "GET", headers: { Accept: "application/json", "User-Agent": "tmdb-client/0.2.0" } }); if (!response.ok) { await this.handleErrorResponse(response); } return await this.parseJsonResponse(response); } catch (error) { if (error instanceof TMDBError) { throw error; } const message = error instanceof Error ? error.message : "Unknown error occurred"; throw new TMDBError(`Network error: ${message}`, { status: 0 }); } } /** * Build query parameters, merging request params with client defaults. */ buildQueryParams(params) { return { ...params, language: params?.language ?? this.language, region: params?.region ?? this.region, ...this.apiKey ? { api_key: this.apiKey } : {} }; } /** * Convert parameters object to URL query string. * Handles arrays by joining with commas, filters out empty values. */ buildQueryString(params) { if (!params) return ""; const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value === void 0 || value === null || value === "") { continue; } if (Array.isArray(value)) { if (value.length > 0) { searchParams.set(key, value.join(",")); } continue; } searchParams.set(key, String(value)); } const queryString = searchParams.toString(); return queryString ? `?${queryString}` : ""; } /** * Handle error responses from TMDB API. * Attempts to parse TMDB error format and throws structured TMDBError. */ async handleErrorResponse(response) { let errorBody; let message = `HTTP ${response.status}: ${response.statusText}`; try { const contentType = response.headers.get("content-type") || ""; if (contentType.toLowerCase().includes("application/json")) { errorBody = await response.json(); if (errorBody.status_message) { message = errorBody.status_message; } } } catch { } if (response.status === 401) { message = "Invalid API key. Please check your TMDB credentials."; } else if (response.status === 429) { message = "Rate limit exceeded. Please slow down your requests."; } else if (response.status >= 500) { message = "TMDB server error. Please try again later."; } throw new TMDBError(message, { status: response.status, code: errorBody?.status_code, body: errorBody }); } /** * Parse JSON response with proper error handling. */ async parseJsonResponse(response) { const contentType = response.headers.get("content-type") || ""; if (!contentType.toLowerCase().includes("application/json")) { throw new TMDBError("Expected JSON response but received different content type", { status: response.status }); } try { const rawJson = await response.json(); return toCamelCase(rawJson); } catch { throw new TMDBError("Failed to parse JSON response from TMDB", { status: response.status }); } } }; // src/shared/validation.ts function validateId(id) { if (!Number.isInteger(id) || id <= 0) { throw new TMDBError("Invalid TMDB movie id. Expected a positive integer."); } } function validateQuery(query) { if (!query?.trim()) { throw new TMDBError("Search query cannot be empty"); } } // src/modules/movies/service.ts var MoviesService = class { constructor(http) { this.http = http; } async get(id, options) { validateId(id); return this.http.get(`/movie/${id}`, options); } async credits(id, options) { validateId(id); return this.http.get(`/movie/${id}/credits`, options); } async images(id, options) { validateId(id); return this.http.get(`/movie/${id}/images`, options); } async recommendations(id, options) { validateId(id); return this.http.get(`/movie/${id}/recommendations`, options); } async similar(id, options) { validateId(id); return this.http.get(`/movie/${id}/similar`, options); } }; // src/modules/tv/service.ts var TvService = class { constructor(http) { this.http = http; } get(id, opts) { validateId(id); return this.http.get(`/tv/${id}`, opts); } credits(id, opts) { validateId(id); return this.http.get(`/tv/${id}/credits`, opts); } images(id, opts) { validateId(id); return this.http.get(`/tv/${id}/images`, opts); } recommendations(id, opts) { validateId(id); return this.http.get(`/tv/${id}/recommendations`, opts); } similar(id, opts) { validateId(id); return this.http.get(`/tv/${id}/similar`, opts); } }; // src/modules/search/service.ts var SearchService = class { constructor(http) { this.http = http; } async searchMovies(query, options) { validateQuery(query); return this.http.get("/search/movie", { query: query.trim(), ...options }); } async searchTv(query, options) { validateQuery(query); return this.http.get("/search/tv", { query: query.trim(), ...options }); } async searchPeople(query, options) { validateQuery(query); return this.http.get("/search/person", { query: query.trim(), ...options }); } async searchMulti(query, options) { validateQuery(query); return this.http.get("/search/multi", { query: query.trim(), ...options }); } }; // src/modules/trending/service.ts var TrendingService = class { constructor(http) { this.http = http; } async getDailyTrending(opts) { return this.http.get(`/trending/all/day`, opts); } async getWeeklyTrending(opts) { return this.http.get(`/trending/all/week`, opts); } }; // src/modules/configuration/service.ts var ConfigurationService = class { constructor(http) { this.http = http; } async getConfig() { return this.http.get("/configuration"); } async countries(opts) { return this.http.get("/configuration/countries", opts); } async jobs() { return this.http.get("/configuration/jobs"); } async languages() { return this.http.get("/configuration/languages"); } async timezones() { return this.http.get("/configuration/timezones"); } }; // src/modules/person/service.ts var PersonService = class { constructor(http) { this.http = http; } async get(id, options) { validateId(id); return this.http.get(`/person/${id}`, options); } async images(id) { validateId(id); return this.http.get(`/person/${id}/images`); } async combinedCredits(id, options) { validateId(id); return this.http.get(`/person/${id}/combined_credits`, options); } }; // src/tmdb.ts var TMDB = class { movies; tv; trending; configuration; search; person; http; constructor(opts) { if (!opts?.apiKey?.trim()) { throw new TMDBError("API key is required.", { status: 401 }); } this.http = new HttpClient({ apiKey: opts.apiKey, baseUrl: opts.baseUrl ?? "https://api.themoviedb.org/3", language: opts.language ?? "en-US", timeout: opts.timeout ?? 2e4 }); const movies = new MoviesService(this.http); const tv = new TvService(this.http); const search = new SearchService(this.http); const trending = new TrendingService(this.http); const configuration = new ConfigurationService(this.http); const person = new PersonService(this.http); this.person = { get: person.get.bind(person), images: person.images.bind(person), combinedCredits: person.combinedCredits.bind(person) }; this.movies = { get: movies.get.bind(movies), credits: movies.credits.bind(movies), images: movies.images.bind(movies), recommendations: movies.recommendations.bind(movies), similar: movies.similar.bind(movies) }; this.tv = { get: tv.get.bind(tv), credits: tv.credits.bind(tv), images: tv.images.bind(tv), recommendations: tv.recommendations.bind(tv), similar: tv.similar.bind(tv) }; this.trending = { daily: trending.getDailyTrending.bind(trending), weekly: trending.getWeeklyTrending.bind(trending) }; this.configuration = { get: configuration.getConfig.bind(configuration), countries: configuration.countries.bind(configuration), jobs: configuration.jobs.bind(configuration), languages: configuration.languages.bind(configuration), timezones: configuration.timezones.bind(configuration) }; this.search = { movies: search.searchMovies.bind(search), tv: search.searchTv.bind(search), people: search.searchPeople.bind(search), multi: search.searchMulti.bind(search) }; } }; exports.TMDB = TMDB; exports.TMDBError = TMDBError; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map