Merge branch 'dev' into v4

This commit is contained in:
mrjvs 2023-06-28 10:59:52 +02:00
commit 18f0e55bb3
14 changed files with 110 additions and 94 deletions

View File

@ -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.

View File

@ -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=...

View File

@ -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",
}; };

View File

@ -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(),
}; };
} }

View File

@ -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
: T extends "show"
? TMDBShowData
: never;
switch (type) { export function getMediaDetails<
case "movie": T extends TMDBContentTypes,
data = await get<TMDBMovieData>(`/movie/${id}`); TReturn = MediaDetailReturn<T>
break; >(id: string, type: T): Promise<TReturn> {
case "show": if (type === "movie") {
data = await get<TMDBShowData>(`/tv/${id}`); return get<TReturn>(`/movie/${id}`);
break;
default:
throw new Error("Invalid media type");
} }
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 {

View File

@ -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 }) {

View File

@ -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 }) {

View File

@ -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 {

View File

@ -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)) {
// eslint-disable-next-line no-alert
window.alert(`Misconfigured instance, missing key: ${key}`);
alerts.push(key);
}
return "";
}
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 { 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()),

View File

@ -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);
}, },
}) })

View File

@ -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
);
const migratedItem = { if (!migratedId) return null;
...item,
item: {
...item.item,
meta: {
...item.item.meta,
id: migratedId,
},
},
};
return { const clone = structuredClone(progress);
...item, clone.item.meta.id = migratedId;
item: migratedId ? migratedItem : item.item, 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 { return {
...oldData, items: updatedItems.filter(isNotNull),
items: newData.items,
}; };
} }

View File

@ -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);
}, },
}) })

View File

@ -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 Normal file
View File

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