@vo1x/tmdb
Version:
Unofficial TypeScript/JavaScript SDK for The Movie Database (TMDb) API v3.
407 lines (396 loc) • 11.3 kB
JavaScript
// 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)
};
}
};
export { TMDB, TMDBError };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map