4 changed files with 529 additions and 0 deletions
@ -0,0 +1,21 @@ |
|||||||
|
import { SimpleCache } from "@/utils/cache"; |
||||||
|
|
||||||
|
import { Trakt, mediaTypeToTTV } from "./trakttv"; |
||||||
|
import { MWMediaMeta, MWQuery } from "./types"; |
||||||
|
|
||||||
|
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>(); |
||||||
|
cache.setCompare((a, b) => { |
||||||
|
return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); |
||||||
|
}); |
||||||
|
cache.initialize(); |
||||||
|
|
||||||
|
export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> { |
||||||
|
if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; |
||||||
|
const { searchQuery, type } = query; |
||||||
|
|
||||||
|
const contentType = mediaTypeToTTV(type); |
||||||
|
|
||||||
|
const results = await Trakt.search(searchQuery, contentType); |
||||||
|
cache.set(query, results, 3600); |
||||||
|
return results; |
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
import { conf } from "@/setup/config"; |
||||||
|
|
||||||
|
import { |
||||||
|
DetailedMeta, |
||||||
|
MWMediaType, |
||||||
|
TMDBMediaStatic, |
||||||
|
TMDBMovieData, |
||||||
|
TMDBShowData, |
||||||
|
} from "./types"; |
||||||
|
import { mwFetch } from "../helpers/fetch"; |
||||||
|
|
||||||
|
export abstract class Tmdb { |
||||||
|
private static baseURL = "https://api.themoviedb.org/3"; |
||||||
|
|
||||||
|
private static headers = { |
||||||
|
accept: "application/json", |
||||||
|
Authorization: `Bearer ${conf().TMDB_API_KEY}`, |
||||||
|
}; |
||||||
|
|
||||||
|
private static async get<T>(url: string): Promise<T> { |
||||||
|
const res = await mwFetch<any>(url, { |
||||||
|
headers: Tmdb.headers, |
||||||
|
baseURL: Tmdb.baseURL, |
||||||
|
}); |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async ( |
||||||
|
id: string, |
||||||
|
type: MWMediaType |
||||||
|
) => { |
||||||
|
let data; |
||||||
|
|
||||||
|
switch (type) { |
||||||
|
case "movie": |
||||||
|
data = await Tmdb.get<TMDBMovieData>(`/movie/${id}`); |
||||||
|
break; |
||||||
|
case "series": |
||||||
|
data = await Tmdb.get<TMDBShowData>(`/tv/${id}`); |
||||||
|
break; |
||||||
|
default: |
||||||
|
throw new Error("Invalid media type"); |
||||||
|
} |
||||||
|
|
||||||
|
return data; |
||||||
|
}; |
||||||
|
|
||||||
|
public static getMediaPoster(posterPath: string | null): string | undefined { |
||||||
|
if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`; |
||||||
|
} |
||||||
|
|
||||||
|
/* public static async getMetaFromId( |
||||||
|
type: MWMediaType, |
||||||
|
id: string, |
||||||
|
seasonId?: string |
||||||
|
): Promise<DetailedMeta | null> { |
||||||
|
console.log("getMetaFromId", type, id, seasonId); |
||||||
|
|
||||||
|
const details = await Tmdb.getMediaDetails(id, type); |
||||||
|
|
||||||
|
if (!details) return null; |
||||||
|
|
||||||
|
let imdbId; |
||||||
|
if (type === MWMediaType.MOVIE) { |
||||||
|
imdbId = (details as TMDBMovieData).imdb_id ?? undefined; |
||||||
|
} |
||||||
|
|
||||||
|
if (!meta.length) return null; |
||||||
|
|
||||||
|
console.log(meta); |
||||||
|
|
||||||
|
return { |
||||||
|
meta, |
||||||
|
imdbId, |
||||||
|
tmdbId: id, |
||||||
|
}; |
||||||
|
} */ |
||||||
|
} |
||||||
@ -0,0 +1,166 @@ |
|||||||
|
import { conf } from "@/setup/config"; |
||||||
|
|
||||||
|
import { Tmdb } from "./tmdb"; |
||||||
|
import { |
||||||
|
DetailedMeta, |
||||||
|
MWMediaMeta, |
||||||
|
MWMediaType, |
||||||
|
MWSeasonMeta, |
||||||
|
TMDBShowData, |
||||||
|
TTVContentTypes, |
||||||
|
TTVMediaResult, |
||||||
|
TTVSearchResult, |
||||||
|
TTVSeasonMetaResult, |
||||||
|
} from "./types"; |
||||||
|
import { mwFetch } from "../helpers/fetch"; |
||||||
|
|
||||||
|
export function mediaTypeToTTV(type: MWMediaType): TTVContentTypes { |
||||||
|
if (type === MWMediaType.MOVIE) return "movie"; |
||||||
|
if (type === MWMediaType.SERIES) return "show"; |
||||||
|
throw new Error("unsupported type"); |
||||||
|
} |
||||||
|
|
||||||
|
export function TTVMediaToMediaType(type: string): MWMediaType { |
||||||
|
if (type === "movie") return MWMediaType.MOVIE; |
||||||
|
if (type === "show") return MWMediaType.SERIES; |
||||||
|
throw new Error("unsupported type"); |
||||||
|
} |
||||||
|
|
||||||
|
export function formatTTVMeta( |
||||||
|
media: TTVMediaResult, |
||||||
|
season?: TTVSeasonMetaResult |
||||||
|
): MWMediaMeta { |
||||||
|
const type = TTVMediaToMediaType(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 TTVMediaToId(media: MWMediaMeta): string { |
||||||
|
return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); |
||||||
|
} |
||||||
|
|
||||||
|
export function decodeTTVId( |
||||||
|
paramId: string |
||||||
|
): { id: string; type: MWMediaType } | null { |
||||||
|
const [prefix, type, id] = paramId.split("-", 3); |
||||||
|
if (prefix !== "TTV") return null; |
||||||
|
let mediaType; |
||||||
|
try { |
||||||
|
mediaType = TTVMediaToMediaType(type); |
||||||
|
} catch { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return { |
||||||
|
type: mediaType, |
||||||
|
id, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export async function formatTTVSearchResult( |
||||||
|
result: TTVSearchResult |
||||||
|
): Promise<TTVMediaResult> { |
||||||
|
const type = TTVMediaToMediaType(result.type); |
||||||
|
const media = result[result.type]; |
||||||
|
|
||||||
|
if (!media) throw new Error("invalid result"); |
||||||
|
|
||||||
|
const details = await Tmdb.getMediaDetails( |
||||||
|
media.ids.tmdb.toString(), |
||||||
|
TTVMediaToMediaType(result.type) |
||||||
|
); |
||||||
|
console.log(details); |
||||||
|
|
||||||
|
const seasons = |
||||||
|
type === MWMediaType.SERIES |
||||||
|
? (details as TMDBShowData).seasons?.map((v) => ({ |
||||||
|
id: v.id, |
||||||
|
title: v.name, |
||||||
|
season_number: v.season_number, |
||||||
|
})) |
||||||
|
: undefined; |
||||||
|
|
||||||
|
return { |
||||||
|
title: media.title, |
||||||
|
poster: Tmdb.getMediaPoster(details.poster_path), |
||||||
|
id: media.ids.trakt, |
||||||
|
original_release_year: media.year, |
||||||
|
ttv_entity_id: media.ids.slug, |
||||||
|
object_type: mediaTypeToTTV(type), |
||||||
|
seasons, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export abstract class Trakt { |
||||||
|
private static baseURL = "https://api.trakt.tv"; |
||||||
|
|
||||||
|
private static headers = { |
||||||
|
"Content-Type": "application/json", |
||||||
|
"trakt-api-version": "2", |
||||||
|
"trakt-api-key": conf().TRAKT_CLIENT_ID, |
||||||
|
}; |
||||||
|
|
||||||
|
private static async get<T>(url: string): Promise<T> { |
||||||
|
const res = await mwFetch<any>(url, { |
||||||
|
headers: Trakt.headers, |
||||||
|
baseURL: Trakt.baseURL, |
||||||
|
}); |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
public static async search( |
||||||
|
query: string, |
||||||
|
type: "movie" | "show" |
||||||
|
): Promise<MWMediaMeta[]> { |
||||||
|
const data = await Trakt.get<TTVSearchResult[]>( |
||||||
|
`/search/${type}?query=${encodeURIComponent(query)}` |
||||||
|
); |
||||||
|
|
||||||
|
const formatted = await Promise.all( |
||||||
|
// eslint-disable-next-line no-return-await
|
||||||
|
data.map(async (v) => await formatTTVSearchResult(v)) |
||||||
|
); |
||||||
|
return formatted.map((v) => formatTTVMeta(v)); |
||||||
|
} |
||||||
|
|
||||||
|
public static async getMetaFromId( |
||||||
|
type: MWMediaType, |
||||||
|
id: string, |
||||||
|
seasonId?: string |
||||||
|
): Promise<DetailedMeta | null> { |
||||||
|
console.log("getMetaFromId", type, id, seasonId); |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,264 @@ |
|||||||
|
export enum MWMediaType { |
||||||
|
MOVIE = "movie", |
||||||
|
SERIES = "series", |
||||||
|
ANIME = "anime", |
||||||
|
} |
||||||
|
|
||||||
|
export type MWSeasonMeta = { |
||||||
|
id: string; |
||||||
|
number: number; |
||||||
|
title: string; |
||||||
|
}; |
||||||
|
|
||||||
|
export type MWSeasonWithEpisodeMeta = { |
||||||
|
id: string; |
||||||
|
number: number; |
||||||
|
title: string; |
||||||
|
episodes: { |
||||||
|
id: string; |
||||||
|
number: number; |
||||||
|
title: string; |
||||||
|
}[]; |
||||||
|
}; |
||||||
|
|
||||||
|
type MWMediaMetaBase = { |
||||||
|
title: string; |
||||||
|
id: string; |
||||||
|
year?: string; |
||||||
|
poster?: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type MWMediaMetaSpecific = |
||||||
|
| { |
||||||
|
type: MWMediaType.MOVIE | MWMediaType.ANIME; |
||||||
|
seasons: undefined; |
||||||
|
} |
||||||
|
| { |
||||||
|
type: MWMediaType.SERIES; |
||||||
|
seasons: MWSeasonMeta[]; |
||||||
|
seasonData: MWSeasonWithEpisodeMeta; |
||||||
|
}; |
||||||
|
|
||||||
|
export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific; |
||||||
|
|
||||||
|
export interface MWQuery { |
||||||
|
searchQuery: string; |
||||||
|
type: MWMediaType; |
||||||
|
} |
||||||
|
|
||||||
|
export type TTVContentTypes = "movie" | "show"; |
||||||
|
|
||||||
|
export type TTVSeasonShort = { |
||||||
|
title: string; |
||||||
|
id: number; |
||||||
|
season_number: number; |
||||||
|
}; |
||||||
|
|
||||||
|
export type TTVEpisodeShort = { |
||||||
|
title: string; |
||||||
|
id: number; |
||||||
|
episode_number: number; |
||||||
|
}; |
||||||
|
|
||||||
|
export type TTVMediaResult = { |
||||||
|
title: string; |
||||||
|
poster?: string; |
||||||
|
id: number; |
||||||
|
original_release_year?: number; |
||||||
|
ttv_entity_id: string; |
||||||
|
object_type: TTVContentTypes; |
||||||
|
seasons?: TTVSeasonShort[]; |
||||||
|
}; |
||||||
|
|
||||||
|
export type TTVSeasonMetaResult = { |
||||||
|
title: string; |
||||||
|
id: string; |
||||||
|
season_number: number; |
||||||
|
episodes: TTVEpisodeShort[]; |
||||||
|
}; |
||||||
|
|
||||||
|
export interface TTVSearchResult { |
||||||
|
type: "movie" | "show"; |
||||||
|
score: number; |
||||||
|
movie?: { |
||||||
|
title: string; |
||||||
|
year: number; |
||||||
|
ids: { |
||||||
|
trakt: number; |
||||||
|
slug: string; |
||||||
|
imdb: string; |
||||||
|
tmdb: number; |
||||||
|
}; |
||||||
|
}; |
||||||
|
show?: { |
||||||
|
title: string; |
||||||
|
year: number; |
||||||
|
ids: { |
||||||
|
trakt: number; |
||||||
|
slug: string; |
||||||
|
tvdb: number; |
||||||
|
imdb: string; |
||||||
|
tmdb: number; |
||||||
|
}; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export interface DetailedMeta { |
||||||
|
meta: MWMediaMeta; |
||||||
|
imdbId?: string; |
||||||
|
tmdbId?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface TMDBShowData { |
||||||
|
adult: boolean; |
||||||
|
backdrop_path: string | null; |
||||||
|
created_by: { |
||||||
|
id: number; |
||||||
|
credit_id: string; |
||||||
|
name: string; |
||||||
|
gender: number; |
||||||
|
profile_path: string | null; |
||||||
|
}[]; |
||||||
|
episode_run_time: number[]; |
||||||
|
first_air_date: string; |
||||||
|
genres: { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
}[]; |
||||||
|
homepage: string; |
||||||
|
id: number; |
||||||
|
in_production: boolean; |
||||||
|
languages: string[]; |
||||||
|
last_air_date: string; |
||||||
|
last_episode_to_air: { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
overview: string; |
||||||
|
vote_average: number; |
||||||
|
vote_count: number; |
||||||
|
air_date: string; |
||||||
|
episode_number: number; |
||||||
|
production_code: string; |
||||||
|
runtime: number | null; |
||||||
|
season_number: number; |
||||||
|
show_id: number; |
||||||
|
still_path: string | null; |
||||||
|
} | null; |
||||||
|
name: string; |
||||||
|
next_episode_to_air: { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
overview: string; |
||||||
|
vote_average: number; |
||||||
|
vote_count: number; |
||||||
|
air_date: string; |
||||||
|
episode_number: number; |
||||||
|
production_code: string; |
||||||
|
runtime: number | null; |
||||||
|
season_number: number; |
||||||
|
show_id: number; |
||||||
|
still_path: string | null; |
||||||
|
} | null; |
||||||
|
networks: { |
||||||
|
id: number; |
||||||
|
logo_path: string; |
||||||
|
name: 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 | null; |
||||||
|
production_companies: { |
||||||
|
id: number; |
||||||
|
logo_path: string | null; |
||||||
|
name: string; |
||||||
|
origin_country: string; |
||||||
|
}[]; |
||||||
|
production_countries: { |
||||||
|
iso_3166_1: string; |
||||||
|
name: string; |
||||||
|
}[]; |
||||||
|
seasons: { |
||||||
|
air_date: string; |
||||||
|
episode_count: number; |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
overview: string; |
||||||
|
poster_path: string | null; |
||||||
|
season_number: number; |
||||||
|
}[]; |
||||||
|
spoken_languages: { |
||||||
|
english_name: string; |
||||||
|
iso_639_1: string; |
||||||
|
name: string; |
||||||
|
}[]; |
||||||
|
status: string; |
||||||
|
tagline: string; |
||||||
|
type: string; |
||||||
|
vote_average: number; |
||||||
|
vote_count: number; |
||||||
|
} |
||||||
|
|
||||||
|
export interface TMDBMovieData { |
||||||
|
adult: boolean; |
||||||
|
backdrop_path: string | null; |
||||||
|
belongs_to_collection: { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
poster_path: string | null; |
||||||
|
backdrop_path: string | null; |
||||||
|
} | null; |
||||||
|
budget: number; |
||||||
|
genres: { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
}[]; |
||||||
|
homepage: string | null; |
||||||
|
id: number; |
||||||
|
imdb_id: string | null; |
||||||
|
original_language: string; |
||||||
|
original_title: string; |
||||||
|
overview: string | null; |
||||||
|
popularity: number; |
||||||
|
poster_path: string | null; |
||||||
|
production_companies: { |
||||||
|
id: number; |
||||||
|
logo_path: string | null; |
||||||
|
name: string; |
||||||
|
origin_country: string; |
||||||
|
}[]; |
||||||
|
production_countries: { |
||||||
|
iso_3166_1: string; |
||||||
|
name: string; |
||||||
|
}[]; |
||||||
|
release_date: string; |
||||||
|
revenue: number; |
||||||
|
runtime: number | null; |
||||||
|
spoken_languages: { |
||||||
|
english_name: string; |
||||||
|
iso_639_1: string; |
||||||
|
name: string; |
||||||
|
}[]; |
||||||
|
status: string; |
||||||
|
tagline: string | null; |
||||||
|
title: string; |
||||||
|
video: boolean; |
||||||
|
vote_average: number; |
||||||
|
vote_count: number; |
||||||
|
} |
||||||
|
|
||||||
|
export type TMDBMediaDetailsPromise = Promise<TMDBShowData | TMDBMovieData>; |
||||||
|
|
||||||
|
export interface TMDBMediaStatic { |
||||||
|
getMediaDetails( |
||||||
|
id: string, |
||||||
|
type: MWMediaType.SERIES |
||||||
|
): TMDBMediaDetailsPromise; |
||||||
|
getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; |
||||||
|
getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; |
||||||
|
} |
||||||
Loading…
Reference in new issue