Browse Source

Merge branch 'v4' into v4-premid

pull/371/head
mrjvs 2 years ago committed by GitHub
parent
commit
f45f61d89a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      SELFHOSTING.md
  2. 5
      example.env
  3. 3
      public/config.js
  4. 2
      src/backend/metadata/getmeta.ts
  5. 33
      src/backend/metadata/tmdb.ts
  6. 2
      src/backend/providers/gomovies.ts
  7. 2
      src/backend/providers/superstream/index.ts
  8. 3
      src/index.tsx
  9. 43
      src/setup/config.ts
  10. 2
      src/state/bookmark/store.ts
  11. 92
      src/state/watched/migrations/v3.ts
  12. 2
      src/state/watched/store.ts
  13. 7
      src/utils/storage.ts
  14. 3
      src/utils/typeguard.ts

7
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) 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. 2. Extract the zip file so you can edit the files.
3. Open `config.js` in notepad, VScode or similar. 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",` 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_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. Your client has been prepared, you can now host it on any webhost.
It doesn't require php, its just a standard static page. It doesn't require php, its just a standard static page.

5
example.env

@ -1,6 +1,3 @@
# make sure the cors proxy url does NOT have a slash at the end # make sure the cors proxy url does NOT have a slash at the end
VITE_CORS_PROXY_URL=... VITE_CORS_PROXY_URL=...
VITE_TMDB_READ_API_KEY=...
# the keys below are optional - defaults are provided
VITE_TMDB_API_KEY=...
VITE_OMDB_API_KEY=...

3
public/config.js

@ -1,6 +1,5 @@
window.__CONFIG__ = { window.__CONFIG__ = {
// url must NOT end with a slash // url must NOT end with a slash
VITE_CORS_PROXY_URL: "", VITE_CORS_PROXY_URL: "",
VITE_TMDB_API_KEY: "b030404650f279792a8d3287232358e3", VITE_TMDB_READ_API_KEY: ""
VITE_OMDB_API_KEY: "aa0937c0",
}; };

2
src/backend/metadata/getmeta.ts

@ -73,7 +73,7 @@ export function formatTMDBMetaResult(
season_number: v.season_number, season_number: v.season_number,
title: v.name, 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(), original_release_year: new Date(show.first_air_date).getFullYear(),
}; };
} }

33
src/backend/metadata/tmdb.ts

@ -99,7 +99,7 @@ const baseURL = "https://api.themoviedb.org/3";
const headers = { const headers = {
accept: "application/json", 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> { async function get<T>(url: string, params?: object): Promise<T> {
@ -143,21 +143,24 @@ export async function searchMedia(
return data; return data;
} }
export async function getMediaDetails(id: string, type: TMDBContentTypes) { // Conditional type which for inferring the return type based on the content type
let data; type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
? TMDBMovieData
switch (type) { : T extends "show"
case "movie": ? TMDBShowData
data = await get<TMDBMovieData>(`/movie/${id}`); : never;
break;
case "show": export function getMediaDetails<
data = await get<TMDBShowData>(`/tv/${id}`); T extends TMDBContentTypes,
break; TReturn = MediaDetailReturn<T>
default: >(id: string, type: T): Promise<TReturn> {
throw new Error("Invalid media type"); if (type === "movie") {
return get<TReturn>(`/movie/${id}`);
} }
if (type === "show") {
return data; return get<TReturn>(`/tv/${id}`);
}
throw new Error("Invalid media type");
} }
export function getMediaPoster(posterPath: string | null): string | undefined { export function getMediaPoster(posterPath: string | null): string | undefined {

2
src/backend/providers/gomovies.ts

@ -8,7 +8,7 @@ const gomoviesBase = "https://gomovies.sx";
registerProvider({ registerProvider({
id: "gomovies", id: "gomovies",
displayName: "GOmovies", displayName: "GOmovies",
rank: 300, rank: 200,
type: [MWMediaType.MOVIE, MWMediaType.SERIES], type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, episode }) { async scrape({ media, episode }) {

2
src/backend/providers/superstream/index.ts

@ -142,7 +142,7 @@ const convertSubtitles = (subtitleGroup: any): MWCaption | null => {
registerProvider({ registerProvider({
id: "superstream", id: "superstream",
displayName: "Superstream", displayName: "Superstream",
rank: 200, rank: 300,
type: [MWMediaType.MOVIE, MWMediaType.SERIES], type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, episode, progress }) { async scrape({ media, episode, progress }) {

3
src/index.tsx

@ -7,7 +7,7 @@ import { registerSW } from "virtual:pwa-register";
import { ErrorBoundary } from "@/components/layout/ErrorBoundary"; import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
import App from "@/setup/App"; import App from "@/setup/App";
import { conf } from "@/setup/config"; import { assertConfig, conf } from "@/setup/config";
import i18n from "@/setup/i18n"; import i18n from "@/setup/i18n";
import "@/setup/ga"; import "@/setup/ga";
@ -30,6 +30,7 @@ registerSW({
}); });
const LazyLoadedApp = React.lazy(async () => { const LazyLoadedApp = React.lazy(async () => {
await assertConfig();
await initializeStores(); await initializeStores();
i18n.changeLanguage(SettingsStore.get().language ?? "en"); i18n.changeLanguage(SettingsStore.get().language ?? "en");
return { return {

43
src/setup/config.ts

@ -4,8 +4,7 @@ interface Config {
APP_VERSION: string; APP_VERSION: string;
GITHUB_LINK: string; GITHUB_LINK: string;
DISCORD_LINK: string; DISCORD_LINK: string;
OMDB_API_KEY: string; TMDB_READ_API_KEY: string;
TMDB_API_KEY: string;
CORS_PROXY_URL: string; CORS_PROXY_URL: string;
NORMAL_ROUTER: boolean; NORMAL_ROUTER: boolean;
} }
@ -14,15 +13,13 @@ export interface RuntimeConfig {
APP_VERSION: string; APP_VERSION: string;
GITHUB_LINK: string; GITHUB_LINK: string;
DISCORD_LINK: string; DISCORD_LINK: string;
OMDB_API_KEY: string; TMDB_READ_API_KEY: string;
TMDB_API_KEY: string;
NORMAL_ROUTER: boolean; NORMAL_ROUTER: boolean;
PROXY_URLS: string[]; PROXY_URLS: string[];
} }
const env: Record<keyof Config, undefined | string> = { const env: Record<keyof Config, undefined | string> = {
OMDB_API_KEY: import.meta.env.VITE_OMDB_API_KEY, TMDB_READ_API_KEY: import.meta.env.VITE_TMDB_READ_API_KEY,
TMDB_API_KEY: import.meta.env.VITE_TMDB_API_KEY,
APP_VERSION: undefined, APP_VERSION: undefined,
GITHUB_LINK: undefined, GITHUB_LINK: undefined,
DISCORD_LINK: undefined, DISCORD_LINK: undefined,
@ -30,25 +27,28 @@ const env: Record<keyof Config, undefined | string> = {
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, 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) // 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}`]; let windowValue = (window as any)?.__CONFIG__?.[`VITE_${key}`];
if (windowValue !== undefined && windowValue.length === 0) if (windowValue !== undefined && windowValue.length === 0)
windowValue = undefined; windowValue = undefined;
const value = env[key] ?? windowValue ?? undefined; return env[key] ?? windowValue ?? undefined;
if (value === undefined) { }
if (defaultString) return defaultString;
if (!alerts.includes(key)) { function getKey(key: keyof Config, defaultString?: string): string {
// eslint-disable-next-line no-alert return getKeyValue(key) ?? defaultString ?? "";
window.alert(`Misconfigured instance, missing key: ${key}`); }
alerts.push(key);
}
return "";
}
return value; 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 { export function conf(): RuntimeConfig {
@ -56,8 +56,7 @@ export function conf(): RuntimeConfig {
APP_VERSION, APP_VERSION,
GITHUB_LINK, GITHUB_LINK,
DISCORD_LINK, DISCORD_LINK,
OMDB_API_KEY: getKey("OMDB_API_KEY"), TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
TMDB_API_KEY: getKey("TMDB_API_KEY"),
PROXY_URLS: getKey("CORS_PROXY_URL") PROXY_URLS: getKey("CORS_PROXY_URL")
.split(",") .split(",")
.map((v) => v.trim()), .map((v) => v.trim()),

2
src/state/bookmark/store.ts

@ -14,7 +14,7 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
}) })
.addVersion({ .addVersion({
version: 1, version: 1,
migrate(old: OldBookmarks) { migrate(old: BookmarkStoreData) {
return migrateV2Bookmarks(old); return migrateV2Bookmarks(old);
}, },
}) })

92
src/state/watched/migrations/v3.ts

@ -1,14 +1,20 @@
import { getLegacyMetaFromId } from "@/backend/metadata/getmeta"; 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 { MWMediaType } from "@/backend/metadata/types/mw";
import { BookmarkStoreData } from "@/state/bookmark/types";
import { isNotNull } from "@/utils/typeguard";
import { WatchedStoreData } from "../types"; import { WatchedStoreData } from "../types";
async function migrateId( async function migrateId(
id: number, id: string,
type: MWMediaType type: MWMediaType
): Promise<string | undefined> { ): Promise<string | undefined> {
const meta = await getLegacyMetaFromId(type, id.toString()); const meta = await getLegacyMetaFromId(type, id);
if (!meta) return undefined; if (!meta) return undefined;
const { tmdbId, imdbId } = meta; const { tmdbId, imdbId } = meta;
@ -25,57 +31,59 @@ async function migrateId(
} }
} }
export async function migrateV2Bookmarks(old: any) { export async function migrateV2Bookmarks(old: BookmarkStoreData) {
const oldData = old; const updatedBookmarks = old.bookmarks.map(async (item) => ({
if (!oldData) return; ...item,
id: await migrateId(item.id, item.type).catch(() => undefined),
const updatedBookmarks = oldData.bookmarks.map( }));
async (item: { id: number; type: MWMediaType }) => ({
...item,
id: await migrateId(item.id, item.type),
})
);
return { return {
bookmarks: (await Promise.all(updatedBookmarks)).filter((item) => item.id), bookmarks: (await Promise.all(updatedBookmarks)).filter((item) => item.id),
}; };
} }
export async function migrateV3Videos(old: any) { export async function migrateV3Videos(
const oldData = old; old: WatchedStoreData
if (!oldData) return; ): Promise<WatchedStoreData> {
const updatedItems = await Promise.all( const updatedItems = await Promise.all(
oldData.items.map(async (item: any) => { old.items.map(async (progress) => {
const migratedId = await migrateId( try {
item.item.meta.id, const migratedId = await migrateId(
item.item.meta.type progress.item.meta.id,
); progress.item.meta.type
);
if (!migratedId) return null;
const migratedItem = { const clone = structuredClone(progress);
...item, clone.item.meta.id = migratedId;
item: { if (clone.item.series) {
...item.item, const series = clone.item.series;
meta: { const details = await getMediaDetails(migratedId, "show");
...item.item.meta,
id: migratedId,
},
},
};
return { const season = details.seasons.find(
...item, (v) => v.season_number === series.season
item: migratedId ? migratedItem : item.item, );
}; 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 { return {
...oldData, items: updatedItems.filter(isNotNull),
items: newData.items,
}; };
} }

2
src/state/watched/store.ts

@ -22,7 +22,7 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
}) })
.addVersion({ .addVersion({
version: 2, version: 2,
migrate(old: OldData) { migrate(old: WatchedStoreData) {
return migrateV3Videos(old); return migrateV3Videos(old);
}, },
}) })

7
src/utils/storage.ts

@ -46,8 +46,13 @@ export async function initializeStores() {
let mostRecentData = data; let mostRecentData = data;
try { try {
for (const version of relevantVersions) { 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); mostRecentData = await version.migrate(mostRecentData);
}
} }
} catch (err) { } catch (err) {
console.error(`FAILED TO MIGRATE STORE ${internal.key}`, err); console.error(`FAILED TO MIGRATE STORE ${internal.key}`, err);

3
src/utils/typeguard.ts

@ -0,0 +1,3 @@
export function isNotNull<T>(obj: T | null): obj is T {
return obj != null;
}
Loading…
Cancel
Save