diff --git a/SELFHOSTING.md b/SELFHOSTING.md index 7137be1f..5784b228 100644 --- a/SELFHOSTING.md +++ b/SELFHOSTING.md @@ -29,10 +29,11 @@ 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. Save the file + 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. 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..c08704c2 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", + VITE_TMDB_READ_API_KEY: "" }; 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(), }; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index db665528..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(url: string, params?: object): Promise { @@ -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 "movie" + ? TMDBMovieData + : T extends "show" + ? TMDBShowData + : never; - switch (type) { - case "movie": - data = await get(`/movie/${id}`); - break; - case "show": - data = await get(`/tv/${id}`); - break; - default: - throw new Error("Invalid media type"); +export function getMediaDetails< + T extends TMDBContentTypes, + TReturn = MediaDetailReturn +>(id: string, type: T): Promise { + if (type === "movie") { + return get(`/movie/${id}`); } - - return data; + if (type === "show") { + return get(`/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/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 = { - 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 = { 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 = ["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()), 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() }) .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 { - 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 { 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() }) .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(obj: T | null): obj is T { + return obj != null; +}