From 40e45ae103f29796e7725e368f3605f8d292ce56 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 20:06:46 +0200 Subject: [PATCH 01/56] partial refactor --- src/backend/metadata/search_new.ts | 21 +++ src/backend/metadata/tmdb.ts | 78 +++++++++ src/backend/metadata/trakttv.ts | 166 ++++++++++++++++++ src/backend/metadata/types_new.ts | 264 +++++++++++++++++++++++++++++ 4 files changed, 529 insertions(+) create mode 100644 src/backend/metadata/search_new.ts create mode 100644 src/backend/metadata/tmdb.ts create mode 100644 src/backend/metadata/trakttv.ts create mode 100644 src/backend/metadata/types_new.ts diff --git a/src/backend/metadata/search_new.ts b/src/backend/metadata/search_new.ts new file mode 100644 index 00000000..4506514a --- /dev/null +++ b/src/backend/metadata/search_new.ts @@ -0,0 +1,21 @@ +import { SimpleCache } from "@/utils/cache"; + +import { Trakt, mediaTypeToTTV } from "./trakttv"; +import { MWMediaMeta, MWQuery } from "./types"; + +const cache = new SimpleCache(); +cache.setCompare((a, b) => { + return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); +}); +cache.initialize(); + +export async function searchForMedia(query: MWQuery): Promise { + 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; +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts new file mode 100644 index 00000000..0700945b --- /dev/null +++ b/src/backend/metadata/tmdb.ts @@ -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(url: string): Promise { + const res = await mwFetch(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(`/movie/${id}`); + break; + case "series": + data = await Tmdb.get(`/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 { + 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, + }; + } */ +} diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts new file mode 100644 index 00000000..5fb67a17 --- /dev/null +++ b/src/backend/metadata/trakttv.ts @@ -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 { + 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(url: string): Promise { + const res = await mwFetch(url, { + headers: Trakt.headers, + baseURL: Trakt.baseURL, + }); + return res; + } + + public static async search( + query: string, + type: "movie" | "show" + ): Promise { + const data = await Trakt.get( + `/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 { + console.log("getMetaFromId", type, id, seasonId); + return null; + } +} diff --git a/src/backend/metadata/types_new.ts b/src/backend/metadata/types_new.ts new file mode 100644 index 00000000..f954ff6a --- /dev/null +++ b/src/backend/metadata/types_new.ts @@ -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; + +export interface TMDBMediaStatic { + getMediaDetails( + id: string, + type: MWMediaType.SERIES + ): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; +} From dfe67157d42308c5ce421ebb3a852a306d6a9716 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 20:17:42 +0200 Subject: [PATCH 02/56] preliminary refactor --- src/backend/metadata/search.ts | 48 +----- src/backend/metadata/search_new.ts | 21 --- src/backend/metadata/search_old.ts | 59 +++++++ src/backend/metadata/tmdb.ts | 1 - src/backend/metadata/types.ts | 217 ++++++++++++++++++++++++ src/backend/metadata/types_new.ts | 264 ----------------------------- src/backend/metadata/types_old.ts | 47 +++++ src/setup/config.ts | 4 + 8 files changed, 332 insertions(+), 329 deletions(-) delete mode 100644 src/backend/metadata/search_new.ts create mode 100644 src/backend/metadata/search_old.ts delete mode 100644 src/backend/metadata/types_new.ts create mode 100644 src/backend/metadata/types_old.ts diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 10cbb285..4506514a 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,14 +1,7 @@ import { SimpleCache } from "@/utils/cache"; -import { - JWContentTypes, - JWMediaResult, - JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; +import { Trakt, mediaTypeToTTV } from "./trakttv"; import { MWMediaMeta, MWQuery } from "./types"; -import { proxiedFetch } from "../helpers/fetch"; const cache = new SimpleCache(); cache.setCompare((a, b) => { @@ -16,44 +9,13 @@ cache.setCompare((a, b) => { }); cache.initialize(); -type JWSearchQuery = { - content_types: JWContentTypes[]; - page: number; - page_size: number; - query: string; -}; - -type JWPage = { - items: T[]; - page: number; - page_size: number; - total_pages: number; - total_results: number; -}; - export async function searchForMedia(query: MWQuery): Promise { if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; const { searchQuery, type } = query; - const contentType = mediaTypeToJW(type); - const body: JWSearchQuery = { - content_types: [contentType], - page: 1, - query: searchQuery, - page_size: 40, - }; + const contentType = mediaTypeToTTV(type); - const data = await proxiedFetch>( - "/content/titles/en_US/popular", - { - baseURL: JW_API_BASE, - params: { - body: JSON.stringify(body), - }, - } - ); - - const returnData = data.items.map((v) => formatJWMeta(v)); - cache.set(query, returnData, 3600); // cache for an hour - return returnData; + const results = await Trakt.search(searchQuery, contentType); + cache.set(query, results, 3600); + return results; } diff --git a/src/backend/metadata/search_new.ts b/src/backend/metadata/search_new.ts deleted file mode 100644 index 4506514a..00000000 --- a/src/backend/metadata/search_new.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; - -import { Trakt, mediaTypeToTTV } from "./trakttv"; -import { MWMediaMeta, MWQuery } from "./types"; - -const cache = new SimpleCache(); -cache.setCompare((a, b) => { - return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); -}); -cache.initialize(); - -export async function searchForMedia(query: MWQuery): Promise { - 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; -} diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts new file mode 100644 index 00000000..05be994d --- /dev/null +++ b/src/backend/metadata/search_old.ts @@ -0,0 +1,59 @@ +import { SimpleCache } from "@/utils/cache"; + +import { + JWContentTypes, + JWMediaResult, + JW_API_BASE, + formatJWMeta, + mediaTypeToJW, +} from "./justwatch"; +import { MWMediaMeta, MWQuery } from "./types_old"; +import { proxiedFetch } from "../helpers/fetch"; + +const cache = new SimpleCache(); +cache.setCompare((a, b) => { + return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); +}); +cache.initialize(); + +type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export async function searchForMedia(query: MWQuery): Promise { + if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; + const { searchQuery, type } = query; + + const contentType = mediaTypeToJW(type); + const body: JWSearchQuery = { + content_types: [contentType], + page: 1, + query: searchQuery, + page_size: 40, + }; + + const data = await proxiedFetch>( + "/content/titles/en_US/popular", + { + baseURL: JW_API_BASE, + params: { + body: JSON.stringify(body), + }, + } + ); + + const returnData = data.items.map((v) => formatJWMeta(v)); + cache.set(query, returnData, 3600); // cache for an hour + return returnData; +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 0700945b..460e13b4 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -1,7 +1,6 @@ import { conf } from "@/setup/config"; import { - DetailedMeta, MWMediaType, TMDBMediaStatic, TMDBMovieData, diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 2723fbe7..f954ff6a 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -45,3 +45,220 @@ 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; + +export interface TMDBMediaStatic { + getMediaDetails( + id: string, + type: MWMediaType.SERIES + ): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; +} diff --git a/src/backend/metadata/types_new.ts b/src/backend/metadata/types_new.ts deleted file mode 100644 index f954ff6a..00000000 --- a/src/backend/metadata/types_new.ts +++ /dev/null @@ -1,264 +0,0 @@ -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; - -export interface TMDBMediaStatic { - getMediaDetails( - id: string, - type: MWMediaType.SERIES - ): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; -} diff --git a/src/backend/metadata/types_old.ts b/src/backend/metadata/types_old.ts new file mode 100644 index 00000000..2723fbe7 --- /dev/null +++ b/src/backend/metadata/types_old.ts @@ -0,0 +1,47 @@ +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; +} diff --git a/src/setup/config.ts b/src/setup/config.ts index f1db01da..c24117bb 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -8,6 +8,7 @@ interface Config { TMDB_API_KEY: string; CORS_PROXY_URL: string; NORMAL_ROUTER: boolean; + TRAKT_CLIENT_ID: string; } export interface RuntimeConfig { @@ -18,6 +19,7 @@ export interface RuntimeConfig { TMDB_API_KEY: string; NORMAL_ROUTER: boolean; PROXY_URLS: string[]; + TRAKT_CLIENT_ID: string; } const env: Record = { @@ -28,6 +30,7 @@ const env: Record = { DISCORD_LINK: undefined, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, + TRAKT_CLIENT_ID: import.meta.env.VITE_TRAKT_CLIENT_ID, }; const alerts = [] as string[]; @@ -62,5 +65,6 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", + TRAKT_CLIENT_ID: getKey("TRAKT_CLIENT_ID"), }; } From 1eac9f886eac6b3d1ecc59ae89396f9caa9c4938 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 21:25:24 +0200 Subject: [PATCH 03/56] more refactorings --- src/backend/metadata/getmeta.ts | 8 ++--- src/backend/metadata/justwatch.ts | 42 +++++-------------------- src/backend/metadata/search_old.ts | 9 ++---- src/backend/metadata/types.ts | 49 ++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 6b3b9a30..c4771451 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,13 +1,13 @@ import { FetchError } from "ofetch"; +import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; -import { MWMediaMeta, MWMediaType } from "./types"; + MWMediaMeta, + MWMediaType, +} from "./types"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; type JWExternalIdType = diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 5c79c1e3..27c5aa4c 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -1,38 +1,10 @@ -import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types"; - -export const JW_API_BASE = "https://apis.justwatch.com"; -export const JW_IMAGE_BASE = "https://images.justwatch.com"; - -export type JWContentTypes = "movie" | "show"; - -export type JWSeasonShort = { - title: string; - id: number; - season_number: number; -}; - -export type JWEpisodeShort = { - title: string; - id: number; - episode_number: number; -}; - -export type JWMediaResult = { - title: string; - poster?: string; - id: number; - original_release_year?: number; - jw_entity_id: string; - object_type: JWContentTypes; - seasons?: JWSeasonShort[]; -}; - -export type JWSeasonMetaResult = { - title: string; - id: string; - season_number: number; - episodes: JWEpisodeShort[]; -}; +import { + JWContentTypes, + JWMediaResult, + JWSeasonMetaResult, + JW_IMAGE_BASE, +} from "./types"; +import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types_old"; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { if (type === MWMediaType.MOVIE) return "movie"; diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts index 05be994d..f12f62d2 100644 --- a/src/backend/metadata/search_old.ts +++ b/src/backend/metadata/search_old.ts @@ -1,12 +1,7 @@ import { SimpleCache } from "@/utils/cache"; -import { - JWContentTypes, - JWMediaResult, - JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; +import { formatJWMeta, mediaTypeToJW } from "./justwatch"; +import { JWContentTypes, JWMediaResult, JW_API_BASE } from "./types"; import { MWMediaMeta, MWQuery } from "./types_old"; import { proxiedFetch } from "../helpers/fetch"; diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index f954ff6a..9e87d49d 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -262,3 +262,52 @@ export interface TMDBMediaStatic { getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; } + +export type JWContentTypes = "movie" | "show"; + +export type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +export type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export const JW_API_BASE = "https://apis.justwatch.com"; +export const JW_IMAGE_BASE = "https://images.justwatch.com"; + +export type JWSeasonShort = { + title: string; + id: number; + season_number: number; +}; + +export type JWEpisodeShort = { + title: string; + id: number; + episode_number: number; +}; + +export type JWMediaResult = { + title: string; + poster?: string; + id: number; + original_release_year?: number; + jw_entity_id: string; + object_type: JWContentTypes; + seasons?: JWSeasonShort[]; +}; + +export type JWSeasonMetaResult = { + title: string; + id: string; + season_number: number; + episodes: JWEpisodeShort[]; +}; From e5ddb9816275251d242f79a52827d7b4bfdfad70 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 10:41:54 +0200 Subject: [PATCH 04/56] finish initial refactor --- src/backend/metadata/getmeta.ts | 55 +++++++++++++++++++++++++++++++++ src/backend/metadata/search.ts | 1 + src/backend/metadata/tmdb.ts | 27 ---------------- src/backend/metadata/trakttv.ts | 45 ++++++++++++++++++++------- src/backend/metadata/types.ts | 12 +++++++ 5 files changed, 101 insertions(+), 39 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index c4771451..e428199e 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,12 +1,17 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; +import { Tmdb } from "./tmdb"; +import { Trakt, formatTTVMeta } from "./trakttv"; import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, MWMediaMeta, MWMediaType, + TMDBMovieData, + TMDBShowData, + TTVSeasonMetaResult, } from "./types"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; @@ -37,6 +42,56 @@ export async function getMetaFromId( type: MWMediaType, id: string, seasonId?: string +): Promise { + const result = await Trakt.searchById(id, mediaTypeToJW(type)); + if (!result) return null; + const details = await Tmdb.getMediaDetails(id, type); + + if (!details) return null; + + let imdbId; + if (type === MWMediaType.MOVIE) { + imdbId = (details as TMDBMovieData).imdb_id ?? undefined; + } + + let seasonData: TTVSeasonMetaResult | 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, + season?.season_number ?? 1 + ); + + if (season && episodes) { + seasonData = { + id: season.id.toString(), + season_number: season.season_number, + title: season.name, + episodes, + }; + } + } + + const meta = formatTTVMeta(result, seasonData); + if (!meta) return null; + + console.log(meta); + + return { + meta, + imdbId, + tmdbId: id, + }; +} + +export async function getLegacyMetaFromId( + type: MWMediaType, + id: string, + seasonId?: string ): Promise { const queryType = mediaTypeToJW(type); diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 4506514a..8eb246b7 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -16,6 +16,7 @@ export async function searchForMedia(query: MWQuery): Promise { const contentType = mediaTypeToTTV(type); const results = await Trakt.search(searchQuery, contentType); + console.log(results[0]); cache.set(query, results, 3600); return results; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 460e13b4..3aa1821f 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -47,31 +47,4 @@ 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 getMetaFromId( - type: MWMediaType, - id: string, - seasonId?: string - ): Promise { - 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, - }; - } */ } diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts index 5fb67a17..ae50aefe 100644 --- a/src/backend/metadata/trakttv.ts +++ b/src/backend/metadata/trakttv.ts @@ -2,12 +2,13 @@ import { conf } from "@/setup/config"; import { Tmdb } from "./tmdb"; import { - DetailedMeta, MWMediaMeta, MWMediaType, MWSeasonMeta, TMDBShowData, TTVContentTypes, + TTVEpisodeResult, + TTVEpisodeShort, TTVMediaResult, TTVSearchResult, TTVSeasonMetaResult, @@ -69,14 +70,14 @@ export function formatTTVMeta( } export function TTVMediaToId(media: MWMediaMeta): string { - return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); + 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 !== "TTV") return null; + if (prefix !== "MW") return null; let mediaType; try { mediaType = TTVMediaToMediaType(type); @@ -101,7 +102,6 @@ export async function formatTTVSearchResult( media.ids.tmdb.toString(), TTVMediaToMediaType(result.type) ); - console.log(details); const seasons = type === MWMediaType.SERIES @@ -115,7 +115,7 @@ export async function formatTTVSearchResult( return { title: media.title, poster: Tmdb.getMediaPoster(details.poster_path), - id: media.ids.trakt, + id: media.ids.tmdb, original_release_year: media.year, ttv_entity_id: media.ids.slug, object_type: mediaTypeToTTV(type), @@ -155,12 +155,33 @@ export abstract class Trakt { return formatted.map((v) => formatTTVMeta(v)); } - public static async getMetaFromId( - type: MWMediaType, - id: string, - seasonId?: string - ): Promise { - console.log("getMetaFromId", type, id, seasonId); - return null; + public static async searchById( + tmdbId: string, + type: "movie" | "show" + ): Promise { + const data = await Trakt.get( + `/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 { + const data = await Trakt.get( + `/shows/${slug}/seasons/${season}` + ); + + return data.map((e) => ({ + id: e.ids.tmdb, + episode_number: e.number, + title: e.title, + })); } } diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 9e87d49d..07671e39 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -311,3 +311,15 @@ export type JWSeasonMetaResult = { season_number: number; episodes: JWEpisodeShort[]; }; + +export interface TTVEpisodeResult { + season: number; + number: number; + title: string; + ids: { + trakt: number; + tvdb: number; + imdb: string; + tmdb: number; + }; +} From baf744b5d6d82bbded5fb902027d35c61d283703 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 11:01:07 +0200 Subject: [PATCH 05/56] refactor url prefix --- src/backend/metadata/getmeta.ts | 28 ++++++++++++++++++- src/components/media/MediaCard.tsx | 4 +-- .../popouts/EpisodeSelectionPopout.tsx | 5 ++-- src/views/media/MediaView.tsx | 9 ++++-- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index e428199e..26464299 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -2,7 +2,12 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { Tmdb } from "./tmdb"; -import { Trakt, formatTTVMeta } from "./trakttv"; +import { + TTVMediaToMediaType, + Trakt, + formatTTVMeta, + mediaTypeToTTV, +} from "./trakttv"; import { JWMediaResult, JWSeasonMetaResult, @@ -137,3 +142,24 @@ export async function getLegacyMetaFromId( tmdbId, }; } + +export function MWMediaToId(media: MWMediaMeta): string { + return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); +} + +export function decodeMWId( + 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, + }; +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 22865717..3bac4d08 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { JWMediaToId } from "@/backend/metadata/justwatch"; +import { MWMediaToId } 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(JWMediaToId(props.media))}` + ? `/media/${encodeURIComponent(MWMediaToId(props.media))}` : "#"; if (canLink && props.series) link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index bd152378..63ab1c81 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -2,8 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { getMetaFromId } from "@/backend/metadata/getmeta"; -import { decodeJWId } from "@/backend/metadata/justwatch"; +import { decodeMWId, getMetaFromId } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; @@ -45,7 +44,7 @@ export function EpisodeSelectionPopout() { seasonId: sId, season: undefined, }); - reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => { + reqSeasonMeta(decodeMWId(params.media)?.id as string, sId).then((v) => { if (v?.meta.type !== MWMediaType.SERIES) return; setCurrentVisibleSeason({ seasonId: sId, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index c55211c7..1a4709e3 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -4,8 +4,11 @@ import { useTranslation } from "react-i18next"; import { useHistory, useParams } from "react-router-dom"; import { MWStream } from "@/backend/helpers/streams"; -import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; -import { decodeJWId } from "@/backend/metadata/justwatch"; +import { + DetailedMeta, + decodeMWId, + getMetaFromId, +} from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; @@ -181,7 +184,7 @@ export function MediaView() { const [selected, setSelected] = useState(null); const [exec, loading, error] = useLoading( async (mediaParams: string, seasonId?: string) => { - const data = decodeJWId(mediaParams); + const data = decodeMWId(mediaParams); if (!data) return null; return getMetaFromId(data.type, data.id, seasonId); } From e889eaebaa60560556ce38f2c22ee71fbd31c453 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:06:37 +0200 Subject: [PATCH 06/56] implement legacy url conversion --- src/backend/metadata/getmeta.ts | 15 +++++++++++++++ src/setup/App.tsx | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 26464299..b6f90f26 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -163,3 +163,18 @@ export function decodeMWId( id, }; } + +export async function convertLegacyUrl( + url: string +): Promise { + 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); + if (!meta) return undefined; + const tmdbId = meta.tmdbId; + if (!tmdbId) return undefined; + return `/media/MW-${type}-${tmdbId}`; + } + return undefined; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 992549e0..7be4d581 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -1,6 +1,13 @@ import { lazy } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; +import { + Redirect, + Route, + Switch, + useHistory, + useLocation, +} from "react-router-dom"; +import { convertLegacyUrl } from "@/backend/metadata/getmeta"; import { MWMediaType } from "@/backend/metadata/types"; import { BannerContextProvider } from "@/hooks/useBanner"; import { Layout } from "@/setup/Layout"; @@ -13,6 +20,15 @@ import { V2MigrationView } from "@/views/other/v2Migration"; import { SearchView } from "@/views/search/SearchView"; function App() { + const location = useLocation(); + const history = useHistory(); + + // Call the conversion function and redirect if necessary + convertLegacyUrl(location.pathname).then((convertedUrl) => { + if (convertedUrl) { + history.replace(convertedUrl); + } + }); return ( From a7af04530805d079f19aea5618c8989e8cab3105 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:20:33 +0200 Subject: [PATCH 07/56] refactor to initial prefix choice --- src/backend/metadata/getmeta.ts | 10 +++++----- src/components/media/MediaCard.tsx | 4 ++-- .../components/popouts/EpisodeSelectionPopout.tsx | 4 ++-- src/views/media/MediaView.tsx | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index b6f90f26..82b3c20b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -143,15 +143,15 @@ export async function getLegacyMetaFromId( }; } -export function MWMediaToId(media: MWMediaMeta): string { - return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); +export function TTVMediaToId(media: MWMediaMeta): string { + return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); } -export function decodeMWId( +export function decodeTTVId( paramId: string ): { id: string; type: MWMediaType } | null { const [prefix, type, id] = paramId.split("-", 3); - if (prefix !== "MW") return null; + if (prefix !== "TTV") return null; let mediaType; try { mediaType = TTVMediaToMediaType(type); @@ -174,7 +174,7 @@ export async function convertLegacyUrl( if (!meta) return undefined; const tmdbId = meta.tmdbId; if (!tmdbId) return undefined; - return `/media/MW-${type}-${tmdbId}`; + return `/media/TTV-${type}-${tmdbId}`; } return undefined; } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 3bac4d08..b87654a9 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { MWMediaToId } from "@/backend/metadata/getmeta"; +import { TTVMediaToId } 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(MWMediaToId(props.media))}` + ? `/media/${encodeURIComponent(TTVMediaToId(props.media))}` : "#"; if (canLink && props.series) link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 63ab1c81..6cf5d2d6 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { decodeMWId, getMetaFromId } from "@/backend/metadata/getmeta"; +import { decodeTTVId, 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(decodeMWId(params.media)?.id as string, sId).then((v) => { + reqSeasonMeta(decodeTTVId(params.media)?.id as string, sId).then((v) => { if (v?.meta.type !== MWMediaType.SERIES) return; setCurrentVisibleSeason({ seasonId: sId, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 1a4709e3..c0e81455 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -6,7 +6,7 @@ import { useHistory, useParams } from "react-router-dom"; import { MWStream } from "@/backend/helpers/streams"; import { DetailedMeta, - decodeMWId, + decodeTTVId, getMetaFromId, } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; @@ -184,7 +184,7 @@ export function MediaView() { const [selected, setSelected] = useState(null); const [exec, loading, error] = useLoading( async (mediaParams: string, seasonId?: string) => { - const data = decodeMWId(mediaParams); + const data = decodeTTVId(mediaParams); if (!data) return null; return getMetaFromId(data.type, data.id, seasonId); } From b22e3ff8c14e4eb78e52dcdeb7a61814a37c6a75 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:25:31 +0200 Subject: [PATCH 08/56] cleanup --- src/backend/metadata/justwatch.ts | 4 ++- src/backend/metadata/search_old.ts | 54 ------------------------------ src/backend/metadata/types_old.ts | 47 -------------------------- 3 files changed, 3 insertions(+), 102 deletions(-) delete mode 100644 src/backend/metadata/search_old.ts delete mode 100644 src/backend/metadata/types_old.ts diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 27c5aa4c..857ff006 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -3,8 +3,10 @@ import { JWMediaResult, JWSeasonMetaResult, JW_IMAGE_BASE, + MWMediaMeta, + MWMediaType, + MWSeasonMeta, } from "./types"; -import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types_old"; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { if (type === MWMediaType.MOVIE) return "movie"; diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts deleted file mode 100644 index f12f62d2..00000000 --- a/src/backend/metadata/search_old.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; - -import { formatJWMeta, mediaTypeToJW } from "./justwatch"; -import { JWContentTypes, JWMediaResult, JW_API_BASE } from "./types"; -import { MWMediaMeta, MWQuery } from "./types_old"; -import { proxiedFetch } from "../helpers/fetch"; - -const cache = new SimpleCache(); -cache.setCompare((a, b) => { - return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); -}); -cache.initialize(); - -type JWSearchQuery = { - content_types: JWContentTypes[]; - page: number; - page_size: number; - query: string; -}; - -type JWPage = { - items: T[]; - page: number; - page_size: number; - total_pages: number; - total_results: number; -}; - -export async function searchForMedia(query: MWQuery): Promise { - if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; - const { searchQuery, type } = query; - - const contentType = mediaTypeToJW(type); - const body: JWSearchQuery = { - content_types: [contentType], - page: 1, - query: searchQuery, - page_size: 40, - }; - - const data = await proxiedFetch>( - "/content/titles/en_US/popular", - { - baseURL: JW_API_BASE, - params: { - body: JSON.stringify(body), - }, - } - ); - - const returnData = data.items.map((v) => formatJWMeta(v)); - cache.set(query, returnData, 3600); // cache for an hour - return returnData; -} diff --git a/src/backend/metadata/types_old.ts b/src/backend/metadata/types_old.ts deleted file mode 100644 index 2723fbe7..00000000 --- a/src/backend/metadata/types_old.ts +++ /dev/null @@ -1,47 +0,0 @@ -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; -} From 3ee9ee43a54cf5f0a21eac66b333fed749663644 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 21:23:47 +0200 Subject: [PATCH 09/56] refactor everything to use tmdb exclusively --- src/backend/metadata/getmeta.ts | 61 +++--- src/backend/metadata/search.ts | 20 +- src/backend/metadata/tmdb.ts | 159 ++++++++++++++- src/backend/metadata/trakttv.ts | 187 ------------------ src/backend/metadata/types.ts | 140 +++++++++---- src/components/media/MediaCard.tsx | 4 +- .../popouts/EpisodeSelectionPopout.tsx | 4 +- src/views/media/MediaView.tsx | 4 +- 8 files changed, 315 insertions(+), 264 deletions(-) delete mode 100644 src/backend/metadata/trakttv.ts diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 82b3c20b..777fae42 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -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 { - 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; } diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 8eb246b7..31b2c682 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -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(); @@ -13,10 +18,17 @@ export async function searchForMedia(query: MWQuery): Promise { 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; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 3aa1821f..f01709bb 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -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( + `search/movie?query=${query}&include_adult=true&language=en-US&page=1` + ); + break; + case "show": + data = await Tmdb.get( + `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(`/movie/${id}`); break; - case "series": + case "show": data = await Tmdb.get(`/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 { + const data = await Tmdb.get(`/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 { + 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, + }; } diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts deleted file mode 100644 index ae50aefe..00000000 --- a/src/backend/metadata/trakttv.ts +++ /dev/null @@ -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 { - 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(url: string): Promise { - const res = await mwFetch(url, { - headers: Trakt.headers, - baseURL: Trakt.baseURL, - }); - return res; - } - - public static async search( - query: string, - type: "movie" | "show" - ): Promise { - const data = await Trakt.get( - `/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 { - const data = await Trakt.get( - `/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 { - const data = await Trakt.get( - `/shows/${slug}/seasons/${season}` - ); - - return data.map((e) => ({ - id: e.ids.tmdb, - episode_number: e.number, - title: e.title, - })); - } -} diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 07671e39..e23d9a5b 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -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; 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; +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index b87654a9..695027a2 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -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( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 6cf5d2d6..bc050bf2 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -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, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index c0e81455..7ae1c01b 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -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(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); } From c4afc372178669c0641049fb29c5da3e96f8feb8 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 21:26:58 +0200 Subject: [PATCH 10/56] cleanup --- src/setup/config.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/setup/config.ts b/src/setup/config.ts index c24117bb..f1db01da 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -8,7 +8,6 @@ interface Config { TMDB_API_KEY: string; CORS_PROXY_URL: string; NORMAL_ROUTER: boolean; - TRAKT_CLIENT_ID: string; } export interface RuntimeConfig { @@ -19,7 +18,6 @@ export interface RuntimeConfig { TMDB_API_KEY: string; NORMAL_ROUTER: boolean; PROXY_URLS: string[]; - TRAKT_CLIENT_ID: string; } const env: Record = { @@ -30,7 +28,6 @@ const env: Record = { DISCORD_LINK: undefined, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, - TRAKT_CLIENT_ID: import.meta.env.VITE_TRAKT_CLIENT_ID, }; const alerts = [] as string[]; @@ -65,6 +62,5 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", - TRAKT_CLIENT_ID: getKey("TRAKT_CLIENT_ID"), }; } From 20c4b14799d6724ae0b34d694dbc6cfa53df950e Mon Sep 17 00:00:00 2001 From: castdrian Date: Wed, 14 Jun 2023 07:48:31 +0200 Subject: [PATCH 11/56] fix movie metadata --- src/backend/metadata/getmeta.ts | 56 ++++++++++++++++++++++----------- src/backend/metadata/search.ts | 2 ++ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 777fae42..41e67772 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -43,6 +43,41 @@ export interface DetailedMeta { tmdbId?: string; } +export function fromatTMDBMetaResult( + details: TMDBShowData | TMDBMovieData, + type: MWMediaType +): TMDBMediaResult | undefined { + let tmdbmeta; + if (type === MWMediaType.MOVIE) { + tmdbmeta = { + id: details.id, + title: (details as TMDBMovieData).title, + object_type: mediaTypeToTMDB(type), + poster: (details as TMDBMovieData).poster_path ?? undefined, + original_release_year: Number( + (details as TMDBMovieData).release_date?.split("-")[0] + ), + }; + } + if (type === MWMediaType.SERIES) { + tmdbmeta = { + id: details.id, + 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: Number( + (details as TMDBShowData).first_air_date?.split("-")[0] + ), + }; + } + return tmdbmeta; +} + export async function getMetaFromId( type: MWMediaType, id: string, @@ -79,25 +114,8 @@ export async function getMetaFromId( } } - 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]), - }; - + const tmdbmeta = fromatTMDBMetaResult(details, type); + if (!tmdbmeta) return null; const meta = formatTMDBMeta(tmdbmeta, seasonData); if (!meta) return null; diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 31b2c682..7d06ab2c 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -29,6 +29,8 @@ export async function searchForMedia(query: MWQuery): Promise { }) ); + console.log(results[0]); + cache.set(query, results, 3600); return results; } From 5d56b847c690a4a8c988026a9f33b6e2c4a072a3 Mon Sep 17 00:00:00 2001 From: castdrian Date: Wed, 14 Jun 2023 07:52:04 +0200 Subject: [PATCH 12/56] cleanup --- src/backend/metadata/search.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 7d06ab2c..31b2c682 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -29,8 +29,6 @@ export async function searchForMedia(query: MWQuery): Promise { }) ); - console.log(results[0]); - cache.set(query, results, 3600); return results; } From 74cc50cfa2d64013e485b48862d3f3f3a673fd9f Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 08:30:05 +0200 Subject: [PATCH 13/56] show poster in bookmarks --- src/backend/metadata/getmeta.ts | 8 ++++++-- src/components/media/MediaCard.tsx | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 41e67772..bba3948e 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -53,7 +53,9 @@ export function fromatTMDBMetaResult( id: details.id, title: (details as TMDBMovieData).title, object_type: mediaTypeToTMDB(type), - poster: (details as TMDBMovieData).poster_path ?? undefined, + poster: + Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? + undefined, original_release_year: Number( (details as TMDBMovieData).release_date?.split("-")[0] ), @@ -69,7 +71,9 @@ export function fromatTMDBMetaResult( season_number: v.season_number, title: v.name, })), - poster: (details as TMDBMovieData).poster_path ?? undefined, + poster: + Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? + undefined, original_release_year: Number( (details as TMDBShowData).first_air_date?.split("-")[0] ), diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 695027a2..ece6d293 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { TMDBMediaToId } from "@/backend/metadata/getmeta"; +import { Tmdb } from "@/backend/metadata/tmdb"; import { MWMediaMeta } from "@/backend/metadata/types"; import { DotList } from "@/components/text/DotList"; @@ -55,7 +56,9 @@ function MediaCardContent({ closable ? "" : "group-hover:rounded-lg", ].join(" ")} style={{ - backgroundImage: media.poster ? `url(${media.poster})` : undefined, + backgroundImage: media.poster + ? `url(${Tmdb.getMediaPoster(media.poster)})` + : undefined, }} > {series ? ( From 28d2dd0e89393fb29c6b363dd6445fb66c546959 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 08:30:57 +0200 Subject: [PATCH 14/56] set adult false in query --- src/backend/metadata/tmdb.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index f01709bb..aed31c1d 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -120,12 +120,12 @@ export abstract class Tmdb { switch (type) { case "movie": data = await Tmdb.get( - `search/movie?query=${query}&include_adult=true&language=en-US&page=1` + `search/movie?query=${query}&include_adult=false&language=en-US&page=1` ); break; case "show": data = await Tmdb.get( - `search/tv?query=${query}&include_adult=true&language=en-US&page=1` + `search/tv?query=${query}&include_adult=false&language=en-US&page=1` ); break; default: From 330cbf2d9e5dfbe00797b118c621252be14aa42d Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 11:06:24 +0200 Subject: [PATCH 15/56] undo duplicate path --- src/backend/metadata/getmeta.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index bba3948e..41e67772 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -53,9 +53,7 @@ export function fromatTMDBMetaResult( id: details.id, title: (details as TMDBMovieData).title, object_type: mediaTypeToTMDB(type), - poster: - Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? - undefined, + poster: (details as TMDBMovieData).poster_path ?? undefined, original_release_year: Number( (details as TMDBMovieData).release_date?.split("-")[0] ), @@ -71,9 +69,7 @@ export function fromatTMDBMetaResult( season_number: v.season_number, title: v.name, })), - poster: - Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? - undefined, + poster: (details as TMDBMovieData).poster_path ?? undefined, original_release_year: Number( (details as TMDBShowData).first_air_date?.split("-")[0] ), From d96165518670d2c3ce8e1ef1a753ecc75956ba98 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 22:13:19 +0200 Subject: [PATCH 16/56] fix typo 'cause I can't type --- src/backend/metadata/getmeta.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 41e67772..a5246fcf 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -43,7 +43,7 @@ export interface DetailedMeta { tmdbId?: string; } -export function fromatTMDBMetaResult( +export function formatTMDBMetaResult( details: TMDBShowData | TMDBMovieData, type: MWMediaType ): TMDBMediaResult | undefined { @@ -114,7 +114,7 @@ export async function getMetaFromId( } } - const tmdbmeta = fromatTMDBMetaResult(details, type); + const tmdbmeta = formatTMDBMetaResult(details, type); if (!tmdbmeta) return null; const meta = formatTMDBMeta(tmdbmeta, seasonData); if (!meta) return null; From ad263916454d31847a56456f040218f340817747 Mon Sep 17 00:00:00 2001 From: castdrian Date: Fri, 16 Jun 2023 11:18:32 +0200 Subject: [PATCH 17/56] use external ids endpoint for imdb ids --- src/backend/metadata/getmeta.ts | 6 ++---- src/backend/metadata/tmdb.ts | 25 +++++++++++++++++++++++++ src/backend/metadata/types.ts | 24 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index a5246fcf..6868c7e4 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -87,10 +87,8 @@ export async function getMetaFromId( if (!details) return null; - let imdbId; - if (type === MWMediaType.MOVIE) { - imdbId = (details as TMDBMovieData).imdb_id ?? undefined; - } + const externalIds = await Tmdb.getExternalIds(id, mediaTypeToTMDB(type)); + const imdbId = externalIds.imdb_id ?? undefined; let seasonData: TMDBSeasonMetaResult | undefined; diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index aed31c1d..e22e86e7 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -6,15 +6,18 @@ import { MWSeasonMeta, TMDBContentTypes, TMDBEpisodeShort, + TMDBExternalIds, TMDBMediaResult, TMDBMediaStatic, TMDBMovieData, + TMDBMovieExternalIds, TMDBMovieResponse, TMDBMovieResult, TMDBSearchResultStatic, TMDBSeason, TMDBSeasonMetaResult, TMDBShowData, + TMDBShowExternalIds, TMDBShowResponse, TMDBShowResult, } from "./types"; @@ -170,6 +173,28 @@ export abstract class Tmdb { title: e.name, })); } + + public static async getExternalIds( + id: string, + type: TMDBContentTypes + ): Promise { + let data; + + switch (type) { + case "movie": + data = await Tmdb.get( + `/movie/${id}/external_ids` + ); + break; + case "show": + data = await Tmdb.get(`/tv/${id}/external_ids`); + break; + default: + throw new Error("Invalid media type"); + } + + return data; + } } export async function formatTMDBSearchResult( diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index e23d9a5b..fa7a7ef0 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -379,3 +379,27 @@ export interface TMDBSeason { poster_path: string | null; season_number: number; } + +export interface TMDBShowExternalIds { + id: number; + imdb_id: null | string; + freebase_mid: null | string; + freebase_id: null | string; + tvdb_id: number; + tvrage_id: null | string; + wikidata_id: null | string; + facebook_id: null | string; + instagram_id: null | string; + twitter_id: null | string; +} + +export interface TMDBMovieExternalIds { + id: number; + imdb_id: null | string; + wikidata_id: null | string; + facebook_id: null | string; + instagram_id: null | string; + twitter_id: null | string; +} + +export type TMDBExternalIds = TMDBShowExternalIds | TMDBMovieExternalIds; From 70852773f94c30ab47d9737bff8a9875f514c564 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 20:06:46 +0200 Subject: [PATCH 18/56] partial refactor --- src/backend/metadata/search_new.ts | 21 +++ src/backend/metadata/tmdb.ts | 78 +++++++++ src/backend/metadata/trakttv.ts | 166 ++++++++++++++++++ src/backend/metadata/types_new.ts | 264 +++++++++++++++++++++++++++++ 4 files changed, 529 insertions(+) create mode 100644 src/backend/metadata/search_new.ts create mode 100644 src/backend/metadata/tmdb.ts create mode 100644 src/backend/metadata/trakttv.ts create mode 100644 src/backend/metadata/types_new.ts diff --git a/src/backend/metadata/search_new.ts b/src/backend/metadata/search_new.ts new file mode 100644 index 00000000..4506514a --- /dev/null +++ b/src/backend/metadata/search_new.ts @@ -0,0 +1,21 @@ +import { SimpleCache } from "@/utils/cache"; + +import { Trakt, mediaTypeToTTV } from "./trakttv"; +import { MWMediaMeta, MWQuery } from "./types"; + +const cache = new SimpleCache(); +cache.setCompare((a, b) => { + return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); +}); +cache.initialize(); + +export async function searchForMedia(query: MWQuery): Promise { + 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; +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts new file mode 100644 index 00000000..0700945b --- /dev/null +++ b/src/backend/metadata/tmdb.ts @@ -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(url: string): Promise { + const res = await mwFetch(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(`/movie/${id}`); + break; + case "series": + data = await Tmdb.get(`/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 { + 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, + }; + } */ +} diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts new file mode 100644 index 00000000..5fb67a17 --- /dev/null +++ b/src/backend/metadata/trakttv.ts @@ -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 { + 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(url: string): Promise { + const res = await mwFetch(url, { + headers: Trakt.headers, + baseURL: Trakt.baseURL, + }); + return res; + } + + public static async search( + query: string, + type: "movie" | "show" + ): Promise { + const data = await Trakt.get( + `/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 { + console.log("getMetaFromId", type, id, seasonId); + return null; + } +} diff --git a/src/backend/metadata/types_new.ts b/src/backend/metadata/types_new.ts new file mode 100644 index 00000000..f954ff6a --- /dev/null +++ b/src/backend/metadata/types_new.ts @@ -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; + +export interface TMDBMediaStatic { + getMediaDetails( + id: string, + type: MWMediaType.SERIES + ): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; +} From 63f26b81dec2bab16f6d19c0d502a05753d85497 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 20:17:42 +0200 Subject: [PATCH 19/56] preliminary refactor --- src/backend/metadata/search.ts | 48 +----- src/backend/metadata/search_new.ts | 21 --- src/backend/metadata/search_old.ts | 59 +++++++ src/backend/metadata/tmdb.ts | 1 - src/backend/metadata/types.ts | 217 ++++++++++++++++++++++++ src/backend/metadata/types_new.ts | 264 ----------------------------- src/backend/metadata/types_old.ts | 47 +++++ src/setup/config.ts | 4 + 8 files changed, 332 insertions(+), 329 deletions(-) delete mode 100644 src/backend/metadata/search_new.ts create mode 100644 src/backend/metadata/search_old.ts delete mode 100644 src/backend/metadata/types_new.ts create mode 100644 src/backend/metadata/types_old.ts diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 10cbb285..4506514a 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,14 +1,7 @@ import { SimpleCache } from "@/utils/cache"; -import { - JWContentTypes, - JWMediaResult, - JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; +import { Trakt, mediaTypeToTTV } from "./trakttv"; import { MWMediaMeta, MWQuery } from "./types"; -import { proxiedFetch } from "../helpers/fetch"; const cache = new SimpleCache(); cache.setCompare((a, b) => { @@ -16,44 +9,13 @@ cache.setCompare((a, b) => { }); cache.initialize(); -type JWSearchQuery = { - content_types: JWContentTypes[]; - page: number; - page_size: number; - query: string; -}; - -type JWPage = { - items: T[]; - page: number; - page_size: number; - total_pages: number; - total_results: number; -}; - export async function searchForMedia(query: MWQuery): Promise { if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; const { searchQuery, type } = query; - const contentType = mediaTypeToJW(type); - const body: JWSearchQuery = { - content_types: [contentType], - page: 1, - query: searchQuery, - page_size: 40, - }; + const contentType = mediaTypeToTTV(type); - const data = await proxiedFetch>( - "/content/titles/en_US/popular", - { - baseURL: JW_API_BASE, - params: { - body: JSON.stringify(body), - }, - } - ); - - const returnData = data.items.map((v) => formatJWMeta(v)); - cache.set(query, returnData, 3600); // cache for an hour - return returnData; + const results = await Trakt.search(searchQuery, contentType); + cache.set(query, results, 3600); + return results; } diff --git a/src/backend/metadata/search_new.ts b/src/backend/metadata/search_new.ts deleted file mode 100644 index 4506514a..00000000 --- a/src/backend/metadata/search_new.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; - -import { Trakt, mediaTypeToTTV } from "./trakttv"; -import { MWMediaMeta, MWQuery } from "./types"; - -const cache = new SimpleCache(); -cache.setCompare((a, b) => { - return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); -}); -cache.initialize(); - -export async function searchForMedia(query: MWQuery): Promise { - 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; -} diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts new file mode 100644 index 00000000..05be994d --- /dev/null +++ b/src/backend/metadata/search_old.ts @@ -0,0 +1,59 @@ +import { SimpleCache } from "@/utils/cache"; + +import { + JWContentTypes, + JWMediaResult, + JW_API_BASE, + formatJWMeta, + mediaTypeToJW, +} from "./justwatch"; +import { MWMediaMeta, MWQuery } from "./types_old"; +import { proxiedFetch } from "../helpers/fetch"; + +const cache = new SimpleCache(); +cache.setCompare((a, b) => { + return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); +}); +cache.initialize(); + +type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export async function searchForMedia(query: MWQuery): Promise { + if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; + const { searchQuery, type } = query; + + const contentType = mediaTypeToJW(type); + const body: JWSearchQuery = { + content_types: [contentType], + page: 1, + query: searchQuery, + page_size: 40, + }; + + const data = await proxiedFetch>( + "/content/titles/en_US/popular", + { + baseURL: JW_API_BASE, + params: { + body: JSON.stringify(body), + }, + } + ); + + const returnData = data.items.map((v) => formatJWMeta(v)); + cache.set(query, returnData, 3600); // cache for an hour + return returnData; +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 0700945b..460e13b4 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -1,7 +1,6 @@ import { conf } from "@/setup/config"; import { - DetailedMeta, MWMediaType, TMDBMediaStatic, TMDBMovieData, diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 2723fbe7..f954ff6a 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -45,3 +45,220 @@ 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; + +export interface TMDBMediaStatic { + getMediaDetails( + id: string, + type: MWMediaType.SERIES + ): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; + getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; +} diff --git a/src/backend/metadata/types_new.ts b/src/backend/metadata/types_new.ts deleted file mode 100644 index f954ff6a..00000000 --- a/src/backend/metadata/types_new.ts +++ /dev/null @@ -1,264 +0,0 @@ -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; - -export interface TMDBMediaStatic { - getMediaDetails( - id: string, - type: MWMediaType.SERIES - ): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; -} diff --git a/src/backend/metadata/types_old.ts b/src/backend/metadata/types_old.ts new file mode 100644 index 00000000..2723fbe7 --- /dev/null +++ b/src/backend/metadata/types_old.ts @@ -0,0 +1,47 @@ +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; +} diff --git a/src/setup/config.ts b/src/setup/config.ts index f1db01da..c24117bb 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -8,6 +8,7 @@ interface Config { TMDB_API_KEY: string; CORS_PROXY_URL: string; NORMAL_ROUTER: boolean; + TRAKT_CLIENT_ID: string; } export interface RuntimeConfig { @@ -18,6 +19,7 @@ export interface RuntimeConfig { TMDB_API_KEY: string; NORMAL_ROUTER: boolean; PROXY_URLS: string[]; + TRAKT_CLIENT_ID: string; } const env: Record = { @@ -28,6 +30,7 @@ const env: Record = { DISCORD_LINK: undefined, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, + TRAKT_CLIENT_ID: import.meta.env.VITE_TRAKT_CLIENT_ID, }; const alerts = [] as string[]; @@ -62,5 +65,6 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", + TRAKT_CLIENT_ID: getKey("TRAKT_CLIENT_ID"), }; } From c17f8a15e8d8b89a488630b267efc86ab181d04a Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 12 Jun 2023 21:25:24 +0200 Subject: [PATCH 20/56] more refactorings --- src/backend/metadata/getmeta.ts | 8 ++--- src/backend/metadata/justwatch.ts | 42 +++++-------------------- src/backend/metadata/search_old.ts | 9 ++---- src/backend/metadata/types.ts | 49 ++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 6b3b9a30..c4771451 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,13 +1,13 @@ import { FetchError } from "ofetch"; +import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; -import { MWMediaMeta, MWMediaType } from "./types"; + MWMediaMeta, + MWMediaType, +} from "./types"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; type JWExternalIdType = diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 5c79c1e3..27c5aa4c 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -1,38 +1,10 @@ -import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types"; - -export const JW_API_BASE = "https://apis.justwatch.com"; -export const JW_IMAGE_BASE = "https://images.justwatch.com"; - -export type JWContentTypes = "movie" | "show"; - -export type JWSeasonShort = { - title: string; - id: number; - season_number: number; -}; - -export type JWEpisodeShort = { - title: string; - id: number; - episode_number: number; -}; - -export type JWMediaResult = { - title: string; - poster?: string; - id: number; - original_release_year?: number; - jw_entity_id: string; - object_type: JWContentTypes; - seasons?: JWSeasonShort[]; -}; - -export type JWSeasonMetaResult = { - title: string; - id: string; - season_number: number; - episodes: JWEpisodeShort[]; -}; +import { + JWContentTypes, + JWMediaResult, + JWSeasonMetaResult, + JW_IMAGE_BASE, +} from "./types"; +import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types_old"; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { if (type === MWMediaType.MOVIE) return "movie"; diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts index 05be994d..f12f62d2 100644 --- a/src/backend/metadata/search_old.ts +++ b/src/backend/metadata/search_old.ts @@ -1,12 +1,7 @@ import { SimpleCache } from "@/utils/cache"; -import { - JWContentTypes, - JWMediaResult, - JW_API_BASE, - formatJWMeta, - mediaTypeToJW, -} from "./justwatch"; +import { formatJWMeta, mediaTypeToJW } from "./justwatch"; +import { JWContentTypes, JWMediaResult, JW_API_BASE } from "./types"; import { MWMediaMeta, MWQuery } from "./types_old"; import { proxiedFetch } from "../helpers/fetch"; diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index f954ff6a..9e87d49d 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -262,3 +262,52 @@ export interface TMDBMediaStatic { getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise; getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise; } + +export type JWContentTypes = "movie" | "show"; + +export type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +export type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export const JW_API_BASE = "https://apis.justwatch.com"; +export const JW_IMAGE_BASE = "https://images.justwatch.com"; + +export type JWSeasonShort = { + title: string; + id: number; + season_number: number; +}; + +export type JWEpisodeShort = { + title: string; + id: number; + episode_number: number; +}; + +export type JWMediaResult = { + title: string; + poster?: string; + id: number; + original_release_year?: number; + jw_entity_id: string; + object_type: JWContentTypes; + seasons?: JWSeasonShort[]; +}; + +export type JWSeasonMetaResult = { + title: string; + id: string; + season_number: number; + episodes: JWEpisodeShort[]; +}; From 3af98373fbafc7d979492fb87d21033bd2241890 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 10:41:54 +0200 Subject: [PATCH 21/56] finish initial refactor --- src/backend/metadata/getmeta.ts | 55 +++++++++++++++++++++++++++++++++ src/backend/metadata/search.ts | 1 + src/backend/metadata/tmdb.ts | 27 ---------------- src/backend/metadata/trakttv.ts | 45 ++++++++++++++++++++------- src/backend/metadata/types.ts | 12 +++++++ 5 files changed, 101 insertions(+), 39 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index c4771451..e428199e 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,12 +1,17 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; +import { Tmdb } from "./tmdb"; +import { Trakt, formatTTVMeta } from "./trakttv"; import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, MWMediaMeta, MWMediaType, + TMDBMovieData, + TMDBShowData, + TTVSeasonMetaResult, } from "./types"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; @@ -37,6 +42,56 @@ export async function getMetaFromId( type: MWMediaType, id: string, seasonId?: string +): Promise { + const result = await Trakt.searchById(id, mediaTypeToJW(type)); + if (!result) return null; + const details = await Tmdb.getMediaDetails(id, type); + + if (!details) return null; + + let imdbId; + if (type === MWMediaType.MOVIE) { + imdbId = (details as TMDBMovieData).imdb_id ?? undefined; + } + + let seasonData: TTVSeasonMetaResult | 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, + season?.season_number ?? 1 + ); + + if (season && episodes) { + seasonData = { + id: season.id.toString(), + season_number: season.season_number, + title: season.name, + episodes, + }; + } + } + + const meta = formatTTVMeta(result, seasonData); + if (!meta) return null; + + console.log(meta); + + return { + meta, + imdbId, + tmdbId: id, + }; +} + +export async function getLegacyMetaFromId( + type: MWMediaType, + id: string, + seasonId?: string ): Promise { const queryType = mediaTypeToJW(type); diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 4506514a..8eb246b7 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -16,6 +16,7 @@ export async function searchForMedia(query: MWQuery): Promise { const contentType = mediaTypeToTTV(type); const results = await Trakt.search(searchQuery, contentType); + console.log(results[0]); cache.set(query, results, 3600); return results; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 460e13b4..3aa1821f 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -47,31 +47,4 @@ 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 getMetaFromId( - type: MWMediaType, - id: string, - seasonId?: string - ): Promise { - 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, - }; - } */ } diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts index 5fb67a17..ae50aefe 100644 --- a/src/backend/metadata/trakttv.ts +++ b/src/backend/metadata/trakttv.ts @@ -2,12 +2,13 @@ import { conf } from "@/setup/config"; import { Tmdb } from "./tmdb"; import { - DetailedMeta, MWMediaMeta, MWMediaType, MWSeasonMeta, TMDBShowData, TTVContentTypes, + TTVEpisodeResult, + TTVEpisodeShort, TTVMediaResult, TTVSearchResult, TTVSeasonMetaResult, @@ -69,14 +70,14 @@ export function formatTTVMeta( } export function TTVMediaToId(media: MWMediaMeta): string { - return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); + 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 !== "TTV") return null; + if (prefix !== "MW") return null; let mediaType; try { mediaType = TTVMediaToMediaType(type); @@ -101,7 +102,6 @@ export async function formatTTVSearchResult( media.ids.tmdb.toString(), TTVMediaToMediaType(result.type) ); - console.log(details); const seasons = type === MWMediaType.SERIES @@ -115,7 +115,7 @@ export async function formatTTVSearchResult( return { title: media.title, poster: Tmdb.getMediaPoster(details.poster_path), - id: media.ids.trakt, + id: media.ids.tmdb, original_release_year: media.year, ttv_entity_id: media.ids.slug, object_type: mediaTypeToTTV(type), @@ -155,12 +155,33 @@ export abstract class Trakt { return formatted.map((v) => formatTTVMeta(v)); } - public static async getMetaFromId( - type: MWMediaType, - id: string, - seasonId?: string - ): Promise { - console.log("getMetaFromId", type, id, seasonId); - return null; + public static async searchById( + tmdbId: string, + type: "movie" | "show" + ): Promise { + const data = await Trakt.get( + `/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 { + const data = await Trakt.get( + `/shows/${slug}/seasons/${season}` + ); + + return data.map((e) => ({ + id: e.ids.tmdb, + episode_number: e.number, + title: e.title, + })); } } diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 9e87d49d..07671e39 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -311,3 +311,15 @@ export type JWSeasonMetaResult = { season_number: number; episodes: JWEpisodeShort[]; }; + +export interface TTVEpisodeResult { + season: number; + number: number; + title: string; + ids: { + trakt: number; + tvdb: number; + imdb: string; + tmdb: number; + }; +} From 70f835538683e3dba953b443a2b17538e9567764 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 11:01:07 +0200 Subject: [PATCH 22/56] refactor url prefix --- src/backend/metadata/getmeta.ts | 28 ++++++++++++++++++- src/components/media/MediaCard.tsx | 4 +-- .../popouts/EpisodeSelectionPopout.tsx | 5 ++-- src/views/media/MediaView.tsx | 9 ++++-- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index e428199e..26464299 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -2,7 +2,12 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { Tmdb } from "./tmdb"; -import { Trakt, formatTTVMeta } from "./trakttv"; +import { + TTVMediaToMediaType, + Trakt, + formatTTVMeta, + mediaTypeToTTV, +} from "./trakttv"; import { JWMediaResult, JWSeasonMetaResult, @@ -137,3 +142,24 @@ export async function getLegacyMetaFromId( tmdbId, }; } + +export function MWMediaToId(media: MWMediaMeta): string { + return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); +} + +export function decodeMWId( + 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, + }; +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 22865717..3bac4d08 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { JWMediaToId } from "@/backend/metadata/justwatch"; +import { MWMediaToId } 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(JWMediaToId(props.media))}` + ? `/media/${encodeURIComponent(MWMediaToId(props.media))}` : "#"; if (canLink && props.series) link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index c80045bd..3d0b431b 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -2,8 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { getMetaFromId } from "@/backend/metadata/getmeta"; -import { decodeJWId } from "@/backend/metadata/justwatch"; +import { decodeMWId, getMetaFromId } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; @@ -45,7 +44,7 @@ export function EpisodeSelectionPopout() { seasonId: sId, season: undefined, }); - reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => { + reqSeasonMeta(decodeMWId(params.media)?.id as string, sId).then((v) => { if (v?.meta.type !== MWMediaType.SERIES) return; setCurrentVisibleSeason({ seasonId: sId, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index c55211c7..1a4709e3 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -4,8 +4,11 @@ import { useTranslation } from "react-i18next"; import { useHistory, useParams } from "react-router-dom"; import { MWStream } from "@/backend/helpers/streams"; -import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; -import { decodeJWId } from "@/backend/metadata/justwatch"; +import { + DetailedMeta, + decodeMWId, + getMetaFromId, +} from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; @@ -181,7 +184,7 @@ export function MediaView() { const [selected, setSelected] = useState(null); const [exec, loading, error] = useLoading( async (mediaParams: string, seasonId?: string) => { - const data = decodeJWId(mediaParams); + const data = decodeMWId(mediaParams); if (!data) return null; return getMetaFromId(data.type, data.id, seasonId); } From 879271c23976126b41a73f439f01cacc5140edbf Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:06:37 +0200 Subject: [PATCH 23/56] implement legacy url conversion --- src/backend/metadata/getmeta.ts | 15 +++++++++++++++ src/setup/App.tsx | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 26464299..b6f90f26 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -163,3 +163,18 @@ export function decodeMWId( id, }; } + +export async function convertLegacyUrl( + url: string +): Promise { + 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); + if (!meta) return undefined; + const tmdbId = meta.tmdbId; + if (!tmdbId) return undefined; + return `/media/MW-${type}-${tmdbId}`; + } + return undefined; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 992549e0..7be4d581 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -1,6 +1,13 @@ import { lazy } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; +import { + Redirect, + Route, + Switch, + useHistory, + useLocation, +} from "react-router-dom"; +import { convertLegacyUrl } from "@/backend/metadata/getmeta"; import { MWMediaType } from "@/backend/metadata/types"; import { BannerContextProvider } from "@/hooks/useBanner"; import { Layout } from "@/setup/Layout"; @@ -13,6 +20,15 @@ import { V2MigrationView } from "@/views/other/v2Migration"; import { SearchView } from "@/views/search/SearchView"; function App() { + const location = useLocation(); + const history = useHistory(); + + // Call the conversion function and redirect if necessary + convertLegacyUrl(location.pathname).then((convertedUrl) => { + if (convertedUrl) { + history.replace(convertedUrl); + } + }); return ( From b5c330d4e3cd4b4aa77b3f3dc7cfcba02a785f7b Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:20:33 +0200 Subject: [PATCH 24/56] refactor to initial prefix choice --- src/backend/metadata/getmeta.ts | 10 +++++----- src/components/media/MediaCard.tsx | 4 ++-- .../components/popouts/EpisodeSelectionPopout.tsx | 4 ++-- src/views/media/MediaView.tsx | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index b6f90f26..82b3c20b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -143,15 +143,15 @@ export async function getLegacyMetaFromId( }; } -export function MWMediaToId(media: MWMediaMeta): string { - return ["MW", mediaTypeToTTV(media.type), media.id].join("-"); +export function TTVMediaToId(media: MWMediaMeta): string { + return ["TTV", mediaTypeToTTV(media.type), media.id].join("-"); } -export function decodeMWId( +export function decodeTTVId( paramId: string ): { id: string; type: MWMediaType } | null { const [prefix, type, id] = paramId.split("-", 3); - if (prefix !== "MW") return null; + if (prefix !== "TTV") return null; let mediaType; try { mediaType = TTVMediaToMediaType(type); @@ -174,7 +174,7 @@ export async function convertLegacyUrl( if (!meta) return undefined; const tmdbId = meta.tmdbId; if (!tmdbId) return undefined; - return `/media/MW-${type}-${tmdbId}`; + return `/media/TTV-${type}-${tmdbId}`; } return undefined; } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 3bac4d08..b87654a9 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { MWMediaToId } from "@/backend/metadata/getmeta"; +import { TTVMediaToId } 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(MWMediaToId(props.media))}` + ? `/media/${encodeURIComponent(TTVMediaToId(props.media))}` : "#"; if (canLink && props.series) link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 3d0b431b..7ea6c5a3 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -import { decodeMWId, getMetaFromId } from "@/backend/metadata/getmeta"; +import { decodeTTVId, 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(decodeMWId(params.media)?.id as string, sId).then((v) => { + reqSeasonMeta(decodeTTVId(params.media)?.id as string, sId).then((v) => { if (v?.meta.type !== MWMediaType.SERIES) return; setCurrentVisibleSeason({ seasonId: sId, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 1a4709e3..c0e81455 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -6,7 +6,7 @@ import { useHistory, useParams } from "react-router-dom"; import { MWStream } from "@/backend/helpers/streams"; import { DetailedMeta, - decodeMWId, + decodeTTVId, getMetaFromId, } from "@/backend/metadata/getmeta"; import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; @@ -184,7 +184,7 @@ export function MediaView() { const [selected, setSelected] = useState(null); const [exec, loading, error] = useLoading( async (mediaParams: string, seasonId?: string) => { - const data = decodeMWId(mediaParams); + const data = decodeTTVId(mediaParams); if (!data) return null; return getMetaFromId(data.type, data.id, seasonId); } From 8da155ba2bfad94160469118804665e34a80c8a1 Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 14:25:31 +0200 Subject: [PATCH 25/56] cleanup --- src/backend/metadata/justwatch.ts | 4 ++- src/backend/metadata/search_old.ts | 54 ------------------------------ src/backend/metadata/types_old.ts | 47 -------------------------- 3 files changed, 3 insertions(+), 102 deletions(-) delete mode 100644 src/backend/metadata/search_old.ts delete mode 100644 src/backend/metadata/types_old.ts diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 27c5aa4c..857ff006 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -3,8 +3,10 @@ import { JWMediaResult, JWSeasonMetaResult, JW_IMAGE_BASE, + MWMediaMeta, + MWMediaType, + MWSeasonMeta, } from "./types"; -import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types_old"; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { if (type === MWMediaType.MOVIE) return "movie"; diff --git a/src/backend/metadata/search_old.ts b/src/backend/metadata/search_old.ts deleted file mode 100644 index f12f62d2..00000000 --- a/src/backend/metadata/search_old.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; - -import { formatJWMeta, mediaTypeToJW } from "./justwatch"; -import { JWContentTypes, JWMediaResult, JW_API_BASE } from "./types"; -import { MWMediaMeta, MWQuery } from "./types_old"; -import { proxiedFetch } from "../helpers/fetch"; - -const cache = new SimpleCache(); -cache.setCompare((a, b) => { - return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); -}); -cache.initialize(); - -type JWSearchQuery = { - content_types: JWContentTypes[]; - page: number; - page_size: number; - query: string; -}; - -type JWPage = { - items: T[]; - page: number; - page_size: number; - total_pages: number; - total_results: number; -}; - -export async function searchForMedia(query: MWQuery): Promise { - if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; - const { searchQuery, type } = query; - - const contentType = mediaTypeToJW(type); - const body: JWSearchQuery = { - content_types: [contentType], - page: 1, - query: searchQuery, - page_size: 40, - }; - - const data = await proxiedFetch>( - "/content/titles/en_US/popular", - { - baseURL: JW_API_BASE, - params: { - body: JSON.stringify(body), - }, - } - ); - - const returnData = data.items.map((v) => formatJWMeta(v)); - cache.set(query, returnData, 3600); // cache for an hour - return returnData; -} diff --git a/src/backend/metadata/types_old.ts b/src/backend/metadata/types_old.ts deleted file mode 100644 index 2723fbe7..00000000 --- a/src/backend/metadata/types_old.ts +++ /dev/null @@ -1,47 +0,0 @@ -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; -} From 46bd20f71866241a09756d46b111f1ef416fe02b Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 21:23:47 +0200 Subject: [PATCH 26/56] refactor everything to use tmdb exclusively --- src/backend/metadata/getmeta.ts | 61 +++--- src/backend/metadata/search.ts | 20 +- src/backend/metadata/tmdb.ts | 159 ++++++++++++++- src/backend/metadata/trakttv.ts | 187 ------------------ src/backend/metadata/types.ts | 140 +++++++++---- src/components/media/MediaCard.tsx | 4 +- .../popouts/EpisodeSelectionPopout.tsx | 4 +- src/views/media/MediaView.tsx | 4 +- 8 files changed, 315 insertions(+), 264 deletions(-) delete mode 100644 src/backend/metadata/trakttv.ts diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 82b3c20b..777fae42 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -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 { - 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; } diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 8eb246b7..31b2c682 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -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(); @@ -13,10 +18,17 @@ export async function searchForMedia(query: MWQuery): Promise { 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; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 3aa1821f..f01709bb 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -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( + `search/movie?query=${query}&include_adult=true&language=en-US&page=1` + ); + break; + case "show": + data = await Tmdb.get( + `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(`/movie/${id}`); break; - case "series": + case "show": data = await Tmdb.get(`/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 { + const data = await Tmdb.get(`/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 { + 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, + }; } diff --git a/src/backend/metadata/trakttv.ts b/src/backend/metadata/trakttv.ts deleted file mode 100644 index ae50aefe..00000000 --- a/src/backend/metadata/trakttv.ts +++ /dev/null @@ -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 { - 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(url: string): Promise { - const res = await mwFetch(url, { - headers: Trakt.headers, - baseURL: Trakt.baseURL, - }); - return res; - } - - public static async search( - query: string, - type: "movie" | "show" - ): Promise { - const data = await Trakt.get( - `/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 { - const data = await Trakt.get( - `/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 { - const data = await Trakt.get( - `/shows/${slug}/seasons/${season}` - ); - - return data.map((e) => ({ - id: e.ids.tmdb, - episode_number: e.number, - title: e.title, - })); - } -} diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index 07671e39..e23d9a5b 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -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; 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; +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index b87654a9..695027a2 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -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( diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 7ea6c5a3..ce45c318 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -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, diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index c0e81455..7ae1c01b 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -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(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); } From 763de37e9eaa4f6933e631fcd4b06c5703d37a2e Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 13 Jun 2023 21:26:58 +0200 Subject: [PATCH 27/56] cleanup --- src/setup/config.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/setup/config.ts b/src/setup/config.ts index c24117bb..f1db01da 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -8,7 +8,6 @@ interface Config { TMDB_API_KEY: string; CORS_PROXY_URL: string; NORMAL_ROUTER: boolean; - TRAKT_CLIENT_ID: string; } export interface RuntimeConfig { @@ -19,7 +18,6 @@ export interface RuntimeConfig { TMDB_API_KEY: string; NORMAL_ROUTER: boolean; PROXY_URLS: string[]; - TRAKT_CLIENT_ID: string; } const env: Record = { @@ -30,7 +28,6 @@ const env: Record = { DISCORD_LINK: undefined, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, - TRAKT_CLIENT_ID: import.meta.env.VITE_TRAKT_CLIENT_ID, }; const alerts = [] as string[]; @@ -65,6 +62,5 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", - TRAKT_CLIENT_ID: getKey("TRAKT_CLIENT_ID"), }; } From 0e9263b6192353b13640da33fbd61e1b990d9aba Mon Sep 17 00:00:00 2001 From: castdrian Date: Wed, 14 Jun 2023 07:48:31 +0200 Subject: [PATCH 28/56] fix movie metadata --- src/backend/metadata/getmeta.ts | 56 ++++++++++++++++++++++----------- src/backend/metadata/search.ts | 2 ++ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 777fae42..41e67772 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -43,6 +43,41 @@ export interface DetailedMeta { tmdbId?: string; } +export function fromatTMDBMetaResult( + details: TMDBShowData | TMDBMovieData, + type: MWMediaType +): TMDBMediaResult | undefined { + let tmdbmeta; + if (type === MWMediaType.MOVIE) { + tmdbmeta = { + id: details.id, + title: (details as TMDBMovieData).title, + object_type: mediaTypeToTMDB(type), + poster: (details as TMDBMovieData).poster_path ?? undefined, + original_release_year: Number( + (details as TMDBMovieData).release_date?.split("-")[0] + ), + }; + } + if (type === MWMediaType.SERIES) { + tmdbmeta = { + id: details.id, + 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: Number( + (details as TMDBShowData).first_air_date?.split("-")[0] + ), + }; + } + return tmdbmeta; +} + export async function getMetaFromId( type: MWMediaType, id: string, @@ -79,25 +114,8 @@ export async function getMetaFromId( } } - 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]), - }; - + const tmdbmeta = fromatTMDBMetaResult(details, type); + if (!tmdbmeta) return null; const meta = formatTMDBMeta(tmdbmeta, seasonData); if (!meta) return null; diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 31b2c682..7d06ab2c 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -29,6 +29,8 @@ export async function searchForMedia(query: MWQuery): Promise { }) ); + console.log(results[0]); + cache.set(query, results, 3600); return results; } From 06eb8e6b6d2b40778e7ae93797d919a08fa8ed52 Mon Sep 17 00:00:00 2001 From: castdrian Date: Wed, 14 Jun 2023 07:52:04 +0200 Subject: [PATCH 29/56] cleanup --- src/backend/metadata/search.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 7d06ab2c..31b2c682 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -29,8 +29,6 @@ export async function searchForMedia(query: MWQuery): Promise { }) ); - console.log(results[0]); - cache.set(query, results, 3600); return results; } From c9bac3ed68f2688ceabbeaac68bb845443b551b7 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 08:30:05 +0200 Subject: [PATCH 30/56] show poster in bookmarks --- src/backend/metadata/getmeta.ts | 8 ++++++-- src/components/media/MediaCard.tsx | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 41e67772..bba3948e 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -53,7 +53,9 @@ export function fromatTMDBMetaResult( id: details.id, title: (details as TMDBMovieData).title, object_type: mediaTypeToTMDB(type), - poster: (details as TMDBMovieData).poster_path ?? undefined, + poster: + Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? + undefined, original_release_year: Number( (details as TMDBMovieData).release_date?.split("-")[0] ), @@ -69,7 +71,9 @@ export function fromatTMDBMetaResult( season_number: v.season_number, title: v.name, })), - poster: (details as TMDBMovieData).poster_path ?? undefined, + poster: + Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? + undefined, original_release_year: Number( (details as TMDBShowData).first_air_date?.split("-")[0] ), diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 695027a2..ece6d293 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { TMDBMediaToId } from "@/backend/metadata/getmeta"; +import { Tmdb } from "@/backend/metadata/tmdb"; import { MWMediaMeta } from "@/backend/metadata/types"; import { DotList } from "@/components/text/DotList"; @@ -55,7 +56,9 @@ function MediaCardContent({ closable ? "" : "group-hover:rounded-lg", ].join(" ")} style={{ - backgroundImage: media.poster ? `url(${media.poster})` : undefined, + backgroundImage: media.poster + ? `url(${Tmdb.getMediaPoster(media.poster)})` + : undefined, }} > {series ? ( From c08a6c7e54b2dff6d2955eb6ee7f561d8379aa42 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 08:30:57 +0200 Subject: [PATCH 31/56] set adult false in query --- src/backend/metadata/tmdb.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index f01709bb..aed31c1d 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -120,12 +120,12 @@ export abstract class Tmdb { switch (type) { case "movie": data = await Tmdb.get( - `search/movie?query=${query}&include_adult=true&language=en-US&page=1` + `search/movie?query=${query}&include_adult=false&language=en-US&page=1` ); break; case "show": data = await Tmdb.get( - `search/tv?query=${query}&include_adult=true&language=en-US&page=1` + `search/tv?query=${query}&include_adult=false&language=en-US&page=1` ); break; default: From 4d51de3bd126feaa4585b3041242f73029cf600f Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 11:06:24 +0200 Subject: [PATCH 32/56] undo duplicate path --- src/backend/metadata/getmeta.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index bba3948e..41e67772 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -53,9 +53,7 @@ export function fromatTMDBMetaResult( id: details.id, title: (details as TMDBMovieData).title, object_type: mediaTypeToTMDB(type), - poster: - Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? - undefined, + poster: (details as TMDBMovieData).poster_path ?? undefined, original_release_year: Number( (details as TMDBMovieData).release_date?.split("-")[0] ), @@ -71,9 +69,7 @@ export function fromatTMDBMetaResult( season_number: v.season_number, title: v.name, })), - poster: - Tmdb.getMediaPoster((details as TMDBMovieData).poster_path) ?? - undefined, + poster: (details as TMDBMovieData).poster_path ?? undefined, original_release_year: Number( (details as TMDBShowData).first_air_date?.split("-")[0] ), From 0d249a3e27fee182b33980c72837804fa5579a13 Mon Sep 17 00:00:00 2001 From: castdrian Date: Thu, 15 Jun 2023 22:13:19 +0200 Subject: [PATCH 33/56] fix typo 'cause I can't type --- src/backend/metadata/getmeta.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 41e67772..a5246fcf 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -43,7 +43,7 @@ export interface DetailedMeta { tmdbId?: string; } -export function fromatTMDBMetaResult( +export function formatTMDBMetaResult( details: TMDBShowData | TMDBMovieData, type: MWMediaType ): TMDBMediaResult | undefined { @@ -114,7 +114,7 @@ export async function getMetaFromId( } } - const tmdbmeta = fromatTMDBMetaResult(details, type); + const tmdbmeta = formatTMDBMetaResult(details, type); if (!tmdbmeta) return null; const meta = formatTMDBMeta(tmdbmeta, seasonData); if (!meta) return null; From 205248a376d9347dac3c0f075bf05b3ff280a1f2 Mon Sep 17 00:00:00 2001 From: castdrian Date: Fri, 16 Jun 2023 11:18:32 +0200 Subject: [PATCH 34/56] use external ids endpoint for imdb ids --- src/backend/metadata/getmeta.ts | 6 ++---- src/backend/metadata/tmdb.ts | 25 +++++++++++++++++++++++++ src/backend/metadata/types.ts | 24 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index a5246fcf..6868c7e4 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -87,10 +87,8 @@ export async function getMetaFromId( if (!details) return null; - let imdbId; - if (type === MWMediaType.MOVIE) { - imdbId = (details as TMDBMovieData).imdb_id ?? undefined; - } + const externalIds = await Tmdb.getExternalIds(id, mediaTypeToTMDB(type)); + const imdbId = externalIds.imdb_id ?? undefined; let seasonData: TMDBSeasonMetaResult | undefined; diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index aed31c1d..e22e86e7 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -6,15 +6,18 @@ import { MWSeasonMeta, TMDBContentTypes, TMDBEpisodeShort, + TMDBExternalIds, TMDBMediaResult, TMDBMediaStatic, TMDBMovieData, + TMDBMovieExternalIds, TMDBMovieResponse, TMDBMovieResult, TMDBSearchResultStatic, TMDBSeason, TMDBSeasonMetaResult, TMDBShowData, + TMDBShowExternalIds, TMDBShowResponse, TMDBShowResult, } from "./types"; @@ -170,6 +173,28 @@ export abstract class Tmdb { title: e.name, })); } + + public static async getExternalIds( + id: string, + type: TMDBContentTypes + ): Promise { + let data; + + switch (type) { + case "movie": + data = await Tmdb.get( + `/movie/${id}/external_ids` + ); + break; + case "show": + data = await Tmdb.get(`/tv/${id}/external_ids`); + break; + default: + throw new Error("Invalid media type"); + } + + return data; + } } export async function formatTMDBSearchResult( diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index e23d9a5b..fa7a7ef0 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -379,3 +379,27 @@ export interface TMDBSeason { poster_path: string | null; season_number: number; } + +export interface TMDBShowExternalIds { + id: number; + imdb_id: null | string; + freebase_mid: null | string; + freebase_id: null | string; + tvdb_id: number; + tvrage_id: null | string; + wikidata_id: null | string; + facebook_id: null | string; + instagram_id: null | string; + twitter_id: null | string; +} + +export interface TMDBMovieExternalIds { + id: number; + imdb_id: null | string; + wikidata_id: null | string; + facebook_id: null | string; + instagram_id: null | string; + twitter_id: null | string; +} + +export type TMDBExternalIds = TMDBShowExternalIds | TMDBMovieExternalIds; From 5661a7873a67437a70b3233fe637acbf9d235fd7 Mon Sep 17 00:00:00 2001 From: castdrian Date: Mon, 19 Jun 2023 17:03:12 +0200 Subject: [PATCH 35/56] remove seasons from search result --- src/backend/metadata/tmdb.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index e22e86e7..0df2df7e 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -202,29 +202,18 @@ export async function formatTMDBSearchResult( mediatype: TMDBContentTypes ): Promise { 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), + poster: Tmdb.getMediaPoster(result.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, }; } From 3f241c2d072d0a05b9ba8a197e62a774fa538c7b Mon Sep 17 00:00:00 2001 From: castdrian Date: Tue, 20 Jun 2023 19:39:16 +0200 Subject: [PATCH 36/56] fix idiotism --- src/backend/metadata/getmeta.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 6868c7e4..4fd39e7b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -99,13 +99,18 @@ export async function getMetaFromId( const episodes = await Tmdb.getEpisodes( details.id.toString(), - season?.season_number ?? 1 + season.season_number === null || season.season_number === 0 + ? 1 + : season.season_number ); if (season && episodes) { seasonData = { id: season.id.toString(), - season_number: season.season_number, + season_number: + season.season_number === null || season.season_number === 0 + ? 1 + : season.season_number, title: season.name, episodes, }; From 33b67f32b14bff402d3162f5c46b3f34d727fe3e Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 12:43:36 +0200 Subject: [PATCH 37/56] no undef for tmdbmetaresult --- src/backend/metadata/getmeta.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 4fd39e7b..d8eca10e 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -46,7 +46,7 @@ export interface DetailedMeta { export function formatTMDBMetaResult( details: TMDBShowData | TMDBMovieData, type: MWMediaType -): TMDBMediaResult | undefined { +): TMDBMediaResult { let tmdbmeta; if (type === MWMediaType.MOVIE) { tmdbmeta = { @@ -75,6 +75,8 @@ export function formatTMDBMetaResult( ), }; } + + if (!tmdbmeta) throw new Error("unsupported type"); return tmdbmeta; } From 9495a3bf413d61d72ebb35cd7f17a6069a47e544 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 12:47:09 +0200 Subject: [PATCH 38/56] reduce casts --- src/backend/metadata/getmeta.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index d8eca10e..0ef84474 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -49,30 +49,28 @@ export function formatTMDBMetaResult( ): TMDBMediaResult { let tmdbmeta; if (type === MWMediaType.MOVIE) { + const movie = details as TMDBMovieData; tmdbmeta = { id: details.id, - title: (details as TMDBMovieData).title, + title: movie.title, object_type: mediaTypeToTMDB(type), - poster: (details as TMDBMovieData).poster_path ?? undefined, - original_release_year: Number( - (details as TMDBMovieData).release_date?.split("-")[0] - ), + poster: movie.poster_path ?? undefined, + original_release_year: Number(movie.release_date?.split("-")[0]), }; } if (type === MWMediaType.SERIES) { + const show = details as TMDBShowData; tmdbmeta = { id: details.id, - title: (details as TMDBShowData).name, + title: show.name, object_type: mediaTypeToTMDB(type), - seasons: (details as TMDBShowData).seasons.map((v) => ({ + seasons: show.seasons.map((v) => ({ id: v.id, season_number: v.season_number, title: v.name, })), poster: (details as TMDBMovieData).poster_path ?? undefined, - original_release_year: Number( - (details as TMDBShowData).first_air_date?.split("-")[0] - ), + original_release_year: Number(show.first_air_date?.split("-")[0]), }; } From 430486a9b9f09cdc0515d9c3513ca49f48dee649 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 12:48:33 +0200 Subject: [PATCH 39/56] direct return --- src/backend/metadata/getmeta.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 0ef84474..8010c89a 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -47,10 +47,9 @@ export function formatTMDBMetaResult( details: TMDBShowData | TMDBMovieData, type: MWMediaType ): TMDBMediaResult { - let tmdbmeta; if (type === MWMediaType.MOVIE) { const movie = details as TMDBMovieData; - tmdbmeta = { + return { id: details.id, title: movie.title, object_type: mediaTypeToTMDB(type), @@ -60,7 +59,7 @@ export function formatTMDBMetaResult( } if (type === MWMediaType.SERIES) { const show = details as TMDBShowData; - tmdbmeta = { + return { id: details.id, title: show.name, object_type: mediaTypeToTMDB(type), @@ -74,8 +73,7 @@ export function formatTMDBMetaResult( }; } - if (!tmdbmeta) throw new Error("unsupported type"); - return tmdbmeta; + throw new Error("unsupported type"); } export async function getMetaFromId( From 984d215312d5cb616af0c03a5c7e76e4d0e9284b Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 12:50:41 +0200 Subject: [PATCH 40/56] parse dates instead of cringe string manipulation --- src/backend/metadata/getmeta.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 8010c89a..4548a436 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -54,7 +54,7 @@ export function formatTMDBMetaResult( title: movie.title, object_type: mediaTypeToTMDB(type), poster: movie.poster_path ?? undefined, - original_release_year: Number(movie.release_date?.split("-")[0]), + original_release_year: new Date(movie.release_date).getFullYear(), }; } if (type === MWMediaType.SERIES) { @@ -69,7 +69,7 @@ export function formatTMDBMetaResult( title: v.name, })), poster: (details as TMDBMovieData).poster_path ?? undefined, - original_release_year: Number(show.first_air_date?.split("-")[0]), + original_release_year: new Date(show.first_air_date).getFullYear(), }; } From 89cdf74b2fa8e2a1d47978620d3ebf852b0613d1 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 12:51:30 +0200 Subject: [PATCH 41/56] readd vanished comment --- src/backend/metadata/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 31b2c682..549d7ba4 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -29,6 +29,6 @@ export async function searchForMedia(query: MWQuery): Promise { }) ); - cache.set(query, results, 3600); + cache.set(query, results, 3600); // cache results for 1 hour return results; } From 1408fcde9300def772923c83fb6bd203344451c9 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 13:07:33 +0200 Subject: [PATCH 42/56] export functions directly --- src/backend/metadata/getmeta.ts | 10 +- src/backend/metadata/search.ts | 4 +- src/backend/metadata/tmdb.ts | 168 ++++++++++++++--------------- src/components/media/MediaCard.tsx | 4 +- 4 files changed, 88 insertions(+), 98 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 4548a436..aa4267c5 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -3,8 +3,10 @@ import { FetchError } from "ofetch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { TMDBMediaToMediaType, - Tmdb, formatTMDBMeta, + getEpisodes, + getExternalIds, + getMediaDetails, mediaTypeToTMDB, } from "./tmdb"; import { @@ -81,11 +83,11 @@ export async function getMetaFromId( id: string, seasonId?: string ): Promise { - const details = await Tmdb.getMediaDetails(id, mediaTypeToTMDB(type)); + const details = await getMediaDetails(id, mediaTypeToTMDB(type)); if (!details) return null; - const externalIds = await Tmdb.getExternalIds(id, mediaTypeToTMDB(type)); + const externalIds = await getExternalIds(id, mediaTypeToTMDB(type)); const imdbId = externalIds.imdb_id ?? undefined; let seasonData: TMDBSeasonMetaResult | undefined; @@ -95,7 +97,7 @@ export async function getMetaFromId( const season = seasons?.find((v) => v.id.toString() === seasonId) ?? seasons?.[0]; - const episodes = await Tmdb.getEpisodes( + const episodes = await getEpisodes( details.id.toString(), season.season_number === null || season.season_number === 0 ? 1 diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 549d7ba4..9e2883d4 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,10 +1,10 @@ import { SimpleCache } from "@/utils/cache"; import { - Tmdb, formatTMDBMeta, formatTMDBSearchResult, mediaTypeToTMDB, + searchMedia, } from "./tmdb"; import { MWMediaMeta, MWQuery } from "./types"; @@ -18,7 +18,7 @@ export async function searchForMedia(query: MWQuery): Promise { if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; const { searchQuery, type } = query; - const data = await Tmdb.searchMedia(searchQuery, mediaTypeToTMDB(type)); + const data = await searchMedia(searchQuery, mediaTypeToTMDB(type)); const results = await Promise.all( data.results.map(async (v) => { const formattedResult = await formatTMDBSearchResult( diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 0df2df7e..cf327070 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -8,12 +8,10 @@ import { TMDBEpisodeShort, TMDBExternalIds, TMDBMediaResult, - TMDBMediaStatic, TMDBMovieData, TMDBMovieExternalIds, TMDBMovieResponse, TMDBMovieResult, - TMDBSearchResultStatic, TMDBSeason, TMDBSeasonMetaResult, TMDBShowData, @@ -98,103 +96,93 @@ export function decodeTMDBId( }; } -export abstract class Tmdb { - private static baseURL = "https://api.themoviedb.org/3"; +const baseURL = "https://api.themoviedb.org/3"; - private static headers = { - accept: "application/json", - Authorization: `Bearer ${conf().TMDB_API_KEY}`, - }; +const headers = { + accept: "application/json", + Authorization: `Bearer ${conf().TMDB_API_KEY}`, +}; - private static async get(url: string): Promise { - const res = await mwFetch(url, { - headers: Tmdb.headers, - baseURL: Tmdb.baseURL, - }); - return res; +async function get(url: string): Promise { + const res = await mwFetch(url, { + headers, + baseURL, + }); + return res; +} + +export async function searchMedia(query: string, type: TMDBContentTypes) { + let data; + + switch (type) { + case "movie": + data = await get( + `search/movie?query=${query}&include_adult=false&language=en-US&page=1` + ); + break; + case "show": + data = await get( + `search/tv?query=${query}&include_adult=false&language=en-US&page=1` + ); + break; + default: + throw new Error("Invalid media type"); } - public static searchMedia: TMDBSearchResultStatic["searchMedia"] = async ( - query: string, - type: TMDBContentTypes - ) => { - let data; + return data; +} - switch (type) { - case "movie": - data = await Tmdb.get( - `search/movie?query=${query}&include_adult=false&language=en-US&page=1` - ); - break; - case "show": - data = await Tmdb.get( - `search/tv?query=${query}&include_adult=false&language=en-US&page=1` - ); - break; - default: - throw new Error("Invalid media type"); - } +export async function getMediaDetails(id: string, type: TMDBContentTypes) { + let data; - return data; - }; - - public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async ( - id: string, - type: TMDBContentTypes - ) => { - let data; - - switch (type) { - case "movie": - data = await Tmdb.get(`/movie/${id}`); - break; - case "show": - data = await Tmdb.get(`/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}`; + switch (type) { + case "movie": + data = await get(`/movie/${id}`); + break; + case "show": + data = await get(`/tv/${id}`); + break; + default: + throw new Error("Invalid media type"); } - public static async getEpisodes( - id: string, - season: number - ): Promise { - const data = await Tmdb.get(`/tv/${id}/season/${season}`); - return data.episodes.map((e) => ({ - id: e.id, - episode_number: e.episode_number, - title: e.name, - })); + return data; +} + +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 getExternalIds( + id: string, + type: TMDBContentTypes +): Promise { + let data; + + switch (type) { + case "movie": + data = await get(`/movie/${id}/external_ids`); + break; + case "show": + data = await get(`/tv/${id}/external_ids`); + break; + default: + throw new Error("Invalid media type"); } - public static async getExternalIds( - id: string, - type: TMDBContentTypes - ): Promise { - let data; - - switch (type) { - case "movie": - data = await Tmdb.get( - `/movie/${id}/external_ids` - ); - break; - case "show": - data = await Tmdb.get(`/tv/${id}/external_ids`); - break; - default: - throw new Error("Invalid media type"); - } - - return data; - } + return data; } export async function formatTMDBSearchResult( @@ -208,7 +196,7 @@ export async function formatTMDBSearchResult( type === MWMediaType.SERIES ? (result as TMDBShowResult).name : (result as TMDBMovieResult).title, - poster: Tmdb.getMediaPoster(result.poster_path), + poster: getMediaPoster(result.poster_path), id: result.id, original_release_year: type === MWMediaType.SERIES diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index ece6d293..fd460bb7 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { TMDBMediaToId } from "@/backend/metadata/getmeta"; -import { Tmdb } from "@/backend/metadata/tmdb"; +import { getMediaPoster } from "@/backend/metadata/tmdb"; import { MWMediaMeta } from "@/backend/metadata/types"; import { DotList } from "@/components/text/DotList"; @@ -57,7 +57,7 @@ function MediaCardContent({ ].join(" ")} style={{ backgroundImage: media.poster - ? `url(${Tmdb.getMediaPoster(media.poster)})` + ? `url(${getMediaPoster(media.poster)})` : undefined, }} > From 7c3d4aac272a77debe89a02a77e4c54c9b09ed5e Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 13:23:39 +0200 Subject: [PATCH 43/56] refactor typedefs --- src/backend/metadata/getmeta.ts | 7 +- src/backend/metadata/justwatch.ts | 6 +- src/backend/metadata/search.ts | 7 +- src/backend/metadata/tmdb.ts | 11 +- src/backend/metadata/types/justwatch.ts | 48 +++++++ src/backend/metadata/types/mw.ts | 53 ++++++++ .../metadata/{types.ts => types/tmdb.ts} | 121 ------------------ 7 files changed, 118 insertions(+), 135 deletions(-) create mode 100644 src/backend/metadata/types/justwatch.ts create mode 100644 src/backend/metadata/types/mw.ts rename src/backend/metadata/{types.ts => types/tmdb.ts} (69%) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index aa4267c5..b347e720 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -13,13 +13,14 @@ import { JWMediaResult, JWSeasonMetaResult, JW_API_BASE, - MWMediaMeta, - MWMediaType, +} from "./types/justwatch"; +import { MWMediaMeta, MWMediaType } from "./types/mw"; +import { TMDBMediaResult, TMDBMovieData, TMDBSeasonMetaResult, TMDBShowData, -} from "./types"; +} from "./types/tmdb"; import { makeUrl, proxiedFetch } from "../helpers/fetch"; type JWExternalIdType = diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 857ff006..724c4acf 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -3,10 +3,8 @@ import { JWMediaResult, JWSeasonMetaResult, JW_IMAGE_BASE, - MWMediaMeta, - MWMediaType, - MWSeasonMeta, -} from "./types"; +} from "./types/justwatch"; +import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw"; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { if (type === MWMediaType.MOVIE) return "movie"; diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 9e2883d4..99cb51ba 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -6,7 +6,8 @@ import { mediaTypeToTMDB, searchMedia, } from "./tmdb"; -import { MWMediaMeta, MWQuery } from "./types"; +import { MWMediaMeta, MWQuery } from "./types/mw"; +import { TMDBMovieResponse, TMDBShowResponse } from "./types/tmdb"; const cache = new SimpleCache(); cache.setCompare((a, b) => { @@ -18,7 +19,9 @@ export async function searchForMedia(query: MWQuery): Promise { if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; const { searchQuery, type } = query; - const data = await searchMedia(searchQuery, mediaTypeToTMDB(type)); + const data = (await searchMedia(searchQuery, mediaTypeToTMDB(type))) as + | TMDBMovieResponse + | TMDBShowResponse; const results = await Promise.all( data.results.map(async (v) => { const formattedResult = await formatTMDBSearchResult( diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index cf327070..4a9271ba 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -1,9 +1,7 @@ import { conf } from "@/setup/config"; +import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw"; import { - MWMediaMeta, - MWMediaType, - MWSeasonMeta, TMDBContentTypes, TMDBEpisodeShort, TMDBExternalIds, @@ -18,7 +16,7 @@ import { TMDBShowExternalIds, TMDBShowResponse, TMDBShowResult, -} from "./types"; +} from "./types/tmdb"; import { mwFetch } from "../helpers/fetch"; export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes { @@ -111,7 +109,10 @@ async function get(url: string): Promise { return res; } -export async function searchMedia(query: string, type: TMDBContentTypes) { +export async function searchMedia( + query: string, + type: TMDBContentTypes +): Promise { let data; switch (type) { diff --git a/src/backend/metadata/types/justwatch.ts b/src/backend/metadata/types/justwatch.ts new file mode 100644 index 00000000..cb3ac092 --- /dev/null +++ b/src/backend/metadata/types/justwatch.ts @@ -0,0 +1,48 @@ +export type JWContentTypes = "movie" | "show"; + +export type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +export type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export const JW_API_BASE = "https://apis.justwatch.com"; +export const JW_IMAGE_BASE = "https://images.justwatch.com"; + +export type JWSeasonShort = { + title: string; + id: number; + season_number: number; +}; + +export type JWEpisodeShort = { + title: string; + id: number; + episode_number: number; +}; + +export type JWMediaResult = { + title: string; + poster?: string; + id: number; + original_release_year?: number; + jw_entity_id: string; + object_type: JWContentTypes; + seasons?: JWSeasonShort[]; +}; + +export type JWSeasonMetaResult = { + title: string; + id: string; + season_number: number; + episodes: JWEpisodeShort[]; +}; diff --git a/src/backend/metadata/types/mw.ts b/src/backend/metadata/types/mw.ts new file mode 100644 index 00000000..e7cc26fe --- /dev/null +++ b/src/backend/metadata/types/mw.ts @@ -0,0 +1,53 @@ +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 interface DetailedMeta { + meta: MWMediaMeta; + imdbId?: string; + tmdbId?: string; +} diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types/tmdb.ts similarity index 69% rename from src/backend/metadata/types.ts rename to src/backend/metadata/types/tmdb.ts index fa7a7ef0..cb5e9aa4 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types/tmdb.ts @@ -1,51 +1,3 @@ -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 TMDBContentTypes = "movie" | "show"; export type TMDBSeasonShort = { @@ -76,12 +28,6 @@ export type TMDBSeasonMetaResult = { episodes: TMDBEpisodeShort[]; }; -export interface DetailedMeta { - meta: MWMediaMeta; - imdbId?: string; - tmdbId?: string; -} - export interface TMDBShowData { adult: boolean; backdrop_path: string | null; @@ -225,63 +171,6 @@ export interface TMDBMovieData { vote_count: number; } -export type TMDBMediaDetailsPromise = Promise; - -export interface TMDBMediaStatic { - getMediaDetails(id: string, type: "show"): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: "movie"): TMDBMediaDetailsPromise; - getMediaDetails(id: string, type: TMDBContentTypes): TMDBMediaDetailsPromise; -} - -export type JWContentTypes = "movie" | "show"; - -export type JWSearchQuery = { - content_types: JWContentTypes[]; - page: number; - page_size: number; - query: string; -}; - -export type JWPage = { - items: T[]; - page: number; - page_size: number; - total_pages: number; - total_results: number; -}; - -export const JW_API_BASE = "https://apis.justwatch.com"; -export const JW_IMAGE_BASE = "https://images.justwatch.com"; - -export type JWSeasonShort = { - title: string; - id: number; - season_number: number; -}; - -export type JWEpisodeShort = { - title: string; - id: number; - episode_number: number; -}; - -export type JWMediaResult = { - title: string; - poster?: string; - id: number; - original_release_year?: number; - jw_entity_id: string; - object_type: JWContentTypes; - seasons?: JWSeasonShort[]; -}; - -export type JWSeasonMetaResult = { - title: string; - id: string; - season_number: number; - episodes: JWEpisodeShort[]; -}; - export interface TMDBEpisodeResult { season: number; number: number; @@ -342,16 +231,6 @@ export interface TMDBMovieResponse { 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; From dccab9b0bf084b974db4d2fd5ae64179c7ed2eb9 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 13:26:03 +0200 Subject: [PATCH 44/56] directly get poster url --- src/backend/metadata/getmeta.ts | 3 ++- src/components/media/MediaCard.tsx | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index b347e720..21b6843a 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -7,6 +7,7 @@ import { getEpisodes, getExternalIds, getMediaDetails, + getMediaPoster, mediaTypeToTMDB, } from "./tmdb"; import { @@ -56,7 +57,7 @@ export function formatTMDBMetaResult( id: details.id, title: movie.title, object_type: mediaTypeToTMDB(type), - poster: movie.poster_path ?? undefined, + poster: getMediaPoster(movie.poster_path) ?? undefined, original_release_year: new Date(movie.release_date).getFullYear(), }; } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index fd460bb7..c38b4a2b 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -2,8 +2,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { TMDBMediaToId } from "@/backend/metadata/getmeta"; -import { getMediaPoster } from "@/backend/metadata/tmdb"; -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types/mw"; import { DotList } from "@/components/text/DotList"; import { IconPatch } from "../buttons/IconPatch"; @@ -56,9 +55,7 @@ function MediaCardContent({ closable ? "" : "group-hover:rounded-lg", ].join(" ")} style={{ - backgroundImage: media.poster - ? `url(${getMediaPoster(media.poster)})` - : undefined, + backgroundImage: media.poster ? `url(${media.poster})` : undefined, }} > {series ? ( From a46cfa43d3597516cb37e97e7592ba9b74079c7d Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 13:31:50 +0200 Subject: [PATCH 45/56] fix test imports --- src/__tests__/providers/providers.test.ts | 2 +- src/__tests__/providers/testdata.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/providers/providers.test.ts b/src/__tests__/providers/providers.test.ts index 35c77d5d..350d4255 100644 --- a/src/__tests__/providers/providers.test.ts +++ b/src/__tests__/providers/providers.test.ts @@ -4,7 +4,7 @@ import "@/backend"; import { testData } from "@/__tests__/providers/testdata"; import { getProviders } from "@/backend/helpers/register"; import { runProvider } from "@/backend/helpers/run"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; describe("providers", () => { const providers = getProviders(); diff --git a/src/__tests__/providers/testdata.ts b/src/__tests__/providers/testdata.ts index 37e63e06..6db686e3 100644 --- a/src/__tests__/providers/testdata.ts +++ b/src/__tests__/providers/testdata.ts @@ -1,5 +1,5 @@ import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; export const testData: DetailedMeta[] = [ { From 436fb2707b4ed03c60e32735ba2a5f68150eae38 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 13:38:48 +0200 Subject: [PATCH 46/56] update all remaining imports --- src/backend/helpers/provider.ts | 2 +- src/backend/helpers/scrape.ts | 2 +- src/backend/providers/2embed.ts | 2 +- src/backend/providers/flixhq.ts | 2 +- src/backend/providers/gdriveplayer.ts | 2 +- src/backend/providers/gomovies.ts | 2 +- src/backend/providers/hdwatched.ts | 2 +- src/backend/providers/kissasian.ts | 2 +- src/backend/providers/m4ufree.ts | 2 +- src/backend/providers/netfilm.ts | 2 +- src/backend/providers/remotestream.ts | 2 +- src/backend/providers/sflix.ts | 2 +- src/backend/providers/streamflix.ts | 2 +- src/backend/providers/superstream/index.ts | 2 +- src/components/SearchBar.tsx | 2 +- src/components/media/WatchedMediaCard.tsx | 2 +- src/hooks/useScrape.ts | 2 +- src/hooks/useSearchQuery.ts | 2 +- src/setup/App.tsx | 2 +- src/state/bookmark/context.tsx | 2 +- src/state/bookmark/types.ts | 2 +- src/state/watched/context.tsx | 2 +- src/state/watched/migrations/v2.ts | 2 +- src/state/watched/types.ts | 2 +- src/video/components/actions/DividerAction.tsx | 2 +- src/video/components/actions/SeriesSelectionAction.tsx | 2 +- src/video/components/controllers/MetaController.tsx | 2 +- src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts | 2 +- src/video/components/parts/VideoErrorBoundary.tsx | 2 +- src/video/components/parts/VideoPlayerHeader.tsx | 2 +- src/video/components/popouts/EpisodeSelectionPopout.tsx | 5 ++++- src/views/developer/VideoTesterView.tsx | 2 +- src/views/media/MediaView.tsx | 5 ++++- src/views/other/v2Migration.tsx | 2 +- src/views/search/SearchResultsPartial.tsx | 2 +- src/views/search/SearchResultsView.tsx | 2 +- 36 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/backend/helpers/provider.ts b/src/backend/helpers/provider.ts index 6eed4560..58dea7d4 100644 --- a/src/backend/helpers/provider.ts +++ b/src/backend/helpers/provider.ts @@ -1,7 +1,7 @@ import { MWEmbed } from "./embed"; import { MWStream } from "./streams"; import { DetailedMeta } from "../metadata/getmeta"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; export type MWProviderScrapeResult = { stream?: MWStream; diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 70e20348..5f1a100c 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -3,7 +3,7 @@ import { getEmbedScraperByType, getProviders } from "./register"; import { runEmbedScraper, runProvider } from "./run"; import { MWStream } from "./streams"; import { DetailedMeta } from "../metadata/getmeta"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; interface MWProgressData { type: "embed" | "provider"; diff --git a/src/backend/providers/2embed.ts b/src/backend/providers/2embed.ts index 7cc8938e..507d5a2d 100644 --- a/src/backend/providers/2embed.ts +++ b/src/backend/providers/2embed.ts @@ -8,7 +8,7 @@ import { MWStreamQuality, MWStreamType, } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const twoEmbedBase = "https://www.2embed.to"; diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts index 376abd08..fd905019 100644 --- a/src/backend/providers/flixhq.ts +++ b/src/backend/providers/flixhq.ts @@ -7,7 +7,7 @@ import { import { mwFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const flixHqBase = "https://consumet-api-clone.vercel.app/meta/tmdb"; // instance stolen from streaminal :) diff --git a/src/backend/providers/gdriveplayer.ts b/src/backend/providers/gdriveplayer.ts index 5478b6ed..c184fea7 100644 --- a/src/backend/providers/gdriveplayer.ts +++ b/src/backend/providers/gdriveplayer.ts @@ -3,7 +3,7 @@ import { unpack } from "unpacker"; import { registerProvider } from "@/backend/helpers/register"; import { MWStreamQuality } from "@/backend/helpers/streams"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { proxiedFetch } from "../helpers/fetch"; diff --git a/src/backend/providers/gomovies.ts b/src/backend/providers/gomovies.ts index 9e22d095..fdce289b 100644 --- a/src/backend/providers/gomovies.ts +++ b/src/backend/providers/gomovies.ts @@ -1,7 +1,7 @@ import { MWEmbedType } from "../helpers/embed"; import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const gomoviesBase = "https://gomovies.sx"; diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts index 2096e160..458c3424 100644 --- a/src/backend/providers/hdwatched.ts +++ b/src/backend/providers/hdwatched.ts @@ -2,7 +2,7 @@ import { proxiedFetch } from "../helpers/fetch"; import { MWProviderContext } from "../helpers/provider"; import { registerProvider } from "../helpers/register"; import { MWStreamQuality, MWStreamType } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const hdwatchedBase = "https://www.hdwatched.xyz"; diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts index 90708970..a95e05ab 100644 --- a/src/backend/providers/kissasian.ts +++ b/src/backend/providers/kissasian.ts @@ -1,7 +1,7 @@ import { MWEmbedType } from "../helpers/embed"; import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const kissasianBase = "https://kissasian.li"; diff --git a/src/backend/providers/m4ufree.ts b/src/backend/providers/m4ufree.ts index 0fe5303d..b9d5aef0 100644 --- a/src/backend/providers/m4ufree.ts +++ b/src/backend/providers/m4ufree.ts @@ -2,7 +2,7 @@ import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const HOST = "m4ufree.com"; const URL_BASE = `https://${HOST}`; diff --git a/src/backend/providers/netfilm.ts b/src/backend/providers/netfilm.ts index f7efcfbe..54016733 100644 --- a/src/backend/providers/netfilm.ts +++ b/src/backend/providers/netfilm.ts @@ -5,7 +5,7 @@ import { MWStreamQuality, MWStreamType, } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const netfilmBase = "https://net-film.vercel.app"; diff --git a/src/backend/providers/remotestream.ts b/src/backend/providers/remotestream.ts index 02c0f199..093069e8 100644 --- a/src/backend/providers/remotestream.ts +++ b/src/backend/providers/remotestream.ts @@ -1,7 +1,7 @@ import { mwFetch } from "@/backend/helpers/fetch"; import { registerProvider } from "@/backend/helpers/register"; import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; const remotestreamBase = `https://fsa.remotestre.am`; diff --git a/src/backend/providers/sflix.ts b/src/backend/providers/sflix.ts index 4121046b..2cb1c598 100644 --- a/src/backend/providers/sflix.ts +++ b/src/backend/providers/sflix.ts @@ -1,7 +1,7 @@ import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; import { MWStreamQuality, MWStreamType } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types"; +import { MWMediaType } from "../metadata/types/mw"; const sflixBase = "https://sflix.video"; diff --git a/src/backend/providers/streamflix.ts b/src/backend/providers/streamflix.ts index 90dd4975..d4488b03 100644 --- a/src/backend/providers/streamflix.ts +++ b/src/backend/providers/streamflix.ts @@ -5,7 +5,7 @@ import { MWStreamQuality, MWStreamType, } from "@/backend/helpers/streams"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; const streamflixBase = "https://us-west2-compute-proxied.streamflix.one"; diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 585d8d8a..75a8b844 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -13,7 +13,7 @@ import { MWStreamQuality, MWStreamType, } from "@/backend/helpers/streams"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { compareTitle } from "@/utils/titleMatch"; const nanoid = customAlphabet("0123456789abcdef", 32); diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 4940cbc7..431de337 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { MWMediaType, MWQuery } from "@/backend/metadata/types"; +import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw"; import { DropdownButton } from "./buttons/DropdownButton"; import { Icon, Icons } from "./Icon"; diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 346c77b6..ade1612a 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types/mw"; import { useWatchedContext } from "@/state/watched"; import { MediaCard } from "./MediaCard"; diff --git a/src/hooks/useScrape.ts b/src/hooks/useScrape.ts index a375e618..3cffa4ee 100644 --- a/src/hooks/useScrape.ts +++ b/src/hooks/useScrape.ts @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { findBestStream } from "@/backend/helpers/scrape"; import { MWStream } from "@/backend/helpers/streams"; import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; export interface ScrapeEventLog { type: "provider" | "embed"; diff --git a/src/hooks/useSearchQuery.ts b/src/hooks/useSearchQuery.ts index d431a0d0..cb8c3171 100644 --- a/src/hooks/useSearchQuery.ts +++ b/src/hooks/useSearchQuery.ts @@ -1,7 +1,7 @@ import { useState } from "react"; import { generatePath, useHistory, useRouteMatch } from "react-router-dom"; -import { MWMediaType, MWQuery } from "@/backend/metadata/types"; +import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw"; function getInitialValue(params: { type: string; query: string }) { const type = diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 7be4d581..d0a0887f 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -8,7 +8,7 @@ import { } from "react-router-dom"; import { convertLegacyUrl } from "@/backend/metadata/getmeta"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { BannerContextProvider } from "@/hooks/useBanner"; import { Layout } from "@/setup/Layout"; import { BookmarkContextProvider } from "@/state/bookmark"; diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx index 9dca821f..692d4e76 100644 --- a/src/state/bookmark/context.tsx +++ b/src/state/bookmark/context.tsx @@ -1,6 +1,6 @@ import { ReactNode, createContext, useContext, useMemo } from "react"; -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types/mw"; import { useStore } from "@/utils/storage"; import { BookmarkStore } from "./store"; diff --git a/src/state/bookmark/types.ts b/src/state/bookmark/types.ts index 05cb3641..79b92a5c 100644 --- a/src/state/bookmark/types.ts +++ b/src/state/bookmark/types.ts @@ -1,4 +1,4 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types/mw"; export interface BookmarkStoreData { bookmarks: MWMediaMeta[]; diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index 3ce17b2a..661b0ed3 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -8,7 +8,7 @@ import { } from "react"; import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { useStore } from "@/utils/storage"; import { VideoProgressStore } from "./store"; diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index 8f7a56b6..94f1141b 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -1,6 +1,6 @@ import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; import { searchForMedia } from "@/backend/metadata/search"; -import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; +import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types/mw"; import { compareTitle } from "@/utils/titleMatch"; import { WatchedStoreData, WatchedStoreItem } from "../types"; diff --git a/src/state/watched/types.ts b/src/state/watched/types.ts index a3246c38..0854b90b 100644 --- a/src/state/watched/types.ts +++ b/src/state/watched/types.ts @@ -1,4 +1,4 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types/mw"; export interface StoreMediaItem { meta: MWMediaMeta; diff --git a/src/video/components/actions/DividerAction.tsx b/src/video/components/actions/DividerAction.tsx index 5778e16f..3aeaeaef 100644 --- a/src/video/components/actions/DividerAction.tsx +++ b/src/video/components/actions/DividerAction.tsx @@ -1,4 +1,4 @@ -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; diff --git a/src/video/components/actions/SeriesSelectionAction.tsx b/src/video/components/actions/SeriesSelectionAction.tsx index d228b047..9eff0bb6 100644 --- a/src/video/components/actions/SeriesSelectionAction.tsx +++ b/src/video/components/actions/SeriesSelectionAction.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { Icons } from "@/components/Icon"; import { FloatingAnchor } from "@/components/popout/FloatingAnchor"; import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; diff --git a/src/video/components/controllers/MetaController.tsx b/src/video/components/controllers/MetaController.tsx index ee6bc696..25757e25 100644 --- a/src/video/components/controllers/MetaController.tsx +++ b/src/video/components/controllers/MetaController.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { MWCaption } from "@/backend/helpers/streams"; -import { MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; +import { MWSeasonWithEpisodeMeta } from "@/backend/metadata/types/mw"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useControls } from "@/video/state/logic/controls"; import { VideoPlayerMeta } from "@/video/state/types"; diff --git a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts index 6eb51170..11dfdc88 100644 --- a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts +++ b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { useMeta } from "@/video/state/logic/meta"; export function useCurrentSeriesEpisodeInfo(descriptor: string) { diff --git a/src/video/components/parts/VideoErrorBoundary.tsx b/src/video/components/parts/VideoErrorBoundary.tsx index 5786aa7a..061bf2b7 100644 --- a/src/video/components/parts/VideoErrorBoundary.tsx +++ b/src/video/components/parts/VideoErrorBoundary.tsx @@ -2,7 +2,7 @@ import { Component } from "react"; import { Trans } from "react-i18next"; import type { ReactNode } from "react-router-dom/node_modules/@types/react/index"; -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types/mw"; import { ErrorMessage } from "@/components/layout/ErrorBoundary"; import { Link } from "@/components/text/Link"; import { conf } from "@/setup/config"; diff --git a/src/video/components/parts/VideoPlayerHeader.tsx b/src/video/components/parts/VideoPlayerHeader.tsx index 8c026c49..3a333ee3 100644 --- a/src/video/components/parts/VideoPlayerHeader.tsx +++ b/src/video/components/parts/VideoPlayerHeader.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types/mw"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; import { BrandPill } from "@/components/layout/BrandPill"; diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index ce45c318..66c9ae49 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -3,7 +3,10 @@ import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta"; -import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; +import { + MWMediaType, + MWSeasonWithEpisodeMeta, +} from "@/backend/metadata/types/mw"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; import { Loading } from "@/components/layout/Loading"; diff --git a/src/views/developer/VideoTesterView.tsx b/src/views/developer/VideoTesterView.tsx index b192cd40..e1b3fa35 100644 --- a/src/views/developer/VideoTesterView.tsx +++ b/src/views/developer/VideoTesterView.tsx @@ -3,7 +3,7 @@ import { Helmet } from "react-helmet"; import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { Button } from "@/components/Button"; import { Dropdown } from "@/components/Dropdown"; import { Navigation } from "@/components/layout/Navigation"; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 7ae1c01b..6e1659a6 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -9,7 +9,10 @@ import { decodeTMDBId, getMetaFromId, } from "@/backend/metadata/getmeta"; -import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; +import { + MWMediaType, + MWSeasonWithEpisodeMeta, +} from "@/backend/metadata/types/mw"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Loading } from "@/components/layout/Loading"; diff --git a/src/views/other/v2Migration.tsx b/src/views/other/v2Migration.tsx index 1334ae26..d0b05e42 100644 --- a/src/views/other/v2Migration.tsx +++ b/src/views/other/v2Migration.tsx @@ -1,7 +1,7 @@ import pako from "pako"; import { useEffect, useState } from "react"; -import { MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types/mw"; import { conf } from "@/setup/config"; function fromBinary(str: string): Uint8Array { diff --git a/src/views/search/SearchResultsPartial.tsx b/src/views/search/SearchResultsPartial.tsx index 5769338b..e7cfc509 100644 --- a/src/views/search/SearchResultsPartial.tsx +++ b/src/views/search/SearchResultsPartial.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; -import { MWQuery } from "@/backend/metadata/types"; +import { MWQuery } from "@/backend/metadata/types/mw"; import { useDebounce } from "@/hooks/useDebounce"; import { HomeView } from "./HomeView"; diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 331d4f2d..f6507ef1 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { searchForMedia } from "@/backend/metadata/search"; -import { MWMediaMeta, MWQuery } from "@/backend/metadata/types"; +import { MWMediaMeta, MWQuery } from "@/backend/metadata/types/mw"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; From 09f6a3125b7b3a6c0841f4c8408a724cfa153e97 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 13:54:34 +0200 Subject: [PATCH 47/56] clean up remnants from details fetch --- src/backend/metadata/search.ts | 18 +++++------------- src/backend/metadata/tmdb.ts | 31 ++++++++++++++++++------------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 99cb51ba..0d8f561f 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -7,7 +7,6 @@ import { searchMedia, } from "./tmdb"; import { MWMediaMeta, MWQuery } from "./types/mw"; -import { TMDBMovieResponse, TMDBShowResponse } from "./types/tmdb"; const cache = new SimpleCache(); cache.setCompare((a, b) => { @@ -19,18 +18,11 @@ export async function searchForMedia(query: MWQuery): Promise { if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; const { searchQuery, type } = query; - const data = (await searchMedia(searchQuery, mediaTypeToTMDB(type))) as - | TMDBMovieResponse - | TMDBShowResponse; - const results = await Promise.all( - data.results.map(async (v) => { - const formattedResult = await formatTMDBSearchResult( - v, - mediaTypeToTMDB(type) - ); - return formatTMDBMeta(formattedResult); - }) - ); + const data = await searchMedia(searchQuery, mediaTypeToTMDB(type)); + const results = data.results.map((v) => { + const formattedResult = formatTMDBSearchResult(v, mediaTypeToTMDB(type)); + return formatTMDBMeta(formattedResult); + }); cache.set(query, results, 3600); // cache results for 1 hour return results; diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 4a9271ba..4c3259a3 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -186,23 +186,28 @@ export async function getExternalIds( return data; } -export async function formatTMDBSearchResult( +export function formatTMDBSearchResult( result: TMDBShowResult | TMDBMovieResult, mediatype: TMDBContentTypes -): Promise { +): TMDBMediaResult { const type = TMDBMediaToMediaType(mediatype); + if (type === MWMediaType.SERIES) { + const show = result as TMDBShowResult; + 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 TMDBMovieResult; return { - title: - type === MWMediaType.SERIES - ? (result as TMDBShowResult).name - : (result as TMDBMovieResult).title, - poster: getMediaPoster(result.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), + title: movie.title, + poster: getMediaPoster(movie.poster_path), + id: movie.id, + original_release_year: new Date(movie.release_date).getFullYear(), + object_type: mediatype, }; } From 1c17ef679ddb72889771ba488b416590ba1674cb Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 14:04:37 +0200 Subject: [PATCH 48/56] clean up requests --- src/backend/metadata/tmdb.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 4c3259a3..f5d1e370 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -101,10 +101,13 @@ const headers = { Authorization: `Bearer ${conf().TMDB_API_KEY}`, }; -async function get(url: string): Promise { - const res = await mwFetch(url, { +async function get(url: string, params?: object): Promise { + const res = await mwFetch(encodeURI(url), { headers, baseURL, + params: { + ...params, + }, }); return res; } @@ -117,14 +120,20 @@ export async function searchMedia( switch (type) { case "movie": - data = await get( - `search/movie?query=${query}&include_adult=false&language=en-US&page=1` - ); + data = await get("search/movie", { + query, + include_adult: false, + language: "en-US", + page: 1, + }); break; case "show": - data = await get( - `search/tv?query=${query}&include_adult=false&language=en-US&page=1` - ); + data = await get("search/tv", { + query, + include_adult: false, + language: "en-US", + page: 1, + }); break; default: throw new Error("Invalid media type"); From f5f69ca7d4b923e997d1221b1acd9faa3836b811 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 15:14:48 +0200 Subject: [PATCH 49/56] default to season 1, with specials still playable --- src/backend/metadata/getmeta.ts | 28 +++++++++++++--------------- src/components/media/MediaCard.tsx | 17 +++++++++++------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 21b6843a..b2166c34 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -96,24 +96,22 @@ export async function getMetaFromId( if (type === MWMediaType.SERIES) { const seasons = (details as TMDBShowData).seasons; - const season = - seasons?.find((v) => v.id.toString() === seasonId) ?? seasons?.[0]; - const episodes = await getEpisodes( - details.id.toString(), - season.season_number === null || season.season_number === 0 - ? 1 - : season.season_number - ); + let selectedSeason = seasons.find((v) => v.id.toString() === seasonId); + if (!selectedSeason) { + selectedSeason = seasons.find((v) => v.season_number === 1); + } + + if (selectedSeason) { + const episodes = await getEpisodes( + details.id.toString(), + selectedSeason.season_number + ); - if (season && episodes) { seasonData = { - id: season.id.toString(), - season_number: - season.season_number === null || season.season_number === 0 - ? 1 - : season.season_number, - title: season.name, + id: selectedSeason.id.toString(), + season_number: selectedSeason.season_number, + title: selectedSeason.name, episodes, }; } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index c38b4a2b..a153d8b4 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -13,7 +13,7 @@ export interface MediaCardProps { linkable?: boolean; series?: { episode: number; - season: number; + season?: number; episodeId: string; seasonId: string; }; @@ -72,7 +72,7 @@ function MediaCardContent({ ].join(" ")} > {t("seasons.seasonAndEpisode", { - season: series.season, + season: series.season || 1, episode: series.episode, })}

@@ -134,10 +134,15 @@ export function MediaCard(props: MediaCardProps) { let link = canLink ? `/media/${encodeURIComponent(TMDBMediaToId(props.media))}` : "#"; - if (canLink && props.series) - link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( - props.series.episodeId - )}`; + if (canLink && props.series) { + if (props.series.season === 0 && !props.series.episodeId) { + link += `/${encodeURIComponent(props.series.seasonId)}`; + } else { + link += `/${encodeURIComponent( + props.series.seasonId + )}/${encodeURIComponent(props.series.episodeId)}`; + } + } if (!props.linkable) return {content}; return ( From 394271857f9fb78eda78ef0b26745680a0d8d2b3 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Wed, 21 Jun 2023 18:16:41 +0200 Subject: [PATCH 50/56] refactor and improve legacy redirect --- src/backend/metadata/getmeta.ts | 21 +++- src/backend/metadata/tmdb.ts | 14 +++ src/backend/metadata/types/tmdb.ts | 24 +++++ src/setup/App.tsx | 167 ++++++++++++++++------------- 4 files changed, 145 insertions(+), 81 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index b2166c34..fe2ea62b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -8,6 +8,7 @@ import { getExternalIds, getMediaDetails, getMediaPoster, + getMovieFromExternalId, mediaTypeToTMDB, } from "./tmdb"; import { @@ -206,11 +207,23 @@ 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(TMDBMediaToMediaType(type), id); + + const mediaType = TMDBMediaToMediaType(type); + const meta = await getLegacyMetaFromId(mediaType, id); + if (!meta) return undefined; - const tmdbId = meta.tmdbId; - if (!tmdbId) return undefined; - return `/media/tmdb-${type}-${tmdbId}`; + const { tmdbId, imdbId } = meta; + if (!tmdbId && !imdbId) return undefined; + + // movies always have an imdb id on tmdb + if (imdbId && mediaType === MWMediaType.MOVIE) { + const movieId = await getMovieFromExternalId(imdbId); + if (movieId) return `/media/tmdb-movie-${movieId}`; + } + + if (tmdbId) { + return `/media/tmdb-${type}-${tmdbId}`; + } } return undefined; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index f5d1e370..db665528 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -2,6 +2,7 @@ import { conf } from "@/setup/config"; import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw"; import { + ExternalIdMovieSearchResult, TMDBContentTypes, TMDBEpisodeShort, TMDBExternalIds, @@ -195,6 +196,19 @@ export async function getExternalIds( return data; } +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: TMDBShowResult | TMDBMovieResult, mediatype: TMDBContentTypes diff --git a/src/backend/metadata/types/tmdb.ts b/src/backend/metadata/types/tmdb.ts index cb5e9aa4..843786f4 100644 --- a/src/backend/metadata/types/tmdb.ts +++ b/src/backend/metadata/types/tmdb.ts @@ -282,3 +282,27 @@ export interface TMDBMovieExternalIds { } export type TMDBExternalIds = TMDBShowExternalIds | TMDBMovieExternalIds; + +export interface ExternalIdMovieSearchResult { + movie_results: { + adult: boolean; + backdrop_path: string; + id: number; + title: string; + original_language: string; + original_title: string; + overview: string; + poster_path: string; + media_type: string; + genre_ids: number[]; + popularity: number; + release_date: string; + video: boolean; + vote_average: number; + vote_count: number; + }[]; + person_results: any[]; + tv_results: any[]; + tv_episode_results: any[]; + tv_season_results: any[]; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index d0a0887f..0516eb48 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -1,11 +1,5 @@ -import { lazy } from "react"; -import { - Redirect, - Route, - Switch, - useHistory, - useLocation, -} from "react-router-dom"; +import { lazy, useEffect, useState } from "react"; +import { Redirect, Route, Switch, useLocation } from "react-router-dom"; import { convertLegacyUrl } from "@/backend/metadata/getmeta"; import { MWMediaType } from "@/backend/metadata/types/mw"; @@ -19,86 +13,105 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { V2MigrationView } from "@/views/other/v2Migration"; import { SearchView } from "@/views/search/SearchView"; -function App() { +// eslint-disable-next-line react/function-component-definition, react/prop-types +const LegacyUrlView: React.FC = ({ children }) => { const location = useLocation(); - const history = useHistory(); + const [redirectUrl, setRedirectUrl] = useState(null); - // Call the conversion function and redirect if necessary - convertLegacyUrl(location.pathname).then((convertedUrl) => { - if (convertedUrl) { - history.replace(convertedUrl); - } - }); + useEffect(() => { + // Call the conversion function and set the redirect URL if necessary + convertLegacyUrl(location.pathname).then((convertedUrl) => { + if (convertedUrl) { + setRedirectUrl(convertedUrl); + } + }); + }, [location.pathname]); + + if (redirectUrl) { + return ; + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +function App() { return ( - - {/* functional routes */} - - - - + + + {/* functional routes */} + + + + - {/* pages */} - - - + {/* pages */} + + + - {/* other */} - import("@/views/developer/DeveloperView") - )} - /> - import("@/views/developer/VideoTesterView") - )} - /> - {/* developer routes that can abuse workers are disabled in production */} - {process.env.NODE_ENV === "development" ? ( - <> - import("@/views/developer/TestView") - )} - /> + {/* other */} + import("@/views/developer/DeveloperView") + )} + /> + import("@/views/developer/VideoTesterView") + )} + /> + {/* developer routes that can abuse workers are disabled in production */} + {process.env.NODE_ENV === "development" ? ( + <> + import("@/views/developer/TestView") + )} + /> - import("@/views/developer/ProviderTesterView") - )} - /> - import("@/views/developer/EmbedTesterView") - )} - /> - - ) : null} - - + import("@/views/developer/ProviderTesterView") + )} + /> + import("@/views/developer/EmbedTesterView") + )} + /> + + ) : null} + + + From f892a3037f39a1a685f5f1bf8db8babaf458c72d Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 21 Jun 2023 21:35:25 +0200 Subject: [PATCH 51/56] fix redirection issues --- src/backend/metadata/getmeta.ts | 38 ++++---- src/setup/App.tsx | 168 ++++++++++++++++---------------- 2 files changed, 104 insertions(+), 102 deletions(-) diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index fe2ea62b..c09d8292 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -201,29 +201,33 @@ export function decodeTMDBId( }; } +export function isLegacyUrl(url: string): boolean { + if (url.startsWith("/media/JW")) return true; + return false; +} + export async function convertLegacyUrl( url: string ): Promise { - if (url.startsWith("/media/JW")) { - const urlParts = url.split("/").slice(2); - const [, type, id] = urlParts[0].split("-", 3); + if (!isLegacyUrl(url)) return undefined; - const mediaType = TMDBMediaToMediaType(type); - const meta = await getLegacyMetaFromId(mediaType, id); + const urlParts = url.split("/").slice(2); + const [, type, id] = urlParts[0].split("-", 3); - if (!meta) return undefined; - const { tmdbId, imdbId } = meta; - if (!tmdbId && !imdbId) return undefined; + const mediaType = TMDBMediaToMediaType(type); + const meta = await getLegacyMetaFromId(mediaType, id); - // movies always have an imdb id on tmdb - if (imdbId && mediaType === MWMediaType.MOVIE) { - const movieId = await getMovieFromExternalId(imdbId); - if (movieId) return `/media/tmdb-movie-${movieId}`; - } + if (!meta) return undefined; + const { tmdbId, imdbId } = meta; + if (!tmdbId && !imdbId) return undefined; - if (tmdbId) { - return `/media/tmdb-${type}-${tmdbId}`; - } + // movies always have an imdb id on tmdb + if (imdbId && mediaType === MWMediaType.MOVIE) { + const movieId = await getMovieFromExternalId(imdbId); + if (movieId) return `/media/tmdb-movie-${movieId}`; + } + + if (tmdbId) { + return `/media/tmdb-${type}-${tmdbId}`; } - return undefined; } diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 0516eb48..7d1847ae 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -1,7 +1,13 @@ -import { lazy, useEffect, useState } from "react"; -import { Redirect, Route, Switch, useLocation } from "react-router-dom"; +import { ReactElement, lazy, useEffect } from "react"; +import { + Redirect, + Route, + Switch, + useHistory, + useLocation, +} from "react-router-dom"; -import { convertLegacyUrl } from "@/backend/metadata/getmeta"; +import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta"; import { MWMediaType } from "@/backend/metadata/types/mw"; import { BannerContextProvider } from "@/hooks/useBanner"; import { Layout } from "@/setup/Layout"; @@ -13,27 +19,21 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { V2MigrationView } from "@/views/other/v2Migration"; import { SearchView } from "@/views/search/SearchView"; -// eslint-disable-next-line react/function-component-definition, react/prop-types -const LegacyUrlView: React.FC = ({ children }) => { +function LegacyUrlView({ children }: { children: ReactElement }) { const location = useLocation(); - const [redirectUrl, setRedirectUrl] = useState(null); + const { replace } = useHistory(); useEffect(() => { - // Call the conversion function and set the redirect URL if necessary + const url = location.pathname; + if (!isLegacyUrl(url)) return; convertLegacyUrl(location.pathname).then((convertedUrl) => { - if (convertedUrl) { - setRedirectUrl(convertedUrl); - } + replace(convertedUrl ?? "/"); }); - }, [location.pathname]); + }, [location.pathname, replace]); - if (redirectUrl) { - return ; - } - - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{children}; -}; + if (isLegacyUrl(location.pathname)) return null; + return children; +} function App() { return ( @@ -42,76 +42,74 @@ function App() { - - - {/* functional routes */} - - - - + + {/* functional routes */} + + + + - {/* pages */} - - - + {/* pages */} + + + + + + + + + + + - {/* other */} - import("@/views/developer/DeveloperView") - )} - /> - import("@/views/developer/VideoTesterView") - )} - /> - {/* developer routes that can abuse workers are disabled in production */} - {process.env.NODE_ENV === "development" ? ( - <> - import("@/views/developer/TestView") - )} - /> + {/* other */} + import("@/views/developer/DeveloperView") + )} + /> + import("@/views/developer/VideoTesterView") + )} + /> + {/* developer routes that can abuse workers are disabled in production */} + {process.env.NODE_ENV === "development" ? ( + <> + import("@/views/developer/TestView") + )} + /> - import("@/views/developer/ProviderTesterView") - )} - /> - import("@/views/developer/EmbedTesterView") - )} - /> - - ) : null} - - - + import("@/views/developer/ProviderTesterView") + )} + /> + import("@/views/developer/EmbedTesterView") + )} + /> + + ) : null} + + From 9fbba7ea55f2a8eedb193b402f56a17edd6ac51e Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Thu, 22 Jun 2023 10:47:14 +0200 Subject: [PATCH 52/56] localstorage migration --- src/state/bookmark/store.ts | 7 +++ src/state/watched/migrations/v3.ts | 87 ++++++++++++++++++++++++++++++ src/state/watched/store.ts | 7 +++ 3 files changed, 101 insertions(+) create mode 100644 src/state/watched/migrations/v3.ts diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts index 1b7a2053..51de0ed0 100644 --- a/src/state/bookmark/store.ts +++ b/src/state/bookmark/store.ts @@ -2,6 +2,7 @@ import { createVersionedStore } from "@/utils/storage"; import { BookmarkStoreData } from "./types"; import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2"; +import { migrateV2Bookmarks } from "../watched/migrations/v3"; export const BookmarkStore = createVersionedStore() .setKey("mw-bookmarks") @@ -13,6 +14,12 @@ export const BookmarkStore = createVersionedStore() }) .addVersion({ version: 1, + migrate(old: OldBookmarks) { + return migrateV2Bookmarks(old); + }, + }) + .addVersion({ + version: 2, create() { return { bookmarks: [], diff --git a/src/state/watched/migrations/v3.ts b/src/state/watched/migrations/v3.ts new file mode 100644 index 00000000..971dacf1 --- /dev/null +++ b/src/state/watched/migrations/v3.ts @@ -0,0 +1,87 @@ +import { getLegacyMetaFromId } from "@/backend/metadata/getmeta"; +import { getMovieFromExternalId } from "@/backend/metadata/tmdb"; +import { MWMediaType } from "@/backend/metadata/types/mw"; + +import { WatchedStoreData } from "../types"; + +async function migrateId( + id: number, + type: MWMediaType +): Promise { + console.log("migrating id", id, type); + const meta = await getLegacyMetaFromId(type, id.toString()); + console.log("migrating id", meta); + + if (!meta) return undefined; + const { tmdbId, imdbId } = meta; + if (!tmdbId && !imdbId) return undefined; + + // movies always have an imdb id on tmdb + if (imdbId && type === MWMediaType.MOVIE) { + const movieId = await getMovieFromExternalId(imdbId); + if (movieId) return movieId; + } + + if (tmdbId) { + return tmdbId; + } +} + +export async function migrateV2Bookmarks(old: any) { + const oldData = old; + if (!oldData) return; + + const updatedBookmarks = oldData.bookmarks.map( + async (item: { id: number; type: MWMediaType }) => ({ + ...item, + mediaId: await migrateId(item.id, item.type), + }) + ); + + return { + bookmarks: (await Promise.all(updatedBookmarks)).filter( + (item) => item.mediaId + ), + }; +} + +export async function migrateV3Videos(old: any) { + console.log("migrating watched"); + const oldData = old; + if (!oldData) return; + console.log(oldData); + + const updatedItems = await Promise.all( + oldData.items.map(async (item: any) => { + const migratedId = await migrateId( + item.item.meta.id, + item.item.meta.type + ); + + const migratedItem = { + ...item, + item: { + ...item.item, + meta: { + ...item.item.meta, + id: migratedId, + }, + }, + }; + + return { + ...item, + item: migratedId ? migratedItem : item.item, + }; + }) + ); + + const newData: WatchedStoreData = { + items: updatedItems.map((item) => item.item), // Extract the "item" object + }; + + return { + ...oldData, + items: newData.items, + }; +} diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 95adef28..b59c37dc 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -1,6 +1,7 @@ import { createVersionedStore } from "@/utils/storage"; import { OldData, migrateV2Videos } from "./migrations/v2"; +import { migrateV3Videos } from "./migrations/v3"; import { WatchedStoreData } from "./types"; export const VideoProgressStore = createVersionedStore() @@ -21,6 +22,12 @@ export const VideoProgressStore = createVersionedStore() }) .addVersion({ version: 2, + migrate(old: OldData) { + return migrateV3Videos(old); + }, + }) + .addVersion({ + version: 3, create() { return { items: [], From e0bf711a79b6e2a3fc9e6c1b634b18dbad369e18 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Thu, 22 Jun 2023 10:48:00 +0200 Subject: [PATCH 53/56] cleanup --- src/state/watched/migrations/v3.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/state/watched/migrations/v3.ts b/src/state/watched/migrations/v3.ts index 971dacf1..cbf081e2 100644 --- a/src/state/watched/migrations/v3.ts +++ b/src/state/watched/migrations/v3.ts @@ -8,9 +8,7 @@ async function migrateId( id: number, type: MWMediaType ): Promise { - console.log("migrating id", id, type); const meta = await getLegacyMetaFromId(type, id.toString()); - console.log("migrating id", meta); if (!meta) return undefined; const { tmdbId, imdbId } = meta; @@ -46,10 +44,8 @@ export async function migrateV2Bookmarks(old: any) { } export async function migrateV3Videos(old: any) { - console.log("migrating watched"); const oldData = old; if (!oldData) return; - console.log(oldData); const updatedItems = await Promise.all( oldData.items.map(async (item: any) => { @@ -77,7 +73,7 @@ export async function migrateV3Videos(old: any) { ); const newData: WatchedStoreData = { - items: updatedItems.map((item) => item.item), // Extract the "item" object + items: updatedItems.map((item) => item.item), }; return { From 845fd935979aa3006384fe53a83a460a0716fa18 Mon Sep 17 00:00:00 2001 From: adrifcastr Date: Thu, 22 Jun 2023 20:29:10 +0200 Subject: [PATCH 54/56] fix small oversight --- src/state/watched/migrations/v3.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/state/watched/migrations/v3.ts b/src/state/watched/migrations/v3.ts index cbf081e2..71e0b182 100644 --- a/src/state/watched/migrations/v3.ts +++ b/src/state/watched/migrations/v3.ts @@ -32,14 +32,12 @@ export async function migrateV2Bookmarks(old: any) { const updatedBookmarks = oldData.bookmarks.map( async (item: { id: number; type: MWMediaType }) => ({ ...item, - mediaId: await migrateId(item.id, item.type), + id: await migrateId(item.id, item.type), }) ); return { - bookmarks: (await Promise.all(updatedBookmarks)).filter( - (item) => item.mediaId - ), + bookmarks: (await Promise.all(updatedBookmarks)).filter((item) => item.id), }; } From 545120d5cc6c5fc4fcbad00a2ea6bc7a99dc4970 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 22 Jun 2023 20:58:44 +0200 Subject: [PATCH 55/56] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43436852..3f7c4bf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movie-web", - "version": "3.0.15", + "version": "3.1.0", "private": true, "homepage": "https://movie-web.app", "dependencies": { From 8acf4ef478a669f192ea4bc66c4b654b2dcd8212 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 22 Jun 2023 20:59:31 +0200 Subject: [PATCH 56/56] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f7c4bf9..3228d02f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movie-web", - "version": "3.1.0", + "version": "4.0.0", "private": true, "homepage": "https://movie-web.app", "dependencies": {