import slugify from "slugify"; import { conf } from "@/setup/config"; import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw"; import { ExternalIdMovieSearchResult, TMDBContentTypes, TMDBEpisodeShort, TMDBMediaResult, TMDBMovieData, TMDBMovieSearchResult, TMDBSearchResult, TMDBSeason, TMDBSeasonMetaResult, TMDBShowData, TMDBShowSearchResult, } from "./types/tmdb"; import { mwFetch } from "../helpers/fetch"; export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes { if (type === MWMediaType.MOVIE) return TMDBContentTypes.MOVIE; if (type === MWMediaType.SERIES) return TMDBContentTypes.TV; throw new Error("unsupported type"); } export function TMDBMediaToMediaType(type: TMDBContentTypes): MWMediaType { if (type === TMDBContentTypes.MOVIE) return MWMediaType.MOVIE; if (type === TMDBContentTypes.TV) return MWMediaType.SERIES; throw new Error("unsupported type"); } export function formatTMDBMeta( media: TMDBMediaResult, season?: TMDBSeasonMetaResult ): MWMediaMeta { const type = TMDBMediaToMediaType(media.object_type); let seasons: undefined | MWSeasonMeta[]; if (type === MWMediaType.SERIES) { seasons = media.seasons ?.sort((a, b) => a.season_number - b.season_number) .map( (v): MWSeasonMeta => ({ title: v.title, id: v.id.toString(), number: v.season_number, }) ); } return { title: media.title, id: media.id.toString(), year: media.original_release_year?.toString(), poster: media.poster, type, seasons: seasons as any, seasonData: season ? ({ id: season.id.toString(), number: season.season_number, title: season.title, episodes: season.episodes .sort((a, b) => a.episode_number - b.episode_number) .map((v) => ({ id: v.id.toString(), number: v.episode_number, title: v.title, })), } as any) : (undefined as any), }; } export function TMDBIdToUrlId( type: MWMediaType, tmdbId: string, title: string ) { return [ "tmdb", mediaTypeToTMDB(type), tmdbId, slugify(title, { lower: true, strict: true }), ].join("-"); } export function TMDBMediaToId(media: MWMediaMeta): string { return TMDBIdToUrlId(media.type, media.id, media.title); } export function decodeTMDBId( paramId: string ): { id: string; type: MWMediaType } | null { const [prefix, type, id] = paramId.split("-", 3); if (prefix !== "tmdb") return null; let mediaType; try { mediaType = TMDBMediaToMediaType(type as TMDBContentTypes); } catch { return null; } return { type: mediaType, id, }; } const baseURL = "https://api.themoviedb.org/3"; const headers = { accept: "application/json", Authorization: `Bearer ${conf().TMDB_READ_API_KEY}`, }; async function get(url: string, params?: object): Promise { const res = await mwFetch(encodeURI(url), { headers, baseURL, params: { ...params, }, }); return res; } export async function multiSearch( query: string ): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> { const data = await get("search/multi", { query, include_adult: false, language: "en-US", page: 1, }); // filter out results that aren't movies or shows const results = data.results.filter( (r) => r.media_type === TMDBContentTypes.MOVIE || r.media_type === TMDBContentTypes.TV ); return results; } export async function generateQuickSearchMediaUrl( query: string ): Promise { const data = await multiSearch(query); if (data.length === 0) return undefined; const result = data[0]; const title = result.media_type === TMDBContentTypes.MOVIE ? result.title : result.name; return `/media/${TMDBIdToUrlId( TMDBMediaToMediaType(result.media_type), result.id.toString(), title )}`; } // Conditional type which for inferring the return type based on the content type type MediaDetailReturn = T extends TMDBContentTypes.MOVIE ? TMDBMovieData : T extends TMDBContentTypes.TV ? TMDBShowData : never; export function getMediaDetails< T extends TMDBContentTypes, TReturn = MediaDetailReturn >(id: string, type: T): Promise { if (type === TMDBContentTypes.MOVIE) { return get(`/movie/${id}`, { append_to_response: "external_ids" }); } if (type === TMDBContentTypes.TV) { return get(`/tv/${id}`, { append_to_response: "external_ids" }); } throw new Error("Invalid media type"); } export function getMediaPoster(posterPath: string | null): string | undefined { if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`; } export async function getEpisodes( id: string, season: number ): Promise { const data = await get(`/tv/${id}/season/${season}`); return data.episodes.map((e) => ({ id: e.id, episode_number: e.episode_number, title: e.name, })); } export async function getMovieFromExternalId( imdbId: string ): Promise { const data = await get(`/find/${imdbId}`, { external_source: "imdb_id", }); const movie = data.movie_results[0]; if (!movie) return undefined; return movie.id.toString(); } export function formatTMDBSearchResult( result: TMDBMovieSearchResult | TMDBShowSearchResult, mediatype: TMDBContentTypes ): TMDBMediaResult { const type = TMDBMediaToMediaType(mediatype); if (type === MWMediaType.SERIES) { const show = result as TMDBShowSearchResult; return { title: show.name, poster: getMediaPoster(show.poster_path), id: show.id, original_release_year: new Date(show.first_air_date).getFullYear(), object_type: mediatype, }; } const movie = result as TMDBMovieSearchResult; return { title: movie.title, poster: getMediaPoster(movie.poster_path), id: movie.id, original_release_year: new Date(movie.release_date).getFullYear(), object_type: mediatype, }; }