From 40e45ae103f29796e7725e368f3605f8d292ce56 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Mon, 12 Jun 2023 20:06:46 +0200
Subject: [PATCH 01/55] 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<MWQuery, MWMediaMeta[]>();
+cache.setCompare((a, b) => {
+  return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim();
+});
+cache.initialize();
+
+export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
+  if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
+  const { searchQuery, type } = query;
+
+  const contentType = mediaTypeToTTV(type);
+
+  const results = await Trakt.search(searchQuery, contentType);
+  cache.set(query, results, 3600);
+  return results;
+}
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<T>(url: string): Promise<T> {
+    const res = await mwFetch<any>(url, {
+      headers: Tmdb.headers,
+      baseURL: Tmdb.baseURL,
+    });
+    return res;
+  }
+
+  public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async (
+    id: string,
+    type: MWMediaType
+  ) => {
+    let data;
+
+    switch (type) {
+      case "movie":
+        data = await Tmdb.get<TMDBMovieData>(`/movie/${id}`);
+        break;
+      case "series":
+        data = await Tmdb.get<TMDBShowData>(`/tv/${id}`);
+        break;
+      default:
+        throw new Error("Invalid media type");
+    }
+
+    return data;
+  };
+
+  public static getMediaPoster(posterPath: string | null): string | undefined {
+    if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`;
+  }
+
+  /* public static async getMetaFromId(
+    type: MWMediaType,
+    id: string,
+    seasonId?: string
+  ): Promise<DetailedMeta | null> {
+    console.log("getMetaFromId", type, id, seasonId);
+
+    const details = await Tmdb.getMediaDetails(id, type);
+
+    if (!details) return null;
+
+    let imdbId;
+    if (type === MWMediaType.MOVIE) {
+      imdbId = (details as TMDBMovieData).imdb_id ?? undefined;
+    }
+
+    if (!meta.length) return null;
+
+    console.log(meta);
+
+    return {
+      meta,
+      imdbId,
+      tmdbId: id,
+    };
+  } */
+}
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<TTVMediaResult> {
+  const type = TTVMediaToMediaType(result.type);
+  const media = result[result.type];
+
+  if (!media) throw new Error("invalid result");
+
+  const details = await Tmdb.getMediaDetails(
+    media.ids.tmdb.toString(),
+    TTVMediaToMediaType(result.type)
+  );
+  console.log(details);
+
+  const seasons =
+    type === MWMediaType.SERIES
+      ? (details as TMDBShowData).seasons?.map((v) => ({
+          id: v.id,
+          title: v.name,
+          season_number: v.season_number,
+        }))
+      : undefined;
+
+  return {
+    title: media.title,
+    poster: Tmdb.getMediaPoster(details.poster_path),
+    id: media.ids.trakt,
+    original_release_year: media.year,
+    ttv_entity_id: media.ids.slug,
+    object_type: mediaTypeToTTV(type),
+    seasons,
+  };
+}
+
+export abstract class Trakt {
+  private static baseURL = "https://api.trakt.tv";
+
+  private static headers = {
+    "Content-Type": "application/json",
+    "trakt-api-version": "2",
+    "trakt-api-key": conf().TRAKT_CLIENT_ID,
+  };
+
+  private static async get<T>(url: string): Promise<T> {
+    const res = await mwFetch<any>(url, {
+      headers: Trakt.headers,
+      baseURL: Trakt.baseURL,
+    });
+    return res;
+  }
+
+  public static async search(
+    query: string,
+    type: "movie" | "show"
+  ): Promise<MWMediaMeta[]> {
+    const data = await Trakt.get<TTVSearchResult[]>(
+      `/search/${type}?query=${encodeURIComponent(query)}`
+    );
+
+    const formatted = await Promise.all(
+      // eslint-disable-next-line no-return-await
+      data.map(async (v) => await formatTTVSearchResult(v))
+    );
+    return formatted.map((v) => formatTTVMeta(v));
+  }
+
+  public static async getMetaFromId(
+    type: MWMediaType,
+    id: string,
+    seasonId?: string
+  ): Promise<DetailedMeta | null> {
+    console.log("getMetaFromId", type, id, seasonId);
+    return null;
+  }
+}
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<TMDBShowData | TMDBMovieData>;
+
+export interface TMDBMediaStatic {
+  getMediaDetails(
+    id: string,
+    type: MWMediaType.SERIES
+  ): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
+}

From dfe67157d42308c5ce421ebb3a852a306d6a9716 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Mon, 12 Jun 2023 20:17:42 +0200
Subject: [PATCH 02/55] 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<MWQuery, MWMediaMeta[]>();
 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<T> = {
-  items: T[];
-  page: number;
-  page_size: number;
-  total_pages: number;
-  total_results: number;
-};
-
 export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
   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<JWPage<JWMediaResult>>(
-    "/content/titles/en_US/popular",
-    {
-      baseURL: JW_API_BASE,
-      params: {
-        body: JSON.stringify(body),
-      },
-    }
-  );
-
-  const returnData = data.items.map<MWMediaMeta>((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<MWQuery, MWMediaMeta[]>();
-cache.setCompare((a, b) => {
-  return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim();
-});
-cache.initialize();
-
-export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
-  if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
-  const { searchQuery, type } = query;
-
-  const contentType = mediaTypeToTTV(type);
-
-  const results = await Trakt.search(searchQuery, contentType);
-  cache.set(query, results, 3600);
-  return results;
-}
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<MWQuery, MWMediaMeta[]>();
+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<T> = {
+  items: T[];
+  page: number;
+  page_size: number;
+  total_pages: number;
+  total_results: number;
+};
+
+export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
+  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<JWPage<JWMediaResult>>(
+    "/content/titles/en_US/popular",
+    {
+      baseURL: JW_API_BASE,
+      params: {
+        body: JSON.stringify(body),
+      },
+    }
+  );
+
+  const returnData = data.items.map<MWMediaMeta>((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<TMDBShowData | TMDBMovieData>;
+
+export interface TMDBMediaStatic {
+  getMediaDetails(
+    id: string,
+    type: MWMediaType.SERIES
+  ): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
+}
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<TMDBShowData | TMDBMovieData>;
-
-export interface TMDBMediaStatic {
-  getMediaDetails(
-    id: string,
-    type: MWMediaType.SERIES
-  ): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
-}
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<keyof Config, undefined | string> = {
@@ -28,6 +30,7 @@ const env: Record<keyof Config, undefined | string> = {
   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 <adrifcastr@gmail.com>
Date: Mon, 12 Jun 2023 21:25:24 +0200
Subject: [PATCH 03/55] 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<T> = {
+  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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 10:41:54 +0200
Subject: [PATCH 04/55] 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<DetailedMeta | null> {
+  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<DetailedMeta | null> {
   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<MWMediaMeta[]> {
   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<DetailedMeta | null> {
-    console.log("getMetaFromId", type, id, seasonId);
-
-    const details = await Tmdb.getMediaDetails(id, type);
-
-    if (!details) return null;
-
-    let imdbId;
-    if (type === MWMediaType.MOVIE) {
-      imdbId = (details as TMDBMovieData).imdb_id ?? undefined;
-    }
-
-    if (!meta.length) return null;
-
-    console.log(meta);
-
-    return {
-      meta,
-      imdbId,
-      tmdbId: id,
-    };
-  } */
 }
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<DetailedMeta | null> {
-    console.log("getMetaFromId", type, id, seasonId);
-    return null;
+  public static async searchById(
+    tmdbId: string,
+    type: "movie" | "show"
+  ): Promise<TTVMediaResult> {
+    const data = await Trakt.get<TTVSearchResult[]>(
+      `/search/tmdb/${tmdbId}?type=${type}`
+    );
+
+    const formatted = await Promise.all(
+      // eslint-disable-next-line no-return-await
+      data.map(async (v) => await formatTTVSearchResult(v))
+    );
+    return formatted[0];
+  }
+
+  public static async getEpisodes(
+    slug: string,
+    season: number
+  ): Promise<TTVEpisodeShort[]> {
+    const data = await Trakt.get<TTVEpisodeResult[]>(
+      `/shows/${slug}/seasons/${season}`
+    );
+
+    return data.map((e) => ({
+      id: e.ids.tmdb,
+      episode_number: e.number,
+      title: e.title,
+    }));
   }
 }
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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 11:01:07 +0200
Subject: [PATCH 05/55] 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<SelectedMediaData | null>(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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 14:06:37 +0200
Subject: [PATCH 06/55] 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<string | undefined> {
+  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 (
     <SettingsProvider>
       <WatchedContextProvider>

From a7af04530805d079f19aea5618c8989e8cab3105 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 14:20:33 +0200
Subject: [PATCH 07/55] 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<SelectedMediaData | null>(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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 14:25:31 +0200
Subject: [PATCH 08/55] 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<MWQuery, MWMediaMeta[]>();
-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<T> = {
-  items: T[];
-  page: number;
-  page_size: number;
-  total_pages: number;
-  total_results: number;
-};
-
-export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
-  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<JWPage<JWMediaResult>>(
-    "/content/titles/en_US/popular",
-    {
-      baseURL: JW_API_BASE,
-      params: {
-        body: JSON.stringify(body),
-      },
-    }
-  );
-
-  const returnData = data.items.map<MWMediaMeta>((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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 21:23:47 +0200
Subject: [PATCH 09/55] 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<DetailedMeta | null> {
-  const result = await Trakt.searchById(id, mediaTypeToJW(type));
-  if (!result) return null;
-  const details = await Tmdb.getMediaDetails(id, type);
+  const details = await Tmdb.getMediaDetails(id, mediaTypeToTMDB(type));
 
   if (!details) return null;
 
@@ -59,15 +57,15 @@ export async function getMetaFromId(
     imdbId = (details as TMDBMovieData).imdb_id ?? undefined;
   }
 
-  let seasonData: TTVSeasonMetaResult | undefined;
+  let seasonData: TMDBSeasonMetaResult | undefined;
 
   if (type === MWMediaType.SERIES) {
     const seasons = (details as TMDBShowData).seasons;
     const season =
       seasons?.find((v) => v.id.toString() === seasonId) ?? seasons?.[0];
 
-    const episodes = await Trakt.getEpisodes(
-      result.ttv_entity_id,
+    const episodes = await Tmdb.getEpisodes(
+      details.id.toString(),
       season?.season_number ?? 1
     );
 
@@ -81,10 +79,27 @@ export async function getMetaFromId(
     }
   }
 
-  const meta = formatTTVMeta(result, seasonData);
-  if (!meta) return null;
+  const tmdbmeta: TMDBMediaResult = {
+    id: details.id,
+    title:
+      type === MWMediaType.MOVIE
+        ? (details as TMDBMovieData).title
+        : (details as TMDBShowData).name,
+    object_type: mediaTypeToTMDB(type),
+    seasons: (details as TMDBShowData).seasons.map((v) => ({
+      id: v.id,
+      season_number: v.season_number,
+      title: v.name,
+    })),
+    poster: (details as TMDBMovieData).poster_path ?? undefined,
+    original_release_year:
+      type === MWMediaType.MOVIE
+        ? Number((details as TMDBMovieData).release_date?.split("-")[0])
+        : Number((details as TMDBShowData).first_air_date?.split("-")[0]),
+  };
 
-  console.log(meta);
+  const meta = formatTMDBMeta(tmdbmeta, seasonData);
+  if (!meta) return null;
 
   return {
     meta,
@@ -143,18 +158,18 @@ export async function getLegacyMetaFromId(
   };
 }
 
-export function TTVMediaToId(media: MWMediaMeta): string {
-  return ["TTV", mediaTypeToTTV(media.type), media.id].join("-");
+export function TMDBMediaToId(media: MWMediaMeta): string {
+  return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
 }
 
-export function decodeTTVId(
+export function decodeTMDBId(
   paramId: string
 ): { id: string; type: MWMediaType } | null {
   const [prefix, type, id] = paramId.split("-", 3);
-  if (prefix !== "TTV") return null;
+  if (prefix !== "tmdb") return null;
   let mediaType;
   try {
-    mediaType = TTVMediaToMediaType(type);
+    mediaType = TMDBMediaToMediaType(type);
   } catch {
     return null;
   }
@@ -170,11 +185,11 @@ export async function convertLegacyUrl(
   if (url.startsWith("/media/JW")) {
     const urlParts = url.split("/").slice(2);
     const [, type, id] = urlParts[0].split("-", 3);
-    const meta = await getLegacyMetaFromId(TTVMediaToMediaType(type), id);
+    const meta = await getLegacyMetaFromId(TMDBMediaToMediaType(type), id);
     if (!meta) return undefined;
     const tmdbId = meta.tmdbId;
     if (!tmdbId) return undefined;
-    return `/media/TTV-${type}-${tmdbId}`;
+    return `/media/tmdb-${type}-${tmdbId}`;
   }
   return undefined;
 }
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<MWQuery, MWMediaMeta[]>();
@@ -13,10 +18,17 @@ export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
   if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
   const { searchQuery, type } = query;
 
-  const contentType = mediaTypeToTTV(type);
+  const data = await Tmdb.searchMedia(searchQuery, mediaTypeToTMDB(type));
+  const results = await Promise.all(
+    data.results.map(async (v) => {
+      const formattedResult = await formatTMDBSearchResult(
+        v,
+        mediaTypeToTMDB(type)
+      );
+      return formatTMDBMeta(formattedResult);
+    })
+  );
 
-  const results = await Trakt.search(searchQuery, contentType);
-  console.log(results[0]);
   cache.set(query, results, 3600);
   return results;
 }
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<TMDBMovieResponse>(
+          `search/movie?query=${query}&include_adult=true&language=en-US&page=1`
+        );
+        break;
+      case "show":
+        data = await Tmdb.get<TMDBShowResponse>(
+          `search/tv?query=${query}&include_adult=true&language=en-US&page=1`
+        );
+        break;
+      default:
+        throw new Error("Invalid media type");
+    }
+
+    return data;
+  };
+
   public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async (
     id: string,
-    type: MWMediaType
+    type: TMDBContentTypes
   ) => {
     let data;
 
@@ -34,7 +145,7 @@ export abstract class Tmdb {
       case "movie":
         data = await Tmdb.get<TMDBMovieData>(`/movie/${id}`);
         break;
-      case "series":
+      case "show":
         data = await Tmdb.get<TMDBShowData>(`/tv/${id}`);
         break;
       default:
@@ -47,4 +158,48 @@ export abstract class Tmdb {
   public static getMediaPoster(posterPath: string | null): string | undefined {
     if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`;
   }
+
+  public static async getEpisodes(
+    id: string,
+    season: number
+  ): Promise<TMDBEpisodeShort[]> {
+    const data = await Tmdb.get<TMDBSeason>(`/tv/${id}/season/${season}`);
+    return data.episodes.map((e) => ({
+      id: e.id,
+      episode_number: e.episode_number,
+      title: e.name,
+    }));
+  }
+}
+
+export async function formatTMDBSearchResult(
+  result: TMDBShowResult | TMDBMovieResult,
+  mediatype: TMDBContentTypes
+): Promise<TMDBMediaResult> {
+  const type = TMDBMediaToMediaType(mediatype);
+  const details = await Tmdb.getMediaDetails(result.id.toString(), mediatype);
+
+  const seasons =
+    type === MWMediaType.SERIES
+      ? (details as TMDBShowData).seasons?.map((v) => ({
+          id: v.id,
+          title: v.name,
+          season_number: v.season_number,
+        }))
+      : undefined;
+
+  return {
+    title:
+      type === MWMediaType.SERIES
+        ? (result as TMDBShowResult).name
+        : (result as TMDBMovieResult).title,
+    poster: Tmdb.getMediaPoster(details.poster_path),
+    id: result.id,
+    original_release_year:
+      type === MWMediaType.SERIES
+        ? Number((result as TMDBShowResult).first_air_date?.split("-")[0])
+        : Number((result as TMDBMovieResult).release_date?.split("-")[0]),
+    object_type: mediaTypeToTMDB(type),
+    seasons,
+  };
 }
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<TTVMediaResult> {
-  const type = TTVMediaToMediaType(result.type);
-  const media = result[result.type];
-
-  if (!media) throw new Error("invalid result");
-
-  const details = await Tmdb.getMediaDetails(
-    media.ids.tmdb.toString(),
-    TTVMediaToMediaType(result.type)
-  );
-
-  const seasons =
-    type === MWMediaType.SERIES
-      ? (details as TMDBShowData).seasons?.map((v) => ({
-          id: v.id,
-          title: v.name,
-          season_number: v.season_number,
-        }))
-      : undefined;
-
-  return {
-    title: media.title,
-    poster: Tmdb.getMediaPoster(details.poster_path),
-    id: media.ids.tmdb,
-    original_release_year: media.year,
-    ttv_entity_id: media.ids.slug,
-    object_type: mediaTypeToTTV(type),
-    seasons,
-  };
-}
-
-export abstract class Trakt {
-  private static baseURL = "https://api.trakt.tv";
-
-  private static headers = {
-    "Content-Type": "application/json",
-    "trakt-api-version": "2",
-    "trakt-api-key": conf().TRAKT_CLIENT_ID,
-  };
-
-  private static async get<T>(url: string): Promise<T> {
-    const res = await mwFetch<any>(url, {
-      headers: Trakt.headers,
-      baseURL: Trakt.baseURL,
-    });
-    return res;
-  }
-
-  public static async search(
-    query: string,
-    type: "movie" | "show"
-  ): Promise<MWMediaMeta[]> {
-    const data = await Trakt.get<TTVSearchResult[]>(
-      `/search/${type}?query=${encodeURIComponent(query)}`
-    );
-
-    const formatted = await Promise.all(
-      // eslint-disable-next-line no-return-await
-      data.map(async (v) => await formatTTVSearchResult(v))
-    );
-    return formatted.map((v) => formatTTVMeta(v));
-  }
-
-  public static async searchById(
-    tmdbId: string,
-    type: "movie" | "show"
-  ): Promise<TTVMediaResult> {
-    const data = await Trakt.get<TTVSearchResult[]>(
-      `/search/tmdb/${tmdbId}?type=${type}`
-    );
-
-    const formatted = await Promise.all(
-      // eslint-disable-next-line no-return-await
-      data.map(async (v) => await formatTTVSearchResult(v))
-    );
-    return formatted[0];
-  }
-
-  public static async getEpisodes(
-    slug: string,
-    season: number
-  ): Promise<TTVEpisodeShort[]> {
-    const data = await Trakt.get<TTVEpisodeResult[]>(
-      `/shows/${slug}/seasons/${season}`
-    );
-
-    return data.map((e) => ({
-      id: e.ids.tmdb,
-      episode_number: e.number,
-      title: e.title,
-    }));
-  }
-}
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<TMDBShowData | TMDBMovieData>;
 
 export interface TMDBMediaStatic {
-  getMediaDetails(
-    id: string,
-    type: MWMediaType.SERIES
-  ): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: "show"): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: "movie"): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: TMDBContentTypes): TMDBMediaDetailsPromise;
 }
 
 export type JWContentTypes = "movie" | "show";
@@ -312,7 +282,7 @@ export type JWSeasonMetaResult = {
   episodes: JWEpisodeShort[];
 };
 
-export interface TTVEpisodeResult {
+export interface TMDBEpisodeResult {
   season: number;
   number: number;
   title: string;
@@ -323,3 +293,89 @@ export interface TTVEpisodeResult {
     tmdb: number;
   };
 }
+
+export interface TMDBShowResult {
+  adult: boolean;
+  backdrop_path: string | null;
+  genre_ids: number[];
+  id: number;
+  origin_country: string[];
+  original_language: string;
+  original_name: string;
+  overview: string;
+  popularity: number;
+  poster_path: string | null;
+  first_air_date: string;
+  name: string;
+  vote_average: number;
+  vote_count: number;
+}
+
+export interface TMDBShowResponse {
+  page: number;
+  results: TMDBShowResult[];
+  total_pages: number;
+  total_results: number;
+}
+
+export interface TMDBMovieResult {
+  adult: boolean;
+  backdrop_path: string | null;
+  genre_ids: number[];
+  id: number;
+  original_language: string;
+  original_title: string;
+  overview: string;
+  popularity: number;
+  poster_path: string | null;
+  release_date: string;
+  title: string;
+  video: boolean;
+  vote_average: number;
+  vote_count: number;
+}
+
+export interface TMDBMovieResponse {
+  page: number;
+  results: TMDBMovieResult[];
+  total_pages: number;
+  total_results: number;
+}
+
+export type TMDBSearchResultsPromise = Promise<
+  TMDBShowResponse | TMDBMovieResponse
+>;
+
+export interface TMDBSearchResultStatic {
+  searchMedia(query: string, type: TMDBContentTypes): TMDBSearchResultsPromise;
+  searchMedia(query: string, type: "movie"): TMDBSearchResultsPromise;
+  searchMedia(query: string, type: "show"): TMDBSearchResultsPromise;
+}
+
+export interface TMDBEpisode {
+  air_date: string;
+  episode_number: number;
+  id: number;
+  name: string;
+  overview: string;
+  production_code: string;
+  runtime: number;
+  season_number: number;
+  show_id: number;
+  still_path: string | null;
+  vote_average: number;
+  vote_count: number;
+  crew: any[];
+  guest_stars: any[];
+}
+
+export interface TMDBSeason {
+  _id: string;
+  air_date: string;
+  episodes: TMDBEpisode[];
+  name: string;
+  overview: string;
+  id: number;
+  poster_path: string | null;
+  season_number: number;
+}
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<SelectedMediaData | null>(null);
   const [exec, loading, error] = useLoading(
     async (mediaParams: string, seasonId?: string) => {
-      const data = decodeTTVId(mediaParams);
+      const data = decodeTMDBId(mediaParams);
       if (!data) return null;
       return getMetaFromId(data.type, data.id, seasonId);
     }

From c4afc372178669c0641049fb29c5da3e96f8feb8 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 21:26:58 +0200
Subject: [PATCH 10/55] 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<keyof Config, undefined | string> = {
@@ -30,7 +28,6 @@ const env: Record<keyof Config, undefined | string> = {
   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 <adrifcastr@gmail.com>
Date: Wed, 14 Jun 2023 07:48:31 +0200
Subject: [PATCH 11/55] 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<MWMediaMeta[]> {
     })
   );
 
+  console.log(results[0]);
+
   cache.set(query, results, 3600);
   return results;
 }

From 5d56b847c690a4a8c988026a9f33b6e2c4a072a3 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Wed, 14 Jun 2023 07:52:04 +0200
Subject: [PATCH 12/55] 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<MWMediaMeta[]> {
     })
   );
 
-  console.log(results[0]);
-
   cache.set(query, results, 3600);
   return results;
 }

From 74cc50cfa2d64013e485b48862d3f3f3a673fd9f Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 08:30:05 +0200
Subject: [PATCH 13/55] 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 <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 08:30:57 +0200
Subject: [PATCH 14/55] 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<TMDBMovieResponse>(
-          `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<TMDBShowResponse>(
-          `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 <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 11:06:24 +0200
Subject: [PATCH 15/55] 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 <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 22:13:19 +0200
Subject: [PATCH 16/55] 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 <adrifcastr@gmail.com>
Date: Fri, 16 Jun 2023 11:18:32 +0200
Subject: [PATCH 17/55] 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<TMDBExternalIds> {
+    let data;
+
+    switch (type) {
+      case "movie":
+        data = await Tmdb.get<TMDBMovieExternalIds>(
+          `/movie/${id}/external_ids`
+        );
+        break;
+      case "show":
+        data = await Tmdb.get<TMDBShowExternalIds>(`/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 a4bd9bb87a09da7e1880089816e578053746de30 Mon Sep 17 00:00:00 2001
From: frost768 <droidplus99@gmail.com>
Date: Sun, 18 Jun 2023 15:10:26 +0300
Subject: [PATCH 18/55] fix: language preference persistence

---
 src/index.tsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/index.tsx b/src/index.tsx
index 1bf99f70..36b1fb14 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -8,13 +8,14 @@ import { registerSW } from "virtual:pwa-register";
 import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
 import App from "@/setup/App";
 import { conf } from "@/setup/config";
+import i18n from "@/setup/i18n";
 
 import "@/setup/ga";
 import "@/setup/sentry";
-import "@/setup/i18n";
 import "@/setup/index.css";
 import "@/backend";
 import { initializeChromecast } from "./setup/chromecast";
+import { SettingsStore } from "./state/settings/store";
 import { initializeStores } from "./utils/storage";
 
 // initialize
@@ -30,6 +31,7 @@ registerSW({
 
 const LazyLoadedApp = React.lazy(async () => {
   await initializeStores();
+  i18n.changeLanguage(SettingsStore.get().language ?? "en");
   return {
     default: App,
   };

From 70852773f94c30ab47d9737bff8a9875f514c564 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Mon, 12 Jun 2023 20:06:46 +0200
Subject: [PATCH 19/55] 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<MWQuery, MWMediaMeta[]>();
+cache.setCompare((a, b) => {
+  return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim();
+});
+cache.initialize();
+
+export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
+  if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
+  const { searchQuery, type } = query;
+
+  const contentType = mediaTypeToTTV(type);
+
+  const results = await Trakt.search(searchQuery, contentType);
+  cache.set(query, results, 3600);
+  return results;
+}
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<T>(url: string): Promise<T> {
+    const res = await mwFetch<any>(url, {
+      headers: Tmdb.headers,
+      baseURL: Tmdb.baseURL,
+    });
+    return res;
+  }
+
+  public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async (
+    id: string,
+    type: MWMediaType
+  ) => {
+    let data;
+
+    switch (type) {
+      case "movie":
+        data = await Tmdb.get<TMDBMovieData>(`/movie/${id}`);
+        break;
+      case "series":
+        data = await Tmdb.get<TMDBShowData>(`/tv/${id}`);
+        break;
+      default:
+        throw new Error("Invalid media type");
+    }
+
+    return data;
+  };
+
+  public static getMediaPoster(posterPath: string | null): string | undefined {
+    if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`;
+  }
+
+  /* public static async getMetaFromId(
+    type: MWMediaType,
+    id: string,
+    seasonId?: string
+  ): Promise<DetailedMeta | null> {
+    console.log("getMetaFromId", type, id, seasonId);
+
+    const details = await Tmdb.getMediaDetails(id, type);
+
+    if (!details) return null;
+
+    let imdbId;
+    if (type === MWMediaType.MOVIE) {
+      imdbId = (details as TMDBMovieData).imdb_id ?? undefined;
+    }
+
+    if (!meta.length) return null;
+
+    console.log(meta);
+
+    return {
+      meta,
+      imdbId,
+      tmdbId: id,
+    };
+  } */
+}
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<TTVMediaResult> {
+  const type = TTVMediaToMediaType(result.type);
+  const media = result[result.type];
+
+  if (!media) throw new Error("invalid result");
+
+  const details = await Tmdb.getMediaDetails(
+    media.ids.tmdb.toString(),
+    TTVMediaToMediaType(result.type)
+  );
+  console.log(details);
+
+  const seasons =
+    type === MWMediaType.SERIES
+      ? (details as TMDBShowData).seasons?.map((v) => ({
+          id: v.id,
+          title: v.name,
+          season_number: v.season_number,
+        }))
+      : undefined;
+
+  return {
+    title: media.title,
+    poster: Tmdb.getMediaPoster(details.poster_path),
+    id: media.ids.trakt,
+    original_release_year: media.year,
+    ttv_entity_id: media.ids.slug,
+    object_type: mediaTypeToTTV(type),
+    seasons,
+  };
+}
+
+export abstract class Trakt {
+  private static baseURL = "https://api.trakt.tv";
+
+  private static headers = {
+    "Content-Type": "application/json",
+    "trakt-api-version": "2",
+    "trakt-api-key": conf().TRAKT_CLIENT_ID,
+  };
+
+  private static async get<T>(url: string): Promise<T> {
+    const res = await mwFetch<any>(url, {
+      headers: Trakt.headers,
+      baseURL: Trakt.baseURL,
+    });
+    return res;
+  }
+
+  public static async search(
+    query: string,
+    type: "movie" | "show"
+  ): Promise<MWMediaMeta[]> {
+    const data = await Trakt.get<TTVSearchResult[]>(
+      `/search/${type}?query=${encodeURIComponent(query)}`
+    );
+
+    const formatted = await Promise.all(
+      // eslint-disable-next-line no-return-await
+      data.map(async (v) => await formatTTVSearchResult(v))
+    );
+    return formatted.map((v) => formatTTVMeta(v));
+  }
+
+  public static async getMetaFromId(
+    type: MWMediaType,
+    id: string,
+    seasonId?: string
+  ): Promise<DetailedMeta | null> {
+    console.log("getMetaFromId", type, id, seasonId);
+    return null;
+  }
+}
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<TMDBShowData | TMDBMovieData>;
+
+export interface TMDBMediaStatic {
+  getMediaDetails(
+    id: string,
+    type: MWMediaType.SERIES
+  ): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
+}

From 63f26b81dec2bab16f6d19c0d502a05753d85497 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Mon, 12 Jun 2023 20:17:42 +0200
Subject: [PATCH 20/55] 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<MWQuery, MWMediaMeta[]>();
 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<T> = {
-  items: T[];
-  page: number;
-  page_size: number;
-  total_pages: number;
-  total_results: number;
-};
-
 export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
   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<JWPage<JWMediaResult>>(
-    "/content/titles/en_US/popular",
-    {
-      baseURL: JW_API_BASE,
-      params: {
-        body: JSON.stringify(body),
-      },
-    }
-  );
-
-  const returnData = data.items.map<MWMediaMeta>((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<MWQuery, MWMediaMeta[]>();
-cache.setCompare((a, b) => {
-  return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim();
-});
-cache.initialize();
-
-export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
-  if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
-  const { searchQuery, type } = query;
-
-  const contentType = mediaTypeToTTV(type);
-
-  const results = await Trakt.search(searchQuery, contentType);
-  cache.set(query, results, 3600);
-  return results;
-}
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<MWQuery, MWMediaMeta[]>();
+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<T> = {
+  items: T[];
+  page: number;
+  page_size: number;
+  total_pages: number;
+  total_results: number;
+};
+
+export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
+  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<JWPage<JWMediaResult>>(
+    "/content/titles/en_US/popular",
+    {
+      baseURL: JW_API_BASE,
+      params: {
+        body: JSON.stringify(body),
+      },
+    }
+  );
+
+  const returnData = data.items.map<MWMediaMeta>((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<TMDBShowData | TMDBMovieData>;
+
+export interface TMDBMediaStatic {
+  getMediaDetails(
+    id: string,
+    type: MWMediaType.SERIES
+  ): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
+}
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<TMDBShowData | TMDBMovieData>;
-
-export interface TMDBMediaStatic {
-  getMediaDetails(
-    id: string,
-    type: MWMediaType.SERIES
-  ): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
-}
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<keyof Config, undefined | string> = {
@@ -28,6 +30,7 @@ const env: Record<keyof Config, undefined | string> = {
   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 <adrifcastr@gmail.com>
Date: Mon, 12 Jun 2023 21:25:24 +0200
Subject: [PATCH 21/55] 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<T> = {
+  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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 10:41:54 +0200
Subject: [PATCH 22/55] 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<DetailedMeta | null> {
+  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<DetailedMeta | null> {
   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<MWMediaMeta[]> {
   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<DetailedMeta | null> {
-    console.log("getMetaFromId", type, id, seasonId);
-
-    const details = await Tmdb.getMediaDetails(id, type);
-
-    if (!details) return null;
-
-    let imdbId;
-    if (type === MWMediaType.MOVIE) {
-      imdbId = (details as TMDBMovieData).imdb_id ?? undefined;
-    }
-
-    if (!meta.length) return null;
-
-    console.log(meta);
-
-    return {
-      meta,
-      imdbId,
-      tmdbId: id,
-    };
-  } */
 }
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<DetailedMeta | null> {
-    console.log("getMetaFromId", type, id, seasonId);
-    return null;
+  public static async searchById(
+    tmdbId: string,
+    type: "movie" | "show"
+  ): Promise<TTVMediaResult> {
+    const data = await Trakt.get<TTVSearchResult[]>(
+      `/search/tmdb/${tmdbId}?type=${type}`
+    );
+
+    const formatted = await Promise.all(
+      // eslint-disable-next-line no-return-await
+      data.map(async (v) => await formatTTVSearchResult(v))
+    );
+    return formatted[0];
+  }
+
+  public static async getEpisodes(
+    slug: string,
+    season: number
+  ): Promise<TTVEpisodeShort[]> {
+    const data = await Trakt.get<TTVEpisodeResult[]>(
+      `/shows/${slug}/seasons/${season}`
+    );
+
+    return data.map((e) => ({
+      id: e.ids.tmdb,
+      episode_number: e.number,
+      title: e.title,
+    }));
   }
 }
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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 11:01:07 +0200
Subject: [PATCH 23/55] 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<SelectedMediaData | null>(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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 14:06:37 +0200
Subject: [PATCH 24/55] 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<string | undefined> {
+  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 (
     <SettingsProvider>
       <WatchedContextProvider>

From b5c330d4e3cd4b4aa77b3f3dc7cfcba02a785f7b Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 14:20:33 +0200
Subject: [PATCH 25/55] 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<SelectedMediaData | null>(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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 14:25:31 +0200
Subject: [PATCH 26/55] 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<MWQuery, MWMediaMeta[]>();
-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<T> = {
-  items: T[];
-  page: number;
-  page_size: number;
-  total_pages: number;
-  total_results: number;
-};
-
-export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
-  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<JWPage<JWMediaResult>>(
-    "/content/titles/en_US/popular",
-    {
-      baseURL: JW_API_BASE,
-      params: {
-        body: JSON.stringify(body),
-      },
-    }
-  );
-
-  const returnData = data.items.map<MWMediaMeta>((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 <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 21:23:47 +0200
Subject: [PATCH 27/55] 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<DetailedMeta | null> {
-  const result = await Trakt.searchById(id, mediaTypeToJW(type));
-  if (!result) return null;
-  const details = await Tmdb.getMediaDetails(id, type);
+  const details = await Tmdb.getMediaDetails(id, mediaTypeToTMDB(type));
 
   if (!details) return null;
 
@@ -59,15 +57,15 @@ export async function getMetaFromId(
     imdbId = (details as TMDBMovieData).imdb_id ?? undefined;
   }
 
-  let seasonData: TTVSeasonMetaResult | undefined;
+  let seasonData: TMDBSeasonMetaResult | undefined;
 
   if (type === MWMediaType.SERIES) {
     const seasons = (details as TMDBShowData).seasons;
     const season =
       seasons?.find((v) => v.id.toString() === seasonId) ?? seasons?.[0];
 
-    const episodes = await Trakt.getEpisodes(
-      result.ttv_entity_id,
+    const episodes = await Tmdb.getEpisodes(
+      details.id.toString(),
       season?.season_number ?? 1
     );
 
@@ -81,10 +79,27 @@ export async function getMetaFromId(
     }
   }
 
-  const meta = formatTTVMeta(result, seasonData);
-  if (!meta) return null;
+  const tmdbmeta: TMDBMediaResult = {
+    id: details.id,
+    title:
+      type === MWMediaType.MOVIE
+        ? (details as TMDBMovieData).title
+        : (details as TMDBShowData).name,
+    object_type: mediaTypeToTMDB(type),
+    seasons: (details as TMDBShowData).seasons.map((v) => ({
+      id: v.id,
+      season_number: v.season_number,
+      title: v.name,
+    })),
+    poster: (details as TMDBMovieData).poster_path ?? undefined,
+    original_release_year:
+      type === MWMediaType.MOVIE
+        ? Number((details as TMDBMovieData).release_date?.split("-")[0])
+        : Number((details as TMDBShowData).first_air_date?.split("-")[0]),
+  };
 
-  console.log(meta);
+  const meta = formatTMDBMeta(tmdbmeta, seasonData);
+  if (!meta) return null;
 
   return {
     meta,
@@ -143,18 +158,18 @@ export async function getLegacyMetaFromId(
   };
 }
 
-export function TTVMediaToId(media: MWMediaMeta): string {
-  return ["TTV", mediaTypeToTTV(media.type), media.id].join("-");
+export function TMDBMediaToId(media: MWMediaMeta): string {
+  return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
 }
 
-export function decodeTTVId(
+export function decodeTMDBId(
   paramId: string
 ): { id: string; type: MWMediaType } | null {
   const [prefix, type, id] = paramId.split("-", 3);
-  if (prefix !== "TTV") return null;
+  if (prefix !== "tmdb") return null;
   let mediaType;
   try {
-    mediaType = TTVMediaToMediaType(type);
+    mediaType = TMDBMediaToMediaType(type);
   } catch {
     return null;
   }
@@ -170,11 +185,11 @@ export async function convertLegacyUrl(
   if (url.startsWith("/media/JW")) {
     const urlParts = url.split("/").slice(2);
     const [, type, id] = urlParts[0].split("-", 3);
-    const meta = await getLegacyMetaFromId(TTVMediaToMediaType(type), id);
+    const meta = await getLegacyMetaFromId(TMDBMediaToMediaType(type), id);
     if (!meta) return undefined;
     const tmdbId = meta.tmdbId;
     if (!tmdbId) return undefined;
-    return `/media/TTV-${type}-${tmdbId}`;
+    return `/media/tmdb-${type}-${tmdbId}`;
   }
   return undefined;
 }
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<MWQuery, MWMediaMeta[]>();
@@ -13,10 +18,17 @@ export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
   if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
   const { searchQuery, type } = query;
 
-  const contentType = mediaTypeToTTV(type);
+  const data = await Tmdb.searchMedia(searchQuery, mediaTypeToTMDB(type));
+  const results = await Promise.all(
+    data.results.map(async (v) => {
+      const formattedResult = await formatTMDBSearchResult(
+        v,
+        mediaTypeToTMDB(type)
+      );
+      return formatTMDBMeta(formattedResult);
+    })
+  );
 
-  const results = await Trakt.search(searchQuery, contentType);
-  console.log(results[0]);
   cache.set(query, results, 3600);
   return results;
 }
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<TMDBMovieResponse>(
+          `search/movie?query=${query}&include_adult=true&language=en-US&page=1`
+        );
+        break;
+      case "show":
+        data = await Tmdb.get<TMDBShowResponse>(
+          `search/tv?query=${query}&include_adult=true&language=en-US&page=1`
+        );
+        break;
+      default:
+        throw new Error("Invalid media type");
+    }
+
+    return data;
+  };
+
   public static getMediaDetails: TMDBMediaStatic["getMediaDetails"] = async (
     id: string,
-    type: MWMediaType
+    type: TMDBContentTypes
   ) => {
     let data;
 
@@ -34,7 +145,7 @@ export abstract class Tmdb {
       case "movie":
         data = await Tmdb.get<TMDBMovieData>(`/movie/${id}`);
         break;
-      case "series":
+      case "show":
         data = await Tmdb.get<TMDBShowData>(`/tv/${id}`);
         break;
       default:
@@ -47,4 +158,48 @@ export abstract class Tmdb {
   public static getMediaPoster(posterPath: string | null): string | undefined {
     if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`;
   }
+
+  public static async getEpisodes(
+    id: string,
+    season: number
+  ): Promise<TMDBEpisodeShort[]> {
+    const data = await Tmdb.get<TMDBSeason>(`/tv/${id}/season/${season}`);
+    return data.episodes.map((e) => ({
+      id: e.id,
+      episode_number: e.episode_number,
+      title: e.name,
+    }));
+  }
+}
+
+export async function formatTMDBSearchResult(
+  result: TMDBShowResult | TMDBMovieResult,
+  mediatype: TMDBContentTypes
+): Promise<TMDBMediaResult> {
+  const type = TMDBMediaToMediaType(mediatype);
+  const details = await Tmdb.getMediaDetails(result.id.toString(), mediatype);
+
+  const seasons =
+    type === MWMediaType.SERIES
+      ? (details as TMDBShowData).seasons?.map((v) => ({
+          id: v.id,
+          title: v.name,
+          season_number: v.season_number,
+        }))
+      : undefined;
+
+  return {
+    title:
+      type === MWMediaType.SERIES
+        ? (result as TMDBShowResult).name
+        : (result as TMDBMovieResult).title,
+    poster: Tmdb.getMediaPoster(details.poster_path),
+    id: result.id,
+    original_release_year:
+      type === MWMediaType.SERIES
+        ? Number((result as TMDBShowResult).first_air_date?.split("-")[0])
+        : Number((result as TMDBMovieResult).release_date?.split("-")[0]),
+    object_type: mediaTypeToTMDB(type),
+    seasons,
+  };
 }
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<TTVMediaResult> {
-  const type = TTVMediaToMediaType(result.type);
-  const media = result[result.type];
-
-  if (!media) throw new Error("invalid result");
-
-  const details = await Tmdb.getMediaDetails(
-    media.ids.tmdb.toString(),
-    TTVMediaToMediaType(result.type)
-  );
-
-  const seasons =
-    type === MWMediaType.SERIES
-      ? (details as TMDBShowData).seasons?.map((v) => ({
-          id: v.id,
-          title: v.name,
-          season_number: v.season_number,
-        }))
-      : undefined;
-
-  return {
-    title: media.title,
-    poster: Tmdb.getMediaPoster(details.poster_path),
-    id: media.ids.tmdb,
-    original_release_year: media.year,
-    ttv_entity_id: media.ids.slug,
-    object_type: mediaTypeToTTV(type),
-    seasons,
-  };
-}
-
-export abstract class Trakt {
-  private static baseURL = "https://api.trakt.tv";
-
-  private static headers = {
-    "Content-Type": "application/json",
-    "trakt-api-version": "2",
-    "trakt-api-key": conf().TRAKT_CLIENT_ID,
-  };
-
-  private static async get<T>(url: string): Promise<T> {
-    const res = await mwFetch<any>(url, {
-      headers: Trakt.headers,
-      baseURL: Trakt.baseURL,
-    });
-    return res;
-  }
-
-  public static async search(
-    query: string,
-    type: "movie" | "show"
-  ): Promise<MWMediaMeta[]> {
-    const data = await Trakt.get<TTVSearchResult[]>(
-      `/search/${type}?query=${encodeURIComponent(query)}`
-    );
-
-    const formatted = await Promise.all(
-      // eslint-disable-next-line no-return-await
-      data.map(async (v) => await formatTTVSearchResult(v))
-    );
-    return formatted.map((v) => formatTTVMeta(v));
-  }
-
-  public static async searchById(
-    tmdbId: string,
-    type: "movie" | "show"
-  ): Promise<TTVMediaResult> {
-    const data = await Trakt.get<TTVSearchResult[]>(
-      `/search/tmdb/${tmdbId}?type=${type}`
-    );
-
-    const formatted = await Promise.all(
-      // eslint-disable-next-line no-return-await
-      data.map(async (v) => await formatTTVSearchResult(v))
-    );
-    return formatted[0];
-  }
-
-  public static async getEpisodes(
-    slug: string,
-    season: number
-  ): Promise<TTVEpisodeShort[]> {
-    const data = await Trakt.get<TTVEpisodeResult[]>(
-      `/shows/${slug}/seasons/${season}`
-    );
-
-    return data.map((e) => ({
-      id: e.ids.tmdb,
-      episode_number: e.number,
-      title: e.title,
-    }));
-  }
-}
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<TMDBShowData | TMDBMovieData>;
 
 export interface TMDBMediaStatic {
-  getMediaDetails(
-    id: string,
-    type: MWMediaType.SERIES
-  ): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType.MOVIE): TMDBMediaDetailsPromise;
-  getMediaDetails(id: string, type: MWMediaType): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: "show"): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: "movie"): TMDBMediaDetailsPromise;
+  getMediaDetails(id: string, type: TMDBContentTypes): TMDBMediaDetailsPromise;
 }
 
 export type JWContentTypes = "movie" | "show";
@@ -312,7 +282,7 @@ export type JWSeasonMetaResult = {
   episodes: JWEpisodeShort[];
 };
 
-export interface TTVEpisodeResult {
+export interface TMDBEpisodeResult {
   season: number;
   number: number;
   title: string;
@@ -323,3 +293,89 @@ export interface TTVEpisodeResult {
     tmdb: number;
   };
 }
+
+export interface TMDBShowResult {
+  adult: boolean;
+  backdrop_path: string | null;
+  genre_ids: number[];
+  id: number;
+  origin_country: string[];
+  original_language: string;
+  original_name: string;
+  overview: string;
+  popularity: number;
+  poster_path: string | null;
+  first_air_date: string;
+  name: string;
+  vote_average: number;
+  vote_count: number;
+}
+
+export interface TMDBShowResponse {
+  page: number;
+  results: TMDBShowResult[];
+  total_pages: number;
+  total_results: number;
+}
+
+export interface TMDBMovieResult {
+  adult: boolean;
+  backdrop_path: string | null;
+  genre_ids: number[];
+  id: number;
+  original_language: string;
+  original_title: string;
+  overview: string;
+  popularity: number;
+  poster_path: string | null;
+  release_date: string;
+  title: string;
+  video: boolean;
+  vote_average: number;
+  vote_count: number;
+}
+
+export interface TMDBMovieResponse {
+  page: number;
+  results: TMDBMovieResult[];
+  total_pages: number;
+  total_results: number;
+}
+
+export type TMDBSearchResultsPromise = Promise<
+  TMDBShowResponse | TMDBMovieResponse
+>;
+
+export interface TMDBSearchResultStatic {
+  searchMedia(query: string, type: TMDBContentTypes): TMDBSearchResultsPromise;
+  searchMedia(query: string, type: "movie"): TMDBSearchResultsPromise;
+  searchMedia(query: string, type: "show"): TMDBSearchResultsPromise;
+}
+
+export interface TMDBEpisode {
+  air_date: string;
+  episode_number: number;
+  id: number;
+  name: string;
+  overview: string;
+  production_code: string;
+  runtime: number;
+  season_number: number;
+  show_id: number;
+  still_path: string | null;
+  vote_average: number;
+  vote_count: number;
+  crew: any[];
+  guest_stars: any[];
+}
+
+export interface TMDBSeason {
+  _id: string;
+  air_date: string;
+  episodes: TMDBEpisode[];
+  name: string;
+  overview: string;
+  id: number;
+  poster_path: string | null;
+  season_number: number;
+}
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<SelectedMediaData | null>(null);
   const [exec, loading, error] = useLoading(
     async (mediaParams: string, seasonId?: string) => {
-      const data = decodeTTVId(mediaParams);
+      const data = decodeTMDBId(mediaParams);
       if (!data) return null;
       return getMetaFromId(data.type, data.id, seasonId);
     }

From 763de37e9eaa4f6933e631fcd4b06c5703d37a2e Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Tue, 13 Jun 2023 21:26:58 +0200
Subject: [PATCH 28/55] 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<keyof Config, undefined | string> = {
@@ -30,7 +28,6 @@ const env: Record<keyof Config, undefined | string> = {
   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 <adrifcastr@gmail.com>
Date: Wed, 14 Jun 2023 07:48:31 +0200
Subject: [PATCH 29/55] 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<MWMediaMeta[]> {
     })
   );
 
+  console.log(results[0]);
+
   cache.set(query, results, 3600);
   return results;
 }

From 06eb8e6b6d2b40778e7ae93797d919a08fa8ed52 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Wed, 14 Jun 2023 07:52:04 +0200
Subject: [PATCH 30/55] 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<MWMediaMeta[]> {
     })
   );
 
-  console.log(results[0]);
-
   cache.set(query, results, 3600);
   return results;
 }

From c9bac3ed68f2688ceabbeaac68bb845443b551b7 Mon Sep 17 00:00:00 2001
From: castdrian <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 08:30:05 +0200
Subject: [PATCH 31/55] 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 <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 08:30:57 +0200
Subject: [PATCH 32/55] 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<TMDBMovieResponse>(
-          `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<TMDBShowResponse>(
-          `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 <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 11:06:24 +0200
Subject: [PATCH 33/55] 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 <adrifcastr@gmail.com>
Date: Thu, 15 Jun 2023 22:13:19 +0200
Subject: [PATCH 34/55] 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 <adrifcastr@gmail.com>
Date: Fri, 16 Jun 2023 11:18:32 +0200
Subject: [PATCH 35/55] 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<TMDBExternalIds> {
+    let data;
+
+    switch (type) {
+      case "movie":
+        data = await Tmdb.get<TMDBMovieExternalIds>(
+          `/movie/${id}/external_ids`
+        );
+        break;
+      case "show":
+        data = await Tmdb.get<TMDBShowExternalIds>(`/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 <adrifcastr@gmail.com>
Date: Mon, 19 Jun 2023 17:03:12 +0200
Subject: [PATCH 36/55] 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<TMDBMediaResult> {
   const type = TMDBMediaToMediaType(mediatype);
-  const details = await Tmdb.getMediaDetails(result.id.toString(), mediatype);
-
-  const seasons =
-    type === MWMediaType.SERIES
-      ? (details as TMDBShowData).seasons?.map((v) => ({
-          id: v.id,
-          title: v.name,
-          season_number: v.season_number,
-        }))
-      : undefined;
 
   return {
     title:
       type === MWMediaType.SERIES
         ? (result as TMDBShowResult).name
         : (result as TMDBMovieResult).title,
-    poster: Tmdb.getMediaPoster(details.poster_path),
+    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 <adrifcastr@gmail.com>
Date: Tue, 20 Jun 2023 19:39:16 +0200
Subject: [PATCH 37/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 12:43:36 +0200
Subject: [PATCH 38/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 12:47:09 +0200
Subject: [PATCH 39/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 12:48:33 +0200
Subject: [PATCH 40/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 12:50:41 +0200
Subject: [PATCH 41/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 12:51:30 +0200
Subject: [PATCH 42/55] 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<MWMediaMeta[]> {
     })
   );
 
-  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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 13:07:33 +0200
Subject: [PATCH 43/55] 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<DetailedMeta | null> {
-  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<MWMediaMeta[]> {
   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<T>(url: string): Promise<T> {
-    const res = await mwFetch<any>(url, {
-      headers: Tmdb.headers,
-      baseURL: Tmdb.baseURL,
-    });
-    return res;
+async function get<T>(url: string): Promise<T> {
+  const res = await mwFetch<any>(url, {
+    headers,
+    baseURL,
+  });
+  return res;
+}
+
+export async function searchMedia(query: string, type: TMDBContentTypes) {
+  let data;
+
+  switch (type) {
+    case "movie":
+      data = await get<TMDBMovieResponse>(
+        `search/movie?query=${query}&include_adult=false&language=en-US&page=1`
+      );
+      break;
+    case "show":
+      data = await get<TMDBShowResponse>(
+        `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<TMDBMovieResponse>(
-          `search/movie?query=${query}&include_adult=false&language=en-US&page=1`
-        );
-        break;
-      case "show":
-        data = await Tmdb.get<TMDBShowResponse>(
-          `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<TMDBMovieData>(`/movie/${id}`);
-        break;
-      case "show":
-        data = await Tmdb.get<TMDBShowData>(`/tv/${id}`);
-        break;
-      default:
-        throw new Error("Invalid media type");
-    }
-
-    return data;
-  };
-
-  public static getMediaPoster(posterPath: string | null): string | undefined {
-    if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`;
+  switch (type) {
+    case "movie":
+      data = await get<TMDBMovieData>(`/movie/${id}`);
+      break;
+    case "show":
+      data = await get<TMDBShowData>(`/tv/${id}`);
+      break;
+    default:
+      throw new Error("Invalid media type");
   }
 
-  public static async getEpisodes(
-    id: string,
-    season: number
-  ): Promise<TMDBEpisodeShort[]> {
-    const data = await Tmdb.get<TMDBSeason>(`/tv/${id}/season/${season}`);
-    return data.episodes.map((e) => ({
-      id: e.id,
-      episode_number: e.episode_number,
-      title: e.name,
-    }));
+  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<TMDBEpisodeShort[]> {
+  const data = await get<TMDBSeason>(`/tv/${id}/season/${season}`);
+  return data.episodes.map((e) => ({
+    id: e.id,
+    episode_number: e.episode_number,
+    title: e.name,
+  }));
+}
+
+export async function getExternalIds(
+  id: string,
+  type: TMDBContentTypes
+): Promise<TMDBExternalIds> {
+  let data;
+
+  switch (type) {
+    case "movie":
+      data = await get<TMDBMovieExternalIds>(`/movie/${id}/external_ids`);
+      break;
+    case "show":
+      data = await get<TMDBShowExternalIds>(`/tv/${id}/external_ids`);
+      break;
+    default:
+      throw new Error("Invalid media type");
   }
 
-  public static async getExternalIds(
-    id: string,
-    type: TMDBContentTypes
-  ): Promise<TMDBExternalIds> {
-    let data;
-
-    switch (type) {
-      case "movie":
-        data = await Tmdb.get<TMDBMovieExternalIds>(
-          `/movie/${id}/external_ids`
-        );
-        break;
-      case "show":
-        data = await Tmdb.get<TMDBShowExternalIds>(`/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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 13:23:39 +0200
Subject: [PATCH 44/55] 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<MWQuery, MWMediaMeta[]>();
 cache.setCompare((a, b) => {
@@ -18,7 +19,9 @@ export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
   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<T>(url: string): Promise<T> {
   return res;
 }
 
-export async function searchMedia(query: string, type: TMDBContentTypes) {
+export async function searchMedia(
+  query: string,
+  type: TMDBContentTypes
+): Promise<TMDBMovieResponse | TMDBShowResponse> {
   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<T> = {
+  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<TMDBShowData | TMDBMovieData>;
-
-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<T> = {
-  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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 13:26:03 +0200
Subject: [PATCH 45/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 13:31:50 +0200
Subject: [PATCH 46/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 13:38:48 +0200
Subject: [PATCH 47/55] 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 13:54:34 +0200
Subject: [PATCH 48/55] 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<MWQuery, MWMediaMeta[]>();
 cache.setCompare((a, b) => {
@@ -19,18 +18,11 @@ export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
   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> {
+): 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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 14:04:37 +0200
Subject: [PATCH 49/55] 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<T>(url: string): Promise<T> {
-  const res = await mwFetch<any>(url, {
+async function get<T>(url: string, params?: object): Promise<T> {
+  const res = await mwFetch<any>(encodeURI(url), {
     headers,
     baseURL,
+    params: {
+      ...params,
+    },
   });
   return res;
 }
@@ -117,14 +120,20 @@ export async function searchMedia(
 
   switch (type) {
     case "movie":
-      data = await get<TMDBMovieResponse>(
-        `search/movie?query=${query}&include_adult=false&language=en-US&page=1`
-      );
+      data = await get<TMDBMovieResponse>("search/movie", {
+        query,
+        include_adult: false,
+        language: "en-US",
+        page: 1,
+      });
       break;
     case "show":
-      data = await get<TMDBShowResponse>(
-        `search/tv?query=${query}&include_adult=false&language=en-US&page=1`
-      );
+      data = await get<TMDBShowResponse>("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 <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 15:14:48 +0200
Subject: [PATCH 50/55] 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,
                 })}
               </p>
@@ -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 <span>{content}</span>;
   return (

From 394271857f9fb78eda78ef0b26745680a0d8d2b3 Mon Sep 17 00:00:00 2001
From: adrifcastr <adrifcastr@gmail.com>
Date: Wed, 21 Jun 2023 18:16:41 +0200
Subject: [PATCH 51/55] 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<string | undefined> {
+  const data = await get<ExternalIdMovieSearchResult>(`/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<string | null>(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 <Redirect to={redirectUrl} />;
+  }
+
+  // eslint-disable-next-line react/jsx-no-useless-fragment
+  return <>{children}</>;
+};
+
+function App() {
   return (
     <SettingsProvider>
       <WatchedContextProvider>
         <BookmarkContextProvider>
           <BannerContextProvider>
             <Layout>
-              <Switch>
-                {/* functional routes */}
-                <Route exact path="/v2-migration" component={V2MigrationView} />
-                <Route exact path="/">
-                  <Redirect to={`/search/${MWMediaType.MOVIE}`} />
-                </Route>
+              <LegacyUrlView>
+                <Switch>
+                  {/* functional routes */}
+                  <Route
+                    exact
+                    path="/v2-migration"
+                    component={V2MigrationView}
+                  />
+                  <Route exact path="/">
+                    <Redirect to={`/search/${MWMediaType.MOVIE}`} />
+                  </Route>
 
-                {/* pages */}
-                <Route exact path="/media/:media" component={MediaView} />
-                <Route
-                  exact
-                  path="/media/:media/:season/:episode"
-                  component={MediaView}
-                />
-                <Route
-                  exact
-                  path="/search/:type/:query?"
-                  component={SearchView}
-                />
+                  {/* pages */}
+                  <Route exact path="/media/:media" component={MediaView} />
+                  <Route
+                    exact
+                    path="/media/:media/:season/:episode"
+                    component={MediaView}
+                  />
+                  <Route
+                    exact
+                    path="/search/:type/:query?"
+                    component={SearchView}
+                  />
 
-                {/* other */}
-                <Route
-                  exact
-                  path="/dev"
-                  component={lazy(
-                    () => import("@/views/developer/DeveloperView")
-                  )}
-                />
-                <Route
-                  exact
-                  path="/dev/video"
-                  component={lazy(
-                    () => import("@/views/developer/VideoTesterView")
-                  )}
-                />
-                {/* developer routes that can abuse workers are disabled in production */}
-                {process.env.NODE_ENV === "development" ? (
-                  <>
-                    <Route
-                      exact
-                      path="/dev/test"
-                      component={lazy(
-                        () => import("@/views/developer/TestView")
-                      )}
-                    />
+                  {/* other */}
+                  <Route
+                    exact
+                    path="/dev"
+                    component={lazy(
+                      () => import("@/views/developer/DeveloperView")
+                    )}
+                  />
+                  <Route
+                    exact
+                    path="/dev/video"
+                    component={lazy(
+                      () => import("@/views/developer/VideoTesterView")
+                    )}
+                  />
+                  {/* developer routes that can abuse workers are disabled in production */}
+                  {process.env.NODE_ENV === "development" ? (
+                    <>
+                      <Route
+                        exact
+                        path="/dev/test"
+                        component={lazy(
+                          () => import("@/views/developer/TestView")
+                        )}
+                      />
 
-                    <Route
-                      exact
-                      path="/dev/providers"
-                      component={lazy(
-                        () => import("@/views/developer/ProviderTesterView")
-                      )}
-                    />
-                    <Route
-                      exact
-                      path="/dev/embeds"
-                      component={lazy(
-                        () => import("@/views/developer/EmbedTesterView")
-                      )}
-                    />
-                  </>
-                ) : null}
-                <Route path="*" component={NotFoundPage} />
-              </Switch>
+                      <Route
+                        exact
+                        path="/dev/providers"
+                        component={lazy(
+                          () => import("@/views/developer/ProviderTesterView")
+                        )}
+                      />
+                      <Route
+                        exact
+                        path="/dev/embeds"
+                        component={lazy(
+                          () => import("@/views/developer/EmbedTesterView")
+                        )}
+                      />
+                    </>
+                  ) : null}
+                  <Route path="*" component={NotFoundPage} />
+                </Switch>
+              </LegacyUrlView>
             </Layout>
           </BannerContextProvider>
         </BookmarkContextProvider>

From f892a3037f39a1a685f5f1bf8db8babaf458c72d Mon Sep 17 00:00:00 2001
From: mrjvs <jellevs@gmail.com>
Date: Wed, 21 Jun 2023 21:35:25 +0200
Subject: [PATCH 52/55] 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<string | undefined> {
-  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<string | null>(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 <Redirect to={redirectUrl} />;
-  }
-
-  // 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() {
         <BookmarkContextProvider>
           <BannerContextProvider>
             <Layout>
-              <LegacyUrlView>
-                <Switch>
-                  {/* functional routes */}
-                  <Route
-                    exact
-                    path="/v2-migration"
-                    component={V2MigrationView}
-                  />
-                  <Route exact path="/">
-                    <Redirect to={`/search/${MWMediaType.MOVIE}`} />
-                  </Route>
+              <Switch>
+                {/* functional routes */}
+                <Route exact path="/v2-migration" component={V2MigrationView} />
+                <Route exact path="/">
+                  <Redirect to={`/search/${MWMediaType.MOVIE}`} />
+                </Route>
 
-                  {/* pages */}
-                  <Route exact path="/media/:media" component={MediaView} />
-                  <Route
-                    exact
-                    path="/media/:media/:season/:episode"
-                    component={MediaView}
-                  />
-                  <Route
-                    exact
-                    path="/search/:type/:query?"
-                    component={SearchView}
-                  />
+                {/* pages */}
+                <Route exact path="/media/:media">
+                  <LegacyUrlView>
+                    <MediaView />
+                  </LegacyUrlView>
+                </Route>
+                <Route exact path="/media/:media/:season/:episode">
+                  <LegacyUrlView>
+                    <MediaView />
+                  </LegacyUrlView>
+                </Route>
+                <Route
+                  exact
+                  path="/search/:type/:query?"
+                  component={SearchView}
+                />
 
-                  {/* other */}
-                  <Route
-                    exact
-                    path="/dev"
-                    component={lazy(
-                      () => import("@/views/developer/DeveloperView")
-                    )}
-                  />
-                  <Route
-                    exact
-                    path="/dev/video"
-                    component={lazy(
-                      () => import("@/views/developer/VideoTesterView")
-                    )}
-                  />
-                  {/* developer routes that can abuse workers are disabled in production */}
-                  {process.env.NODE_ENV === "development" ? (
-                    <>
-                      <Route
-                        exact
-                        path="/dev/test"
-                        component={lazy(
-                          () => import("@/views/developer/TestView")
-                        )}
-                      />
+                {/* other */}
+                <Route
+                  exact
+                  path="/dev"
+                  component={lazy(
+                    () => import("@/views/developer/DeveloperView")
+                  )}
+                />
+                <Route
+                  exact
+                  path="/dev/video"
+                  component={lazy(
+                    () => import("@/views/developer/VideoTesterView")
+                  )}
+                />
+                {/* developer routes that can abuse workers are disabled in production */}
+                {process.env.NODE_ENV === "development" ? (
+                  <>
+                    <Route
+                      exact
+                      path="/dev/test"
+                      component={lazy(
+                        () => import("@/views/developer/TestView")
+                      )}
+                    />
 
-                      <Route
-                        exact
-                        path="/dev/providers"
-                        component={lazy(
-                          () => import("@/views/developer/ProviderTesterView")
-                        )}
-                      />
-                      <Route
-                        exact
-                        path="/dev/embeds"
-                        component={lazy(
-                          () => import("@/views/developer/EmbedTesterView")
-                        )}
-                      />
-                    </>
-                  ) : null}
-                  <Route path="*" component={NotFoundPage} />
-                </Switch>
-              </LegacyUrlView>
+                    <Route
+                      exact
+                      path="/dev/providers"
+                      component={lazy(
+                        () => import("@/views/developer/ProviderTesterView")
+                      )}
+                    />
+                    <Route
+                      exact
+                      path="/dev/embeds"
+                      component={lazy(
+                        () => import("@/views/developer/EmbedTesterView")
+                      )}
+                    />
+                  </>
+                ) : null}
+                <Route path="*" component={NotFoundPage} />
+              </Switch>
             </Layout>
           </BannerContextProvider>
         </BookmarkContextProvider>

From 9fbba7ea55f2a8eedb193b402f56a17edd6ac51e Mon Sep 17 00:00:00 2001
From: adrifcastr <adrifcastr@gmail.com>
Date: Thu, 22 Jun 2023 10:47:14 +0200
Subject: [PATCH 53/55] 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<BookmarkStoreData>()
   .setKey("mw-bookmarks")
@@ -13,6 +14,12 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
   })
   .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<string | undefined> {
+  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<WatchedStoreData>()
@@ -21,6 +22,12 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
   })
   .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 <adrifcastr@gmail.com>
Date: Thu, 22 Jun 2023 10:48:00 +0200
Subject: [PATCH 54/55] 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<string | undefined> {
-  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 <adrifcastr@gmail.com>
Date: Thu, 22 Jun 2023 20:29:10 +0200
Subject: [PATCH 55/55] 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),
   };
 }