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] 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;
+}