From c4c7816543900173e17bde50826bd4f397ae21d6 Mon Sep 17 00:00:00 2001
From: mrjvs <jellevs@gmail.com>
Date: Thu, 22 Jun 2023 22:37:16 +0200
Subject: [PATCH 1/6] migrations but better

Co-authored-by: William Oldham <github@binaryoverload.co.uk>
---
 src/backend/metadata/tmdb.ts               | 29 ++++---
 src/backend/providers/gomovies.ts          |  2 +-
 src/backend/providers/superstream/index.ts |  2 +-
 src/state/bookmark/store.ts                |  2 +-
 src/state/watched/migrations/v3.ts         | 92 ++++++++++++----------
 src/state/watched/store.ts                 |  2 +-
 src/utils/storage.ts                       |  7 +-
 src/utils/typeguard.ts                     |  3 +
 8 files changed, 79 insertions(+), 60 deletions(-)
 create mode 100644 src/utils/typeguard.ts

diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts
index db665528..1c442028 100644
--- a/src/backend/metadata/tmdb.ts
+++ b/src/backend/metadata/tmdb.ts
@@ -143,21 +143,24 @@ export async function searchMedia(
   return data;
 }
 
-export async function getMediaDetails(id: string, type: TMDBContentTypes) {
-  let data;
+// Conditional type which for inferring the return type based on the content type
+type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
+  ? TMDBMovieData
+  : T extends "show"
+  ? TMDBShowData
+  : never;
 
-  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");
+export function getMediaDetails<
+  T extends TMDBContentTypes,
+  TReturn = MediaDetailReturn<T>
+>(id: string, type: T): Promise<TReturn> {
+  if (type === "movie") {
+    return get<TReturn>(`/movie/${id}`);
   }
-
-  return data;
+  if (type === "show") {
+    return get<TReturn>(`/tv/${id}`);
+  }
+  throw new Error("Invalid media type");
 }
 
 export function getMediaPoster(posterPath: string | null): string | undefined {
diff --git a/src/backend/providers/gomovies.ts b/src/backend/providers/gomovies.ts
index fdce289b..ddd43509 100644
--- a/src/backend/providers/gomovies.ts
+++ b/src/backend/providers/gomovies.ts
@@ -8,7 +8,7 @@ const gomoviesBase = "https://gomovies.sx";
 registerProvider({
   id: "gomovies",
   displayName: "GOmovies",
-  rank: 300,
+  rank: 200,
   type: [MWMediaType.MOVIE, MWMediaType.SERIES],
 
   async scrape({ media, episode }) {
diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts
index 75a8b844..5af85cb9 100644
--- a/src/backend/providers/superstream/index.ts
+++ b/src/backend/providers/superstream/index.ts
@@ -142,7 +142,7 @@ const convertSubtitles = (subtitleGroup: any): MWCaption | null => {
 registerProvider({
   id: "superstream",
   displayName: "Superstream",
-  rank: 200,
+  rank: 300,
   type: [MWMediaType.MOVIE, MWMediaType.SERIES],
 
   async scrape({ media, episode, progress }) {
diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts
index 51de0ed0..b2020020 100644
--- a/src/state/bookmark/store.ts
+++ b/src/state/bookmark/store.ts
@@ -14,7 +14,7 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
   })
   .addVersion({
     version: 1,
-    migrate(old: OldBookmarks) {
+    migrate(old: BookmarkStoreData) {
       return migrateV2Bookmarks(old);
     },
   })
diff --git a/src/state/watched/migrations/v3.ts b/src/state/watched/migrations/v3.ts
index 71e0b182..dffae637 100644
--- a/src/state/watched/migrations/v3.ts
+++ b/src/state/watched/migrations/v3.ts
@@ -1,14 +1,20 @@
 import { getLegacyMetaFromId } from "@/backend/metadata/getmeta";
-import { getMovieFromExternalId } from "@/backend/metadata/tmdb";
+import {
+  getEpisodes,
+  getMediaDetails,
+  getMovieFromExternalId,
+} from "@/backend/metadata/tmdb";
 import { MWMediaType } from "@/backend/metadata/types/mw";
+import { BookmarkStoreData } from "@/state/bookmark/types";
+import { isNotNull } from "@/utils/typeguard";
 
 import { WatchedStoreData } from "../types";
 
 async function migrateId(
-  id: number,
+  id: string,
   type: MWMediaType
 ): Promise<string | undefined> {
-  const meta = await getLegacyMetaFromId(type, id.toString());
+  const meta = await getLegacyMetaFromId(type, id);
 
   if (!meta) return undefined;
   const { tmdbId, imdbId } = meta;
@@ -25,57 +31,59 @@ async function migrateId(
   }
 }
 
-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),
-    })
-  );
+export async function migrateV2Bookmarks(old: BookmarkStoreData) {
+  const updatedBookmarks = old.bookmarks.map(async (item) => ({
+    ...item,
+    id: await migrateId(item.id, item.type).catch(() => undefined),
+  }));
 
   return {
     bookmarks: (await Promise.all(updatedBookmarks)).filter((item) => item.id),
   };
 }
 
-export async function migrateV3Videos(old: any) {
-  const oldData = old;
-  if (!oldData) return;
-
+export async function migrateV3Videos(
+  old: WatchedStoreData
+): Promise<WatchedStoreData> {
   const updatedItems = await Promise.all(
-    oldData.items.map(async (item: any) => {
-      const migratedId = await migrateId(
-        item.item.meta.id,
-        item.item.meta.type
-      );
+    old.items.map(async (progress) => {
+      try {
+        const migratedId = await migrateId(
+          progress.item.meta.id,
+          progress.item.meta.type
+        );
 
-      const migratedItem = {
-        ...item,
-        item: {
-          ...item.item,
-          meta: {
-            ...item.item.meta,
-            id: migratedId,
-          },
-        },
-      };
+        if (!migratedId) return null;
 
-      return {
-        ...item,
-        item: migratedId ? migratedItem : item.item,
-      };
+        const clone = structuredClone(progress);
+        clone.item.meta.id = migratedId;
+        if (clone.item.series) {
+          const series = clone.item.series;
+          const details = await getMediaDetails(migratedId, "show");
+
+          const season = details.seasons.find(
+            (v) => v.season_number === series.season
+          );
+          if (!season) return null;
+
+          const episodes = await getEpisodes(migratedId, season.season_number);
+          const episode = episodes.find(
+            (v) => v.episode_number === series.episode
+          );
+          if (!episode) return null;
+
+          clone.item.series.episodeId = episode.id.toString();
+          clone.item.series.seasonId = season.id.toString();
+        }
+
+        return clone;
+      } catch (err) {
+        return null;
+      }
     })
   );
 
-  const newData: WatchedStoreData = {
-    items: updatedItems.map((item) => item.item),
-  };
-
   return {
-    ...oldData,
-    items: newData.items,
+    items: updatedItems.filter(isNotNull),
   };
 }
diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts
index b59c37dc..c11e3f59 100644
--- a/src/state/watched/store.ts
+++ b/src/state/watched/store.ts
@@ -22,7 +22,7 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
   })
   .addVersion({
     version: 2,
-    migrate(old: OldData) {
+    migrate(old: WatchedStoreData) {
       return migrateV3Videos(old);
     },
   })
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
index f48e0245..83057d54 100644
--- a/src/utils/storage.ts
+++ b/src/utils/storage.ts
@@ -46,8 +46,13 @@ export async function initializeStores() {
     let mostRecentData = data;
     try {
       for (const version of relevantVersions) {
-        if (version.migrate)
+        if (version.migrate) {
+          localStorage.setItem(
+            `BACKUP-v${version.version}-${internal.key}`,
+            JSON.stringify(mostRecentData)
+          );
           mostRecentData = await version.migrate(mostRecentData);
+        }
       }
     } catch (err) {
       console.error(`FAILED TO MIGRATE STORE ${internal.key}`, err);
diff --git a/src/utils/typeguard.ts b/src/utils/typeguard.ts
new file mode 100644
index 00000000..95dd81a1
--- /dev/null
+++ b/src/utils/typeguard.ts
@@ -0,0 +1,3 @@
+export function isNotNull<T>(obj: T | null): obj is T {
+  return obj != null;
+}

From f68c8148d8fde3ca1478236f761f8670da6fa744 Mon Sep 17 00:00:00 2001
From: adrifcastr <adrifcastr@gmail.com>
Date: Fri, 23 Jun 2023 14:20:04 +0200
Subject: [PATCH 2/6] fix poster path

---
 src/backend/metadata/getmeta.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts
index c09d8292..3893db53 100644
--- a/src/backend/metadata/getmeta.ts
+++ b/src/backend/metadata/getmeta.ts
@@ -73,7 +73,7 @@ export function formatTMDBMetaResult(
         season_number: v.season_number,
         title: v.name,
       })),
-      poster: (details as TMDBMovieData).poster_path ?? undefined,
+      poster: getMediaPoster(show.poster_path) ?? undefined,
       original_release_year: new Date(show.first_air_date).getFullYear(),
     };
   }

From fcf8a9e755ecc3bdc91d5d3c4dd4caee8ac05261 Mon Sep 17 00:00:00 2001
From: mrjvs <jellevs@gmail.com>
Date: Fri, 23 Jun 2023 21:58:33 +0200
Subject: [PATCH 3/6] update configuration documentation

---
 SELFHOSTING.md               |  3 ++-
 example.env                  |  5 +----
 public/config.js             |  3 +--
 src/backend/metadata/tmdb.ts |  2 +-
 src/index.tsx                |  3 ++-
 src/setup/config.ts          | 43 ++++++++++++++++++------------------
 6 files changed, 28 insertions(+), 31 deletions(-)

diff --git a/SELFHOSTING.md b/SELFHOSTING.md
index 7137be1f..926322d6 100644
--- a/SELFHOSTING.md
+++ b/SELFHOSTING.md
@@ -32,7 +32,8 @@ Your proxy is now hosted on cloudflare. Note the url of your worker. you will ne
 4. Put your cloudflare proxy URL inbetween the double qoutes of `VITE_CORS_PROXY_URL: "",`. Make sure to not have a slash at the end of your URL.
 
    Example (THIS IS MINE, IT WONT WORK FOR YOU): `VITE_CORS_PROXY_URL: "https://test-proxy.test.workers.dev",`
-5. Save the file
+5. Put your TMDB read access token inside the quotes of `VITE_TMDB_API_KEY: "",`. You can generate it for free at [https://www.themoviedb.org/settings/api](https://www.themoviedb.org/settings/api).
+6. Save the file
 
 Your client has been prepared, you can now host it on any webhost.
 It doesn't require php, its just a standard static page.
diff --git a/example.env b/example.env
index 5416f0f1..d191d741 100644
--- a/example.env
+++ b/example.env
@@ -1,6 +1,3 @@
 # make sure the cors proxy url does NOT have a slash at the end
 VITE_CORS_PROXY_URL=...
-
-# the keys below are optional - defaults are provided
-VITE_TMDB_API_KEY=...
-VITE_OMDB_API_KEY=...
+VITE_TMDB_READ_API_KEY=...
diff --git a/public/config.js b/public/config.js
index b69f60eb..abd72baf 100644
--- a/public/config.js
+++ b/public/config.js
@@ -1,6 +1,5 @@
 window.__CONFIG__ = {
   // url must NOT end with a slash
   VITE_CORS_PROXY_URL: "",
-  VITE_TMDB_API_KEY: "b030404650f279792a8d3287232358e3",
-  VITE_OMDB_API_KEY: "aa0937c0",
+  TMDB_READ_API_KEY: ""
 };
diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts
index 1c442028..9b38d995 100644
--- a/src/backend/metadata/tmdb.ts
+++ b/src/backend/metadata/tmdb.ts
@@ -99,7 +99,7 @@ const baseURL = "https://api.themoviedb.org/3";
 
 const headers = {
   accept: "application/json",
-  Authorization: `Bearer ${conf().TMDB_API_KEY}`,
+  Authorization: `Bearer ${conf().TMDB_READ_API_KEY}`,
 };
 
 async function get<T>(url: string, params?: object): Promise<T> {
diff --git a/src/index.tsx b/src/index.tsx
index 36b1fb14..839d7a90 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -7,7 +7,7 @@ import { registerSW } from "virtual:pwa-register";
 
 import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
 import App from "@/setup/App";
-import { conf } from "@/setup/config";
+import { assertConfig, conf } from "@/setup/config";
 import i18n from "@/setup/i18n";
 
 import "@/setup/ga";
@@ -30,6 +30,7 @@ registerSW({
 });
 
 const LazyLoadedApp = React.lazy(async () => {
+  await assertConfig();
   await initializeStores();
   i18n.changeLanguage(SettingsStore.get().language ?? "en");
   return {
diff --git a/src/setup/config.ts b/src/setup/config.ts
index f1db01da..a7d9067b 100644
--- a/src/setup/config.ts
+++ b/src/setup/config.ts
@@ -4,8 +4,7 @@ interface Config {
   APP_VERSION: string;
   GITHUB_LINK: string;
   DISCORD_LINK: string;
-  OMDB_API_KEY: string;
-  TMDB_API_KEY: string;
+  TMDB_READ_API_KEY: string;
   CORS_PROXY_URL: string;
   NORMAL_ROUTER: boolean;
 }
@@ -14,15 +13,13 @@ export interface RuntimeConfig {
   APP_VERSION: string;
   GITHUB_LINK: string;
   DISCORD_LINK: string;
-  OMDB_API_KEY: string;
-  TMDB_API_KEY: string;
+  TMDB_READ_API_KEY: string;
   NORMAL_ROUTER: boolean;
   PROXY_URLS: string[];
 }
 
 const env: Record<keyof Config, undefined | string> = {
-  OMDB_API_KEY: import.meta.env.VITE_OMDB_API_KEY,
-  TMDB_API_KEY: import.meta.env.VITE_TMDB_API_KEY,
+  TMDB_READ_API_KEY: import.meta.env.VITE_TMDB_READ_API_KEY,
   APP_VERSION: undefined,
   GITHUB_LINK: undefined,
   DISCORD_LINK: undefined,
@@ -30,25 +27,28 @@ const env: Record<keyof Config, undefined | string> = {
   NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
 };
 
-const alerts = [] as string[];
-
 // loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
-function getKey(key: keyof Config, defaultString?: string): string {
+function getKeyValue(key: keyof Config): string | undefined {
   let windowValue = (window as any)?.__CONFIG__?.[`VITE_${key}`];
   if (windowValue !== undefined && windowValue.length === 0)
     windowValue = undefined;
-  const value = env[key] ?? windowValue ?? undefined;
-  if (value === undefined) {
-    if (defaultString) return defaultString;
-    if (!alerts.includes(key)) {
-      // eslint-disable-next-line no-alert
-      window.alert(`Misconfigured instance, missing key: ${key}`);
-      alerts.push(key);
-    }
-    return "";
-  }
+  return env[key] ?? windowValue ?? undefined;
+}
 
-  return value;
+function getKey(key: keyof Config, defaultString?: string): string {
+  return getKeyValue(key) ?? defaultString ?? "";
+}
+
+export function assertConfig() {
+  const keys: Array<keyof Config> = ["TMDB_READ_API_KEY", "CORS_PROXY_URL"];
+  const values = keys.map((key) => {
+    const val = getKeyValue(key);
+    if (val) return val;
+    // eslint-disable-next-line no-alert
+    window.alert(`Misconfigured instance, missing key: ${key}`);
+    return val;
+  });
+  if (values.includes(undefined)) throw new Error("Misconfigured instance");
 }
 
 export function conf(): RuntimeConfig {
@@ -56,8 +56,7 @@ export function conf(): RuntimeConfig {
     APP_VERSION,
     GITHUB_LINK,
     DISCORD_LINK,
-    OMDB_API_KEY: getKey("OMDB_API_KEY"),
-    TMDB_API_KEY: getKey("TMDB_API_KEY"),
+    TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
     PROXY_URLS: getKey("CORS_PROXY_URL")
       .split(",")
       .map((v) => v.trim()),

From 6aa0c86e42d0a5821d702842aaf941d984ded7a4 Mon Sep 17 00:00:00 2001
From: mrjvs <jellevs@gmail.com>
Date: Fri, 23 Jun 2023 21:58:45 +0200
Subject: [PATCH 4/6] bump version

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3f7c4bf9..025a2844 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "movie-web",
-  "version": "3.1.0",
+  "version": "3.1.1",
   "private": true,
   "homepage": "https://movie-web.app",
   "dependencies": {

From ea52156bb892400f0f6891505e34bda5c04589d7 Mon Sep 17 00:00:00 2001
From: mrjvs <jellevs@gmail.com>
Date: Fri, 23 Jun 2023 23:00:28 +0200
Subject: [PATCH 5/6] fix config.js preset and typo in documentation

---
 SELFHOSTING.md   | 6 +++---
 public/config.js | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/SELFHOSTING.md b/SELFHOSTING.md
index 926322d6..5784b228 100644
--- a/SELFHOSTING.md
+++ b/SELFHOSTING.md
@@ -29,10 +29,10 @@ Your proxy is now hosted on cloudflare. Note the url of your worker. you will ne
 1. Download the file `movie-web.zip` from the latest release: [https://github.com/movie-web/movie-web/releases/latest](https://github.com/movie-web/movie-web/releases/latest)
 2. Extract the zip file so you can edit the files.
 3. Open `config.js` in notepad, VScode or similar.
-4. Put your cloudflare proxy URL inbetween the double qoutes of `VITE_CORS_PROXY_URL: "",`. Make sure to not have a slash at the end of your URL.
+4. Put your cloudflare proxy URL inbetween the double qoutes of `VITE_CORS_PROXY_URL: ""`. Make sure to not have a slash at the end of your URL.
 
-   Example (THIS IS MINE, IT WONT WORK FOR YOU): `VITE_CORS_PROXY_URL: "https://test-proxy.test.workers.dev",`
-5. Put your TMDB read access token inside the quotes of `VITE_TMDB_API_KEY: "",`. You can generate it for free at [https://www.themoviedb.org/settings/api](https://www.themoviedb.org/settings/api).
+   Example (THIS IS MINE, IT WONT WORK FOR YOU): `VITE_CORS_PROXY_URL: "https://test-proxy.test.workers.dev"`
+5. Put your TMDB read access token inside the quotes of `VITE_TMDB_READ_API_KEY: ""`. You can generate it for free at [https://www.themoviedb.org/settings/api](https://www.themoviedb.org/settings/api).
 6. Save the file
 
 Your client has been prepared, you can now host it on any webhost.
diff --git a/public/config.js b/public/config.js
index abd72baf..c08704c2 100644
--- a/public/config.js
+++ b/public/config.js
@@ -1,5 +1,5 @@
 window.__CONFIG__ = {
   // url must NOT end with a slash
   VITE_CORS_PROXY_URL: "",
-  TMDB_READ_API_KEY: ""
+  VITE_TMDB_READ_API_KEY: ""
 };

From ce00f1c5c293e9074902641ab454db130ae043be Mon Sep 17 00:00:00 2001
From: mrjvs <jellevs@gmail.com>
Date: Fri, 23 Jun 2023 23:04:42 +0200
Subject: [PATCH 6/6] version bump

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 025a2844..327ba30b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "movie-web",
-  "version": "3.1.1",
+  "version": "3.1.2",
   "private": true,
   "homepage": "https://movie-web.app",
   "dependencies": {