refactor everything to use tmdb exclusively

This commit is contained in:
castdrian 2023-06-13 21:23:47 +02:00 committed by Adrian Castro
parent 8da155ba2b
commit 46bd20f718
8 changed files with 315 additions and 264 deletions

View File

@ -1,22 +1,22 @@
import { FetchError } from "ofetch";
import { formatJWMeta, mediaTypeToJW } from "./justwatch";
import { Tmdb } from "./tmdb";
import {
TTVMediaToMediaType,
Trakt,
formatTTVMeta,
mediaTypeToTTV,
} from "./trakttv";
TMDBMediaToMediaType,
Tmdb,
formatTMDBMeta,
mediaTypeToTMDB,
} from "./tmdb";
import {
JWMediaResult,
JWSeasonMetaResult,
JW_API_BASE,
MWMediaMeta,
MWMediaType,
TMDBMediaResult,
TMDBMovieData,
TMDBSeasonMetaResult,
TMDBShowData,
TTVSeasonMetaResult,
} from "./types";
import { makeUrl, proxiedFetch } from "../helpers/fetch";
@ -48,9 +48,7 @@ export async function getMetaFromId(
id: string,
seasonId?: string
): Promise<DetailedMeta | null> {
const result = await Trakt.searchById(id, mediaTypeToJW(type));
if (!result) return null;
const details = await Tmdb.getMediaDetails(id, type);
const details = await Tmdb.getMediaDetails(id, mediaTypeToTMDB(type));
if (!details) return null;
@ -59,15 +57,15 @@ export async function getMetaFromId(
imdbId = (details as TMDBMovieData).imdb_id ?? undefined;
}
let seasonData: TTVSeasonMetaResult | undefined;
let seasonData: TMDBSeasonMetaResult | undefined;
if (type === MWMediaType.SERIES) {
const seasons = (details as TMDBShowData).seasons;
const season =
seasons?.find((v) => v.id.toString() === seasonId) ?? seasons?.[0];
const episodes = await Trakt.getEpisodes(
result.ttv_entity_id,
const episodes = await Tmdb.getEpisodes(
details.id.toString(),
season?.season_number ?? 1
);
@ -81,10 +79,27 @@ export async function getMetaFromId(
}
}
const meta = formatTTVMeta(result, seasonData);
if (!meta) return null;
const tmdbmeta: TMDBMediaResult = {
id: details.id,
title:
type === MWMediaType.MOVIE
? (details as TMDBMovieData).title
: (details as TMDBShowData).name,
object_type: mediaTypeToTMDB(type),
seasons: (details as TMDBShowData).seasons.map((v) => ({
id: v.id,
season_number: v.season_number,
title: v.name,
})),
poster: (details as TMDBMovieData).poster_path ?? undefined,
original_release_year:
type === MWMediaType.MOVIE
? Number((details as TMDBMovieData).release_date?.split("-")[0])
: Number((details as TMDBShowData).first_air_date?.split("-")[0]),
};
console.log(meta);
const meta = formatTMDBMeta(tmdbmeta, seasonData);
if (!meta) return null;
return {
meta,
@ -143,18 +158,18 @@ export async function getLegacyMetaFromId(
};
}
export function TTVMediaToId(media: MWMediaMeta): string {
return ["TTV", mediaTypeToTTV(media.type), media.id].join("-");
export function TMDBMediaToId(media: MWMediaMeta): string {
return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
}
export function decodeTTVId(
export function decodeTMDBId(
paramId: string
): { id: string; type: MWMediaType } | null {
const [prefix, type, id] = paramId.split("-", 3);
if (prefix !== "TTV") return null;
if (prefix !== "tmdb") return null;
let mediaType;
try {
mediaType = TTVMediaToMediaType(type);
mediaType = TMDBMediaToMediaType(type);
} catch {
return null;
}
@ -170,11 +185,11 @@ export async function convertLegacyUrl(
if (url.startsWith("/media/JW")) {
const urlParts = url.split("/").slice(2);
const [, type, id] = urlParts[0].split("-", 3);
const meta = await getLegacyMetaFromId(TTVMediaToMediaType(type), id);
const meta = await getLegacyMetaFromId(TMDBMediaToMediaType(type), id);
if (!meta) return undefined;
const tmdbId = meta.tmdbId;
if (!tmdbId) return undefined;
return `/media/TTV-${type}-${tmdbId}`;
return `/media/tmdb-${type}-${tmdbId}`;
}
return undefined;
}

View File

@ -1,6 +1,11 @@
import { SimpleCache } from "@/utils/cache";
import { Trakt, mediaTypeToTTV } from "./trakttv";
import {
Tmdb,
formatTMDBMeta,
formatTMDBSearchResult,
mediaTypeToTMDB,
} from "./tmdb";
import { MWMediaMeta, MWQuery } from "./types";
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
@ -13,10 +18,17 @@ 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 data = await Tmdb.searchMedia(searchQuery, mediaTypeToTMDB(type));
const results = await Promise.all(
data.results.map(async (v) => {
const formattedResult = await formatTMDBSearchResult(
v,
mediaTypeToTMDB(type)
);
return formatTMDBMeta(formattedResult);
})
);
const results = await Trakt.search(searchQuery, contentType);
console.log(results[0]);
cache.set(query, results, 3600);
return results;
}

View File

@ -1,13 +1,100 @@
import { conf } from "@/setup/config";
import {
MWMediaMeta,
MWMediaType,
MWSeasonMeta,
TMDBContentTypes,
TMDBEpisodeShort,
TMDBMediaResult,
TMDBMediaStatic,
TMDBMovieData,
TMDBMovieResponse,
TMDBMovieResult,
TMDBSearchResultStatic,
TMDBSeason,
TMDBSeasonMetaResult,
TMDBShowData,
TMDBShowResponse,
TMDBShowResult,
} from "./types";
import { mwFetch } from "../helpers/fetch";
export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes {
if (type === MWMediaType.MOVIE) return "movie";
if (type === MWMediaType.SERIES) return "show";
throw new Error("unsupported type");
}
export function TMDBMediaToMediaType(type: string): MWMediaType {
if (type === "movie") return MWMediaType.MOVIE;
if (type === "show") 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 TMDBMediaToId(media: MWMediaMeta): string {
return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
}
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);
} catch {
return null;
}
return {
type: mediaType,
id,
};
}
export abstract class Tmdb {
private static baseURL = "https://api.themoviedb.org/3";
@ -24,9 +111,33 @@ export abstract class Tmdb {
return res;
}
public static searchMedia: TMDBSearchResultStatic["searchMedia"] = async (
query: string,
type: TMDBContentTypes
) => {
let data;
switch (type) {
case "movie":
data = await Tmdb.get<TMDBMovieResponse>(
`search/movie?query=${query}&include_adult=true&language=en-US&page=1`
);
break;
case "show":
data = await Tmdb.get<TMDBShowResponse>(
`search/tv?query=${query}&include_adult=true&language=en-US&page=1`
);
break;
default:
throw new Error("Invalid media type");
}
return data;
};
public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async (
id: string,
type: MWMediaType
type: TMDBContentTypes
) => {
let data;
@ -34,7 +145,7 @@ export abstract class Tmdb {
case "movie":
data = await Tmdb.get<TMDBMovieData>(`/movie/${id}`);
break;
case "series":
case "show":
data = await Tmdb.get<TMDBShowData>(`/tv/${id}`);
break;
default:
@ -47,4 +158,48 @@ export abstract class Tmdb {
public static getMediaPoster(posterPath: string | null): string | undefined {
if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`;
}
public static async getEpisodes(
id: string,
season: number
): Promise<TMDBEpisodeShort[]> {
const data = await Tmdb.get<TMDBSeason>(`/tv/${id}/season/${season}`);
return data.episodes.map((e) => ({
id: e.id,
episode_number: e.episode_number,
title: e.name,
}));
}
}
export async function formatTMDBSearchResult(
result: TMDBShowResult | TMDBMovieResult,
mediatype: TMDBContentTypes
): Promise<TMDBMediaResult> {
const type = TMDBMediaToMediaType(mediatype);
const details = await Tmdb.getMediaDetails(result.id.toString(), mediatype);
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:
type === MWMediaType.SERIES
? (result as TMDBShowResult).name
: (result as TMDBMovieResult).title,
poster: Tmdb.getMediaPoster(details.poster_path),
id: result.id,
original_release_year:
type === MWMediaType.SERIES
? Number((result as TMDBShowResult).first_air_date?.split("-")[0])
: Number((result as TMDBMovieResult).release_date?.split("-")[0]),
object_type: mediaTypeToTMDB(type),
seasons,
};
}

View File

@ -1,187 +0,0 @@
import { conf } from "@/setup/config";
import { Tmdb } from "./tmdb";
import {
MWMediaMeta,
MWMediaType,
MWSeasonMeta,
TMDBShowData,
TTVContentTypes,
TTVEpisodeResult,
TTVEpisodeShort,
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 ["MW", 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 !== "MW") 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)
);
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.tmdb,
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 searchById(
tmdbId: string,
type: "movie" | "show"
): Promise<TTVMediaResult> {
const data = await Trakt.get<TTVSearchResult[]>(
`/search/tmdb/${tmdbId}?type=${type}`
);
const formatted = await Promise.all(
// eslint-disable-next-line no-return-await
data.map(async (v) => await formatTTVSearchResult(v))
);
return formatted[0];
}
public static async getEpisodes(
slug: string,
season: number
): Promise<TTVEpisodeShort[]> {
const data = await Trakt.get<TTVEpisodeResult[]>(
`/shows/${slug}/seasons/${season}`
);
return data.map((e) => ({
id: e.ids.tmdb,
episode_number: e.number,
title: e.title,
}));
}
}

View File

@ -46,63 +46,36 @@ export interface MWQuery {
type: MWMediaType;
}
export type TTVContentTypes = "movie" | "show";
export type TMDBContentTypes = "movie" | "show";
export type TTVSeasonShort = {
export type TMDBSeasonShort = {
title: string;
id: number;
season_number: number;
};
export type TTVEpisodeShort = {
export type TMDBEpisodeShort = {
title: string;
id: number;
episode_number: number;
};
export type TTVMediaResult = {
export type TMDBMediaResult = {
title: string;
poster?: string;
id: number;
original_release_year?: number;
ttv_entity_id: string;
object_type: TTVContentTypes;
seasons?: TTVSeasonShort[];
object_type: TMDBContentTypes;
seasons?: TMDBSeasonShort[];
};
export type TTVSeasonMetaResult = {
export type TMDBSeasonMetaResult = {
title: string;
id: string;
season_number: number;
episodes: TTVEpisodeShort[];
episodes: TMDBEpisodeShort[];
};
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;
@ -255,12 +228,9 @@ export interface TMDBMovieData {
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;
getMediaDetails(id: string, type: "show"): TMDBMediaDetailsPromise;
getMediaDetails(id: string, type: "movie"): TMDBMediaDetailsPromise;
getMediaDetails(id: string, type: TMDBContentTypes): TMDBMediaDetailsPromise;
}
export type JWContentTypes = "movie" | "show";
@ -312,7 +282,7 @@ export type JWSeasonMetaResult = {
episodes: JWEpisodeShort[];
};
export interface TTVEpisodeResult {
export interface TMDBEpisodeResult {
season: number;
number: number;
title: string;
@ -323,3 +293,89 @@ export interface TTVEpisodeResult {
tmdb: number;
};
}
export interface TMDBShowResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
origin_country: string[];
original_language: string;
original_name: string;
overview: string;
popularity: number;
poster_path: string | null;
first_air_date: string;
name: string;
vote_average: number;
vote_count: number;
}
export interface TMDBShowResponse {
page: number;
results: TMDBShowResult[];
total_pages: number;
total_results: number;
}
export interface TMDBMovieResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
original_language: 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 TMDBMovieResponse {
page: number;
results: TMDBMovieResult[];
total_pages: number;
total_results: number;
}
export type TMDBSearchResultsPromise = Promise<
TMDBShowResponse | TMDBMovieResponse
>;
export interface TMDBSearchResultStatic {
searchMedia(query: string, type: TMDBContentTypes): TMDBSearchResultsPromise;
searchMedia(query: string, type: "movie"): TMDBSearchResultsPromise;
searchMedia(query: string, type: "show"): TMDBSearchResultsPromise;
}
export interface TMDBEpisode {
air_date: string;
episode_number: number;
id: number;
name: string;
overview: string;
production_code: string;
runtime: number;
season_number: number;
show_id: number;
still_path: string | null;
vote_average: number;
vote_count: number;
crew: any[];
guest_stars: any[];
}
export interface TMDBSeason {
_id: string;
air_date: string;
episodes: TMDBEpisode[];
name: string;
overview: string;
id: number;
poster_path: string | null;
season_number: number;
}

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TTVMediaToId } from "@/backend/metadata/getmeta";
import { TMDBMediaToId } from "@/backend/metadata/getmeta";
import { MWMediaMeta } from "@/backend/metadata/types";
import { DotList } from "@/components/text/DotList";
@ -132,7 +132,7 @@ export function MediaCard(props: MediaCardProps) {
const canLink = props.linkable && !props.closable;
let link = canLink
? `/media/${encodeURIComponent(TTVMediaToId(props.media))}`
? `/media/${encodeURIComponent(TMDBMediaToId(props.media))}`
: "#";
if (canLink && props.series)
link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent(

View File

@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { decodeTTVId, getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta";
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icon, Icons } from "@/components/Icon";
@ -44,7 +44,7 @@ export function EpisodeSelectionPopout() {
seasonId: sId,
season: undefined,
});
reqSeasonMeta(decodeTTVId(params.media)?.id as string, sId).then((v) => {
reqSeasonMeta(decodeTMDBId(params.media)?.id as string, sId).then((v) => {
if (v?.meta.type !== MWMediaType.SERIES) return;
setCurrentVisibleSeason({
seasonId: sId,

View File

@ -6,7 +6,7 @@ import { useHistory, useParams } from "react-router-dom";
import { MWStream } from "@/backend/helpers/streams";
import {
DetailedMeta,
decodeTTVId,
decodeTMDBId,
getMetaFromId,
} from "@/backend/metadata/getmeta";
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
@ -184,7 +184,7 @@ export function MediaView() {
const [selected, setSelected] = useState<SelectedMediaData | null>(null);
const [exec, loading, error] = useLoading(
async (mediaParams: string, seasonId?: string) => {
const data = decodeTTVId(mediaParams);
const data = decodeTMDBId(mediaParams);
if (!data) return null;
return getMetaFromId(data.type, data.id, seasonId);
}