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[] = [
   {
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/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts
index 6b3b9a30..c09d8292 100644
--- a/src/backend/metadata/getmeta.ts
+++ b/src/backend/metadata/getmeta.ts
@@ -1,13 +1,28 @@
 import { FetchError } from "ofetch";
 
+import { formatJWMeta, mediaTypeToJW } from "./justwatch";
+import {
+  TMDBMediaToMediaType,
+  formatTMDBMeta,
+  getEpisodes,
+  getExternalIds,
+  getMediaDetails,
+  getMediaPoster,
+  getMovieFromExternalId,
+  mediaTypeToTMDB,
+} from "./tmdb";
 import {
   JWMediaResult,
   JWSeasonMetaResult,
   JW_API_BASE,
-  formatJWMeta,
-  mediaTypeToJW,
-} from "./justwatch";
-import { MWMediaMeta, MWMediaType } from "./types";
+} from "./types/justwatch";
+import { MWMediaMeta, MWMediaType } from "./types/mw";
+import {
+  TMDBMediaResult,
+  TMDBMovieData,
+  TMDBSeasonMetaResult,
+  TMDBShowData,
+} from "./types/tmdb";
 import { makeUrl, proxiedFetch } from "../helpers/fetch";
 
 type JWExternalIdType =
@@ -33,10 +48,92 @@ export interface DetailedMeta {
   tmdbId?: string;
 }
 
+export function formatTMDBMetaResult(
+  details: TMDBShowData | TMDBMovieData,
+  type: MWMediaType
+): TMDBMediaResult {
+  if (type === MWMediaType.MOVIE) {
+    const movie = details as TMDBMovieData;
+    return {
+      id: details.id,
+      title: movie.title,
+      object_type: mediaTypeToTMDB(type),
+      poster: getMediaPoster(movie.poster_path) ?? undefined,
+      original_release_year: new Date(movie.release_date).getFullYear(),
+    };
+  }
+  if (type === MWMediaType.SERIES) {
+    const show = details as TMDBShowData;
+    return {
+      id: details.id,
+      title: show.name,
+      object_type: mediaTypeToTMDB(type),
+      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: new Date(show.first_air_date).getFullYear(),
+    };
+  }
+
+  throw new Error("unsupported type");
+}
+
 export async function getMetaFromId(
   type: MWMediaType,
   id: string,
   seasonId?: string
+): Promise<DetailedMeta | null> {
+  const details = await getMediaDetails(id, mediaTypeToTMDB(type));
+
+  if (!details) return null;
+
+  const externalIds = await getExternalIds(id, mediaTypeToTMDB(type));
+  const imdbId = externalIds.imdb_id ?? undefined;
+
+  let seasonData: TMDBSeasonMetaResult | undefined;
+
+  if (type === MWMediaType.SERIES) {
+    const seasons = (details as TMDBShowData).seasons;
+
+    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
+      );
+
+      seasonData = {
+        id: selectedSeason.id.toString(),
+        season_number: selectedSeason.season_number,
+        title: selectedSeason.name,
+        episodes,
+      };
+    }
+  }
+
+  const tmdbmeta = formatTMDBMetaResult(details, type);
+  if (!tmdbmeta) return null;
+  const meta = formatTMDBMeta(tmdbmeta, seasonData);
+  if (!meta) return null;
+
+  return {
+    meta,
+    imdbId,
+    tmdbId: id,
+  };
+}
+
+export async function getLegacyMetaFromId(
+  type: MWMediaType,
+  id: string,
+  seasonId?: string
 ): Promise<DetailedMeta | null> {
   const queryType = mediaTypeToJW(type);
 
@@ -82,3 +179,55 @@ export async function getMetaFromId(
     tmdbId,
   };
 }
+
+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 function isLegacyUrl(url: string): boolean {
+  if (url.startsWith("/media/JW")) return true;
+  return false;
+}
+
+export async function convertLegacyUrl(
+  url: string
+): Promise<string | undefined> {
+  if (!isLegacyUrl(url)) return undefined;
+
+  const urlParts = url.split("/").slice(2);
+  const [, type, id] = urlParts[0].split("-", 3);
+
+  const mediaType = TMDBMediaToMediaType(type);
+  const meta = await getLegacyMetaFromId(mediaType, id);
+
+  if (!meta) return undefined;
+  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}`;
+  }
+}
diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts
index 5c79c1e3..724c4acf 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/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 10cbb285..0d8f561f 100644
--- a/src/backend/metadata/search.ts
+++ b/src/backend/metadata/search.ts
@@ -1,14 +1,12 @@
 import { SimpleCache } from "@/utils/cache";
 
 import {
-  JWContentTypes,
-  JWMediaResult,
-  JW_API_BASE,
-  formatJWMeta,
-  mediaTypeToJW,
-} from "./justwatch";
-import { MWMediaMeta, MWQuery } from "./types";
-import { proxiedFetch } from "../helpers/fetch";
+  formatTMDBMeta,
+  formatTMDBSearchResult,
+  mediaTypeToTMDB,
+  searchMedia,
+} from "./tmdb";
+import { MWMediaMeta, MWQuery } from "./types/mw";
 
 const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
 cache.setCompare((a, b) => {
@@ -16,44 +14,16 @@ 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 data = await searchMedia(searchQuery, mediaTypeToTMDB(type));
+  const results = data.results.map((v) => {
+    const formattedResult = formatTMDBSearchResult(v, mediaTypeToTMDB(type));
+    return formatTMDBMeta(formattedResult);
+  });
 
-  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;
+  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
new file mode 100644
index 00000000..db665528
--- /dev/null
+++ b/src/backend/metadata/tmdb.ts
@@ -0,0 +1,236 @@
+import { conf } from "@/setup/config";
+
+import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
+import {
+  ExternalIdMovieSearchResult,
+  TMDBContentTypes,
+  TMDBEpisodeShort,
+  TMDBExternalIds,
+  TMDBMediaResult,
+  TMDBMovieData,
+  TMDBMovieExternalIds,
+  TMDBMovieResponse,
+  TMDBMovieResult,
+  TMDBSeason,
+  TMDBSeasonMetaResult,
+  TMDBShowData,
+  TMDBShowExternalIds,
+  TMDBShowResponse,
+  TMDBShowResult,
+} from "./types/tmdb";
+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,
+  };
+}
+
+const baseURL = "https://api.themoviedb.org/3";
+
+const headers = {
+  accept: "application/json",
+  Authorization: `Bearer ${conf().TMDB_API_KEY}`,
+};
+
+async function get<T>(url: string, params?: object): Promise<T> {
+  const res = await mwFetch<any>(encodeURI(url), {
+    headers,
+    baseURL,
+    params: {
+      ...params,
+    },
+  });
+  return res;
+}
+
+export async function searchMedia(
+  query: string,
+  type: TMDBContentTypes
+): Promise<TMDBMovieResponse | TMDBShowResponse> {
+  let data;
+
+  switch (type) {
+    case "movie":
+      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,
+        include_adult: false,
+        language: "en-US",
+        page: 1,
+      });
+      break;
+    default:
+      throw new Error("Invalid media type");
+  }
+
+  return data;
+}
+
+export async function getMediaDetails(id: string, type: TMDBContentTypes) {
+  let data;
+
+  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");
+  }
+
+  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");
+  }
+
+  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
+): 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: movie.title,
+    poster: getMediaPoster(movie.poster_path),
+    id: movie.id,
+    original_release_year: new Date(movie.release_date).getFullYear(),
+    object_type: mediatype,
+  };
+}
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.ts b/src/backend/metadata/types/mw.ts
similarity index 89%
rename from src/backend/metadata/types.ts
rename to src/backend/metadata/types/mw.ts
index 2723fbe7..e7cc26fe 100644
--- a/src/backend/metadata/types.ts
+++ b/src/backend/metadata/types/mw.ts
@@ -45,3 +45,9 @@ export interface MWQuery {
   searchQuery: string;
   type: MWMediaType;
 }
+
+export interface DetailedMeta {
+  meta: MWMediaMeta;
+  imdbId?: string;
+  tmdbId?: string;
+}
diff --git a/src/backend/metadata/types/tmdb.ts b/src/backend/metadata/types/tmdb.ts
new file mode 100644
index 00000000..843786f4
--- /dev/null
+++ b/src/backend/metadata/types/tmdb.ts
@@ -0,0 +1,308 @@
+export type TMDBContentTypes = "movie" | "show";
+
+export type TMDBSeasonShort = {
+  title: string;
+  id: number;
+  season_number: number;
+};
+
+export type TMDBEpisodeShort = {
+  title: string;
+  id: number;
+  episode_number: number;
+};
+
+export type TMDBMediaResult = {
+  title: string;
+  poster?: string;
+  id: number;
+  original_release_year?: number;
+  object_type: TMDBContentTypes;
+  seasons?: TMDBSeasonShort[];
+};
+
+export type TMDBSeasonMetaResult = {
+  title: string;
+  id: string;
+  season_number: number;
+  episodes: TMDBEpisodeShort[];
+};
+
+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 interface TMDBEpisodeResult {
+  season: number;
+  number: number;
+  title: string;
+  ids: {
+    trakt: number;
+    tvdb: number;
+    imdb: string;
+    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 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;
+}
+
+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;
+
+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/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/MediaCard.tsx b/src/components/media/MediaCard.tsx
index 22865717..a153d8b4 100644
--- a/src/components/media/MediaCard.tsx
+++ b/src/components/media/MediaCard.tsx
@@ -1,8 +1,8 @@
 import { useTranslation } from "react-i18next";
 import { Link } from "react-router-dom";
 
-import { JWMediaToId } from "@/backend/metadata/justwatch";
-import { MWMediaMeta } from "@/backend/metadata/types";
+import { TMDBMediaToId } from "@/backend/metadata/getmeta";
+import { MWMediaMeta } from "@/backend/metadata/types/mw";
 import { DotList } from "@/components/text/DotList";
 
 import { IconPatch } from "../buttons/IconPatch";
@@ -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>
@@ -132,12 +132,17 @@ export function MediaCard(props: MediaCardProps) {
   const canLink = props.linkable && !props.closable;
 
   let link = canLink
-    ? `/media/${encodeURIComponent(JWMediaToId(props.media))}`
+    ? `/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 (
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/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,
   };
diff --git a/src/setup/App.tsx b/src/setup/App.tsx
index 992549e0..7d1847ae 100644
--- a/src/setup/App.tsx
+++ b/src/setup/App.tsx
@@ -1,7 +1,14 @@
-import { lazy } from "react";
-import { Redirect, Route, Switch } from "react-router-dom";
+import { ReactElement, lazy, useEffect } from "react";
+import {
+  Redirect,
+  Route,
+  Switch,
+  useHistory,
+  useLocation,
+} from "react-router-dom";
 
-import { MWMediaType } from "@/backend/metadata/types";
+import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
+import { MWMediaType } from "@/backend/metadata/types/mw";
 import { BannerContextProvider } from "@/hooks/useBanner";
 import { Layout } from "@/setup/Layout";
 import { BookmarkContextProvider } from "@/state/bookmark";
@@ -12,6 +19,22 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView";
 import { V2MigrationView } from "@/views/other/v2Migration";
 import { SearchView } from "@/views/search/SearchView";
 
+function LegacyUrlView({ children }: { children: ReactElement }) {
+  const location = useLocation();
+  const { replace } = useHistory();
+
+  useEffect(() => {
+    const url = location.pathname;
+    if (!isLegacyUrl(url)) return;
+    convertLegacyUrl(location.pathname).then((convertedUrl) => {
+      replace(convertedUrl ?? "/");
+    });
+  }, [location.pathname, replace]);
+
+  if (isLegacyUrl(location.pathname)) return null;
+  return children;
+}
+
 function App() {
   return (
     <SettingsProvider>
@@ -27,12 +50,16 @@ function App() {
                 </Route>
 
                 {/* pages */}
-                <Route exact path="/media/:media" component={MediaView} />
-                <Route
-                  exact
-                  path="/media/:media/:season/:episode"
-                  component={MediaView}
-                />
+                <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?"
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/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/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/migrations/v3.ts b/src/state/watched/migrations/v3.ts
new file mode 100644
index 00000000..71e0b182
--- /dev/null
+++ b/src/state/watched/migrations/v3.ts
@@ -0,0 +1,81 @@
+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> {
+  const meta = await getLegacyMetaFromId(type, id.toString());
+
+  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,
+      id: await migrateId(item.id, item.type),
+    })
+  );
+
+  return {
+    bookmarks: (await Promise.all(updatedBookmarks)).filter((item) => item.id),
+  };
+}
+
+export async function migrateV3Videos(old: any) {
+  const oldData = old;
+  if (!oldData) return;
+
+  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),
+  };
+
+  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: [],
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 c80045bd..66c9ae49 100644
--- a/src/video/components/popouts/EpisodeSelectionPopout.tsx
+++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx
@@ -2,9 +2,11 @@ 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 { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
+import { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta";
+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";
@@ -45,7 +47,7 @@ export function EpisodeSelectionPopout() {
         seasonId: sId,
         season: undefined,
       });
-      reqSeasonMeta(decodeJWId(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/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 c55211c7..6e1659a6 100644
--- a/src/views/media/MediaView.tsx
+++ b/src/views/media/MediaView.tsx
@@ -4,9 +4,15 @@ 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 { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
+import {
+  DetailedMeta,
+  decodeTMDBId,
+  getMetaFromId,
+} from "@/backend/metadata/getmeta";
+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";
@@ -181,7 +187,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 = decodeTMDBId(mediaParams);
       if (!data) return null;
       return getMetaFromId(data.type, data.id, seasonId);
     }
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";