Browse Source

Merge branch 'dev' into meta/code-of-conduct

pull/352/head
William Oldham 2 years ago committed by GitHub
parent
commit
d2d710ad37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      SELFHOSTING.md
  2. 5
      example.env
  3. 2
      package.json
  4. 3
      public/config.js
  5. 2
      src/__tests__/providers/providers.test.ts
  6. 2
      src/__tests__/providers/testdata.ts
  7. 2
      src/backend/helpers/provider.ts
  8. 2
      src/backend/helpers/scrape.ts
  9. 157
      src/backend/metadata/getmeta.ts
  10. 42
      src/backend/metadata/justwatch.ts
  11. 56
      src/backend/metadata/search.ts
  12. 239
      src/backend/metadata/tmdb.ts
  13. 48
      src/backend/metadata/types/justwatch.ts
  14. 6
      src/backend/metadata/types/mw.ts
  15. 308
      src/backend/metadata/types/tmdb.ts
  16. 2
      src/backend/providers/2embed.ts
  17. 2
      src/backend/providers/flixhq.ts
  18. 2
      src/backend/providers/gdriveplayer.ts
  19. 4
      src/backend/providers/gomovies.ts
  20. 2
      src/backend/providers/hdwatched.ts
  21. 2
      src/backend/providers/kissasian.ts
  22. 2
      src/backend/providers/m4ufree.ts
  23. 2
      src/backend/providers/netfilm.ts
  24. 2
      src/backend/providers/remotestream.ts
  25. 2
      src/backend/providers/sflix.ts
  26. 2
      src/backend/providers/streamflix.ts
  27. 4
      src/backend/providers/superstream/index.ts
  28. 2
      src/components/SearchBar.tsx
  29. 23
      src/components/media/MediaCard.tsx
  30. 2
      src/components/media/WatchedMediaCard.tsx
  31. 2
      src/hooks/useScrape.ts
  32. 2
      src/hooks/useSearchQuery.ts
  33. 7
      src/index.tsx
  34. 45
      src/setup/App.tsx
  35. 43
      src/setup/config.ts
  36. 2
      src/state/bookmark/context.tsx
  37. 7
      src/state/bookmark/store.ts
  38. 2
      src/state/bookmark/types.ts
  39. 2
      src/state/watched/context.tsx
  40. 2
      src/state/watched/migrations/v2.ts
  41. 89
      src/state/watched/migrations/v3.ts
  42. 7
      src/state/watched/store.ts
  43. 2
      src/state/watched/types.ts
  44. 7
      src/utils/storage.ts
  45. 3
      src/utils/typeguard.ts
  46. 2
      src/video/components/actions/DividerAction.tsx
  47. 2
      src/video/components/actions/SeriesSelectionAction.tsx
  48. 2
      src/video/components/controllers/MetaController.tsx
  49. 2
      src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts
  50. 2
      src/video/components/parts/VideoErrorBoundary.tsx
  51. 2
      src/video/components/parts/VideoPlayerHeader.tsx
  52. 10
      src/video/components/popouts/EpisodeSelectionPopout.tsx
  53. 2
      src/views/developer/VideoTesterView.tsx
  54. 14
      src/views/media/MediaView.tsx
  55. 2
      src/views/other/v2Migration.tsx
  56. 2
      src/views/search/SearchResultsPartial.tsx
  57. 2
      src/views/search/SearchResultsView.tsx

5
SELFHOSTING.md

@ -31,10 +31,11 @@ Your proxy is now hosted on Cloudflare. Note the url of your worker as you will @@ -31,10 +31,11 @@ Your proxy is now hosted on Cloudflare. Note the url of your worker as you will
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, Visual Studio Code or similar.
4. Put your Cloudflare proxy URL in-between the double quotes of `VITE_CORS_PROXY_URL: "",`. Make sure to not have a slash at the end of your URL.
4. Put your Cloudflare proxy URL in-between the double quotes 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_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 now been prepared, you can now host it with any static website hosting (Common ones include [GitHub Pages](https://pages.github.com/), [Netlify](https://www.netlify.com/) and [Vercel](https://vercel.com/) but any will work!).
It doesn't require PHP, it's just a standard static page.

5
example.env

@ -1,6 +1,3 @@ @@ -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=...

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "movie-web",
"version": "3.0.15",
"version": "3.1.2",
"private": true,
"homepage": "https://movie-web.app",
"dependencies": {

3
public/config.js

@ -1,6 +1,5 @@ @@ -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: ""
};

2
src/__tests__/providers/providers.test.ts

@ -4,7 +4,7 @@ import "@/backend"; @@ -4,7 +4,7 @@ import "@/backend";
import { testData } from "@/__tests__/providers/testdata";
import { getProviders } from "@/backend/helpers/register";
import { runProvider } from "@/backend/helpers/run";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
describe("providers", () => {
const providers = getProviders();

2
src/__tests__/providers/testdata.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { DetailedMeta } from "@/backend/metadata/getmeta";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
export const testData: DetailedMeta[] = [
{

2
src/backend/helpers/provider.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { MWEmbed } from "./embed";
import { MWStream } from "./streams";
import { DetailedMeta } from "../metadata/getmeta";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
export type MWProviderScrapeResult = {
stream?: MWStream;

2
src/backend/helpers/scrape.ts

@ -3,7 +3,7 @@ import { getEmbedScraperByType, getProviders } from "./register"; @@ -3,7 +3,7 @@ import { getEmbedScraperByType, getProviders } from "./register";
import { runEmbedScraper, runProvider } from "./run";
import { MWStream } from "./streams";
import { DetailedMeta } from "../metadata/getmeta";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
interface MWProgressData {
type: "embed" | "provider";

157
src/backend/metadata/getmeta.ts

@ -1,13 +1,28 @@ @@ -1,13 +1,28 @@
import { FetchError } from "ofetch";
import { formatJWMeta, mediaTypeToJW } from "./justwatch";
import {
TMDBMediaToMediaType,
formatTMDBMeta,
getEpisodes,
getExternalIds,
getMediaDetails,
getMediaPoster,
getMovieFromExternalId,
mediaTypeToTMDB,
} from "./tmdb";
import {
JWMediaResult,
JWSeasonMetaResult,
JW_API_BASE,
formatJWMeta,
mediaTypeToJW,
} from "./justwatch";
import { MWMediaMeta, MWMediaType } from "./types";
} from "./types/justwatch";
import { MWMediaMeta, MWMediaType } from "./types/mw";
import {
TMDBMediaResult,
TMDBMovieData,
TMDBSeasonMetaResult,
TMDBShowData,
} from "./types/tmdb";
import { makeUrl, proxiedFetch } from "../helpers/fetch";
type JWExternalIdType =
@ -33,10 +48,92 @@ export interface DetailedMeta { @@ -33,10 +48,92 @@ export interface DetailedMeta {
tmdbId?: string;
}
export function formatTMDBMetaResult(
details: TMDBShowData | TMDBMovieData,
type: MWMediaType
): TMDBMediaResult {
if (type === MWMediaType.MOVIE) {
const movie = details as TMDBMovieData;
return {
id: details.id,
title: movie.title,
object_type: mediaTypeToTMDB(type),
poster: getMediaPoster(movie.poster_path) ?? undefined,
original_release_year: new Date(movie.release_date).getFullYear(),
};
}
if (type === MWMediaType.SERIES) {
const show = details as TMDBShowData;
return {
id: details.id,
title: show.name,
object_type: mediaTypeToTMDB(type),
seasons: show.seasons.map((v) => ({
id: v.id,
season_number: v.season_number,
title: v.name,
})),
poster: getMediaPoster(show.poster_path) ?? undefined,
original_release_year: new Date(show.first_air_date).getFullYear(),
};
}
throw new Error("unsupported type");
}
export async function getMetaFromId(
type: MWMediaType,
id: string,
seasonId?: string
): Promise<DetailedMeta | null> {
const details = await getMediaDetails(id, mediaTypeToTMDB(type));
if (!details) return null;
const externalIds = await getExternalIds(id, mediaTypeToTMDB(type));
const imdbId = externalIds.imdb_id ?? undefined;
let seasonData: TMDBSeasonMetaResult | undefined;
if (type === MWMediaType.SERIES) {
const seasons = (details as TMDBShowData).seasons;
let selectedSeason = seasons.find((v) => v.id.toString() === seasonId);
if (!selectedSeason) {
selectedSeason = seasons.find((v) => v.season_number === 1);
}
if (selectedSeason) {
const episodes = await getEpisodes(
details.id.toString(),
selectedSeason.season_number
);
seasonData = {
id: selectedSeason.id.toString(),
season_number: selectedSeason.season_number,
title: selectedSeason.name,
episodes,
};
}
}
const tmdbmeta = formatTMDBMetaResult(details, type);
if (!tmdbmeta) return null;
const meta = formatTMDBMeta(tmdbmeta, seasonData);
if (!meta) return null;
return {
meta,
imdbId,
tmdbId: id,
};
}
export async function getLegacyMetaFromId(
type: MWMediaType,
id: string,
seasonId?: string
): Promise<DetailedMeta | null> {
const queryType = mediaTypeToJW(type);
@ -82,3 +179,55 @@ export async function getMetaFromId( @@ -82,3 +179,55 @@ export async function getMetaFromId(
tmdbId,
};
}
export function TMDBMediaToId(media: MWMediaMeta): string {
return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
}
export function decodeTMDBId(
paramId: string
): { id: string; type: MWMediaType } | null {
const [prefix, type, id] = paramId.split("-", 3);
if (prefix !== "tmdb") return null;
let mediaType;
try {
mediaType = TMDBMediaToMediaType(type);
} catch {
return null;
}
return {
type: mediaType,
id,
};
}
export function isLegacyUrl(url: string): boolean {
if (url.startsWith("/media/JW")) return true;
return false;
}
export async function convertLegacyUrl(
url: string
): Promise<string | undefined> {
if (!isLegacyUrl(url)) return undefined;
const urlParts = url.split("/").slice(2);
const [, type, id] = urlParts[0].split("-", 3);
const mediaType = TMDBMediaToMediaType(type);
const meta = await getLegacyMetaFromId(mediaType, id);
if (!meta) return undefined;
const { tmdbId, imdbId } = meta;
if (!tmdbId && !imdbId) return undefined;
// movies always have an imdb id on tmdb
if (imdbId && mediaType === MWMediaType.MOVIE) {
const movieId = await getMovieFromExternalId(imdbId);
if (movieId) return `/media/tmdb-movie-${movieId}`;
}
if (tmdbId) {
return `/media/tmdb-${type}-${tmdbId}`;
}
}

42
src/backend/metadata/justwatch.ts

@ -1,38 +1,10 @@ @@ -1,38 +1,10 @@
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types";
export const JW_API_BASE = "https://apis.justwatch.com";
export const JW_IMAGE_BASE = "https://images.justwatch.com";
export type JWContentTypes = "movie" | "show";
export type JWSeasonShort = {
title: string;
id: number;
season_number: number;
};
export type JWEpisodeShort = {
title: string;
id: number;
episode_number: number;
};
export type JWMediaResult = {
title: string;
poster?: string;
id: number;
original_release_year?: number;
jw_entity_id: string;
object_type: JWContentTypes;
seasons?: JWSeasonShort[];
};
export type JWSeasonMetaResult = {
title: string;
id: string;
season_number: number;
episodes: JWEpisodeShort[];
};
import {
JWContentTypes,
JWMediaResult,
JWSeasonMetaResult,
JW_IMAGE_BASE,
} from "./types/justwatch";
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
export function mediaTypeToJW(type: MWMediaType): JWContentTypes {
if (type === MWMediaType.MOVIE) return "movie";

56
src/backend/metadata/search.ts

@ -1,14 +1,12 @@ @@ -1,14 +1,12 @@
import { SimpleCache } from "@/utils/cache";
import {
JWContentTypes,
JWMediaResult,
JW_API_BASE,
formatJWMeta,
mediaTypeToJW,
} from "./justwatch";
import { MWMediaMeta, MWQuery } from "./types";
import { proxiedFetch } from "../helpers/fetch";
formatTMDBMeta,
formatTMDBSearchResult,
mediaTypeToTMDB,
searchMedia,
} from "./tmdb";
import { MWMediaMeta, MWQuery } from "./types/mw";
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
cache.setCompare((a, b) => {
@ -16,44 +14,16 @@ cache.setCompare((a, b) => { @@ -16,44 +14,16 @@ cache.setCompare((a, b) => {
});
cache.initialize();
type JWSearchQuery = {
content_types: JWContentTypes[];
page: number;
page_size: number;
query: string;
};
type JWPage<T> = {
items: T[];
page: number;
page_size: number;
total_pages: number;
total_results: number;
};
export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
const { searchQuery, type } = query;
const contentType = mediaTypeToJW(type);
const body: JWSearchQuery = {
content_types: [contentType],
page: 1,
query: searchQuery,
page_size: 40,
};
const data = await proxiedFetch<JWPage<JWMediaResult>>(
"/content/titles/en_US/popular",
{
baseURL: JW_API_BASE,
params: {
body: JSON.stringify(body),
},
}
);
const data = await searchMedia(searchQuery, mediaTypeToTMDB(type));
const results = data.results.map((v) => {
const formattedResult = formatTMDBSearchResult(v, mediaTypeToTMDB(type));
return formatTMDBMeta(formattedResult);
});
const returnData = data.items.map<MWMediaMeta>((v) => formatJWMeta(v));
cache.set(query, returnData, 3600); // cache for an hour
return returnData;
cache.set(query, results, 3600); // cache results for 1 hour
return results;
}

239
src/backend/metadata/tmdb.ts

@ -0,0 +1,239 @@ @@ -0,0 +1,239 @@
import { conf } from "@/setup/config";
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
import {
ExternalIdMovieSearchResult,
TMDBContentTypes,
TMDBEpisodeShort,
TMDBExternalIds,
TMDBMediaResult,
TMDBMovieData,
TMDBMovieExternalIds,
TMDBMovieResponse,
TMDBMovieResult,
TMDBSeason,
TMDBSeasonMetaResult,
TMDBShowData,
TMDBShowExternalIds,
TMDBShowResponse,
TMDBShowResult,
} from "./types/tmdb";
import { mwFetch } from "../helpers/fetch";
export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes {
if (type === MWMediaType.MOVIE) return "movie";
if (type === MWMediaType.SERIES) return "show";
throw new Error("unsupported type");
}
export function TMDBMediaToMediaType(type: string): MWMediaType {
if (type === "movie") return MWMediaType.MOVIE;
if (type === "show") return MWMediaType.SERIES;
throw new Error("unsupported type");
}
export function formatTMDBMeta(
media: TMDBMediaResult,
season?: TMDBSeasonMetaResult
): MWMediaMeta {
const type = TMDBMediaToMediaType(media.object_type);
let seasons: undefined | MWSeasonMeta[];
if (type === MWMediaType.SERIES) {
seasons = media.seasons
?.sort((a, b) => a.season_number - b.season_number)
.map(
(v): MWSeasonMeta => ({
title: v.title,
id: v.id.toString(),
number: v.season_number,
})
);
}
return {
title: media.title,
id: media.id.toString(),
year: media.original_release_year?.toString(),
poster: media.poster,
type,
seasons: seasons as any,
seasonData: season
? ({
id: season.id.toString(),
number: season.season_number,
title: season.title,
episodes: season.episodes
.sort((a, b) => a.episode_number - b.episode_number)
.map((v) => ({
id: v.id.toString(),
number: v.episode_number,
title: v.title,
})),
} as any)
: (undefined as any),
};
}
export function TMDBMediaToId(media: MWMediaMeta): string {
return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
}
export function decodeTMDBId(
paramId: string
): { id: string; type: MWMediaType } | null {
const [prefix, type, id] = paramId.split("-", 3);
if (prefix !== "tmdb") return null;
let mediaType;
try {
mediaType = TMDBMediaToMediaType(type);
} catch {
return null;
}
return {
type: mediaType,
id,
};
}
const baseURL = "https://api.themoviedb.org/3";
const headers = {
accept: "application/json",
Authorization: `Bearer ${conf().TMDB_READ_API_KEY}`,
};
async function get<T>(url: string, params?: object): Promise<T> {
const res = await mwFetch<any>(encodeURI(url), {
headers,
baseURL,
params: {
...params,
},
});
return res;
}
export async function searchMedia(
query: string,
type: TMDBContentTypes
): Promise<TMDBMovieResponse | TMDBShowResponse> {
let data;
switch (type) {
case "movie":
data = await get<TMDBMovieResponse>("search/movie", {
query,
include_adult: false,
language: "en-US",
page: 1,
});
break;
case "show":
data = await get<TMDBShowResponse>("search/tv", {
query,
include_adult: false,
language: "en-US",
page: 1,
});
break;
default:
throw new Error("Invalid media type");
}
return 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;
export function getMediaDetails<
T extends TMDBContentTypes,
TReturn = MediaDetailReturn<T>
>(id: string, type: T): Promise<TReturn> {
if (type === "movie") {
return get<TReturn>(`/movie/${id}`);
}
if (type === "show") {
return get<TReturn>(`/tv/${id}`);
}
throw new Error("Invalid media type");
}
export function getMediaPoster(posterPath: string | null): string | undefined {
if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`;
}
export async function getEpisodes(
id: string,
season: number
): Promise<TMDBEpisodeShort[]> {
const data = await get<TMDBSeason>(`/tv/${id}/season/${season}`);
return data.episodes.map((e) => ({
id: e.id,
episode_number: e.episode_number,
title: e.name,
}));
}
export async function getExternalIds(
id: string,
type: TMDBContentTypes
): Promise<TMDBExternalIds> {
let data;
switch (type) {
case "movie":
data = await get<TMDBMovieExternalIds>(`/movie/${id}/external_ids`);
break;
case "show":
data = await get<TMDBShowExternalIds>(`/tv/${id}/external_ids`);
break;
default:
throw new Error("Invalid media type");
}
return data;
}
export async function getMovieFromExternalId(
imdbId: string
): Promise<string | undefined> {
const data = await get<ExternalIdMovieSearchResult>(`/find/${imdbId}`, {
external_source: "imdb_id",
});
const movie = data.movie_results[0];
if (!movie) return undefined;
return movie.id.toString();
}
export function formatTMDBSearchResult(
result: TMDBShowResult | TMDBMovieResult,
mediatype: TMDBContentTypes
): TMDBMediaResult {
const type = TMDBMediaToMediaType(mediatype);
if (type === MWMediaType.SERIES) {
const show = result as TMDBShowResult;
return {
title: show.name,
poster: getMediaPoster(show.poster_path),
id: show.id,
original_release_year: new Date(show.first_air_date).getFullYear(),
object_type: mediatype,
};
}
const movie = result as TMDBMovieResult;
return {
title: movie.title,
poster: getMediaPoster(movie.poster_path),
id: movie.id,
original_release_year: new Date(movie.release_date).getFullYear(),
object_type: mediatype,
};
}

48
src/backend/metadata/types/justwatch.ts

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
export type JWContentTypes = "movie" | "show";
export type JWSearchQuery = {
content_types: JWContentTypes[];
page: number;
page_size: number;
query: string;
};
export type JWPage<T> = {
items: T[];
page: number;
page_size: number;
total_pages: number;
total_results: number;
};
export const JW_API_BASE = "https://apis.justwatch.com";
export const JW_IMAGE_BASE = "https://images.justwatch.com";
export type JWSeasonShort = {
title: string;
id: number;
season_number: number;
};
export type JWEpisodeShort = {
title: string;
id: number;
episode_number: number;
};
export type JWMediaResult = {
title: string;
poster?: string;
id: number;
original_release_year?: number;
jw_entity_id: string;
object_type: JWContentTypes;
seasons?: JWSeasonShort[];
};
export type JWSeasonMetaResult = {
title: string;
id: string;
season_number: number;
episodes: JWEpisodeShort[];
};

6
src/backend/metadata/types.ts → src/backend/metadata/types/mw.ts

@ -45,3 +45,9 @@ export interface MWQuery { @@ -45,3 +45,9 @@ export interface MWQuery {
searchQuery: string;
type: MWMediaType;
}
export interface DetailedMeta {
meta: MWMediaMeta;
imdbId?: string;
tmdbId?: string;
}

308
src/backend/metadata/types/tmdb.ts

@ -0,0 +1,308 @@ @@ -0,0 +1,308 @@
export type TMDBContentTypes = "movie" | "show";
export type TMDBSeasonShort = {
title: string;
id: number;
season_number: number;
};
export type TMDBEpisodeShort = {
title: string;
id: number;
episode_number: number;
};
export type TMDBMediaResult = {
title: string;
poster?: string;
id: number;
original_release_year?: number;
object_type: TMDBContentTypes;
seasons?: TMDBSeasonShort[];
};
export type TMDBSeasonMetaResult = {
title: string;
id: string;
season_number: number;
episodes: TMDBEpisodeShort[];
};
export interface TMDBShowData {
adult: boolean;
backdrop_path: string | null;
created_by: {
id: number;
credit_id: string;
name: string;
gender: number;
profile_path: string | null;
}[];
episode_run_time: number[];
first_air_date: string;
genres: {
id: number;
name: string;
}[];
homepage: string;
id: number;
in_production: boolean;
languages: string[];
last_air_date: string;
last_episode_to_air: {
id: number;
name: string;
overview: string;
vote_average: number;
vote_count: number;
air_date: string;
episode_number: number;
production_code: string;
runtime: number | null;
season_number: number;
show_id: number;
still_path: string | null;
} | null;
name: string;
next_episode_to_air: {
id: number;
name: string;
overview: string;
vote_average: number;
vote_count: number;
air_date: string;
episode_number: number;
production_code: string;
runtime: number | null;
season_number: number;
show_id: number;
still_path: string | null;
} | null;
networks: {
id: number;
logo_path: string;
name: string;
origin_country: string;
}[];
number_of_episodes: number;
number_of_seasons: number;
origin_country: string[];
original_language: string;
original_name: string;
overview: string;
popularity: number;
poster_path: string | null;
production_companies: {
id: number;
logo_path: string | null;
name: string;
origin_country: string;
}[];
production_countries: {
iso_3166_1: string;
name: string;
}[];
seasons: {
air_date: string;
episode_count: number;
id: number;
name: string;
overview: string;
poster_path: string | null;
season_number: number;
}[];
spoken_languages: {
english_name: string;
iso_639_1: string;
name: string;
}[];
status: string;
tagline: string;
type: string;
vote_average: number;
vote_count: number;
}
export interface TMDBMovieData {
adult: boolean;
backdrop_path: string | null;
belongs_to_collection: {
id: number;
name: string;
poster_path: string | null;
backdrop_path: string | null;
} | null;
budget: number;
genres: {
id: number;
name: string;
}[];
homepage: string | null;
id: number;
imdb_id: string | null;
original_language: string;
original_title: string;
overview: string | null;
popularity: number;
poster_path: string | null;
production_companies: {
id: number;
logo_path: string | null;
name: string;
origin_country: string;
}[];
production_countries: {
iso_3166_1: string;
name: string;
}[];
release_date: string;
revenue: number;
runtime: number | null;
spoken_languages: {
english_name: string;
iso_639_1: string;
name: string;
}[];
status: string;
tagline: string | null;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface TMDBEpisodeResult {
season: number;
number: number;
title: string;
ids: {
trakt: number;
tvdb: number;
imdb: string;
tmdb: number;
};
}
export interface TMDBShowResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
origin_country: string[];
original_language: string;
original_name: string;
overview: string;
popularity: number;
poster_path: string | null;
first_air_date: string;
name: string;
vote_average: number;
vote_count: number;
}
export interface TMDBShowResponse {
page: number;
results: TMDBShowResult[];
total_pages: number;
total_results: number;
}
export interface TMDBMovieResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
original_language: string;
original_title: string;
overview: string;
popularity: number;
poster_path: string | null;
release_date: string;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface TMDBMovieResponse {
page: number;
results: TMDBMovieResult[];
total_pages: number;
total_results: number;
}
export interface TMDBEpisode {
air_date: string;
episode_number: number;
id: number;
name: string;
overview: string;
production_code: string;
runtime: number;
season_number: number;
show_id: number;
still_path: string | null;
vote_average: number;
vote_count: number;
crew: any[];
guest_stars: any[];
}
export interface TMDBSeason {
_id: string;
air_date: string;
episodes: TMDBEpisode[];
name: string;
overview: string;
id: number;
poster_path: string | null;
season_number: number;
}
export interface TMDBShowExternalIds {
id: number;
imdb_id: null | string;
freebase_mid: null | string;
freebase_id: null | string;
tvdb_id: number;
tvrage_id: null | string;
wikidata_id: null | string;
facebook_id: null | string;
instagram_id: null | string;
twitter_id: null | string;
}
export interface TMDBMovieExternalIds {
id: number;
imdb_id: null | string;
wikidata_id: null | string;
facebook_id: null | string;
instagram_id: null | string;
twitter_id: null | string;
}
export type TMDBExternalIds = TMDBShowExternalIds | TMDBMovieExternalIds;
export interface ExternalIdMovieSearchResult {
movie_results: {
adult: boolean;
backdrop_path: string;
id: number;
title: string;
original_language: string;
original_title: string;
overview: string;
poster_path: string;
media_type: string;
genre_ids: number[];
popularity: number;
release_date: string;
video: boolean;
vote_average: number;
vote_count: number;
}[];
person_results: any[];
tv_results: any[];
tv_episode_results: any[];
tv_season_results: any[];
}

2
src/backend/providers/2embed.ts

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
MWStreamQuality,
MWStreamType,
} from "../helpers/streams";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const twoEmbedBase = "https://www.2embed.to";

2
src/backend/providers/flixhq.ts

@ -7,7 +7,7 @@ import { @@ -7,7 +7,7 @@ import {
import { mwFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register";
import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const flixHqBase = "https://consumet-api-clone.vercel.app/meta/tmdb"; // instance stolen from streaminal :)

2
src/backend/providers/gdriveplayer.ts

@ -3,7 +3,7 @@ import { unpack } from "unpacker"; @@ -3,7 +3,7 @@ import { unpack } from "unpacker";
import { registerProvider } from "@/backend/helpers/register";
import { MWStreamQuality } from "@/backend/helpers/streams";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { proxiedFetch } from "../helpers/fetch";

4
src/backend/providers/gomovies.ts

@ -1,14 +1,14 @@ @@ -1,14 +1,14 @@
import { MWEmbedType } from "../helpers/embed";
import { proxiedFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const gomoviesBase = "https://gomovies.sx";
registerProvider({
id: "gomovies",
displayName: "GOmovies",
rank: 300,
rank: 200,
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, episode }) {

2
src/backend/providers/hdwatched.ts

@ -2,7 +2,7 @@ import { proxiedFetch } from "../helpers/fetch"; @@ -2,7 +2,7 @@ import { proxiedFetch } from "../helpers/fetch";
import { MWProviderContext } from "../helpers/provider";
import { registerProvider } from "../helpers/register";
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const hdwatchedBase = "https://www.hdwatched.xyz";

2
src/backend/providers/kissasian.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { MWEmbedType } from "../helpers/embed";
import { proxiedFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const kissasianBase = "https://kissasian.li";

2
src/backend/providers/m4ufree.ts

@ -2,7 +2,7 @@ import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; @@ -2,7 +2,7 @@ import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
import { proxiedFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const HOST = "m4ufree.com";
const URL_BASE = `https://${HOST}`;

2
src/backend/providers/netfilm.ts

@ -5,7 +5,7 @@ import { @@ -5,7 +5,7 @@ import {
MWStreamQuality,
MWStreamType,
} from "../helpers/streams";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const netfilmBase = "https://net-film.vercel.app";

2
src/backend/providers/remotestream.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { mwFetch } from "@/backend/helpers/fetch";
import { registerProvider } from "@/backend/helpers/register";
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
const remotestreamBase = `https://fsa.remotestre.am`;

2
src/backend/providers/sflix.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { proxiedFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register";
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types";
import { MWMediaType } from "../metadata/types/mw";
const sflixBase = "https://sflix.video";

2
src/backend/providers/streamflix.ts

@ -5,7 +5,7 @@ import { @@ -5,7 +5,7 @@ import {
MWStreamQuality,
MWStreamType,
} from "@/backend/helpers/streams";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
const streamflixBase = "https://us-west2-compute-proxied.streamflix.one";

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

@ -13,7 +13,7 @@ import { @@ -13,7 +13,7 @@ import {
MWStreamQuality,
MWStreamType,
} from "@/backend/helpers/streams";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { compareTitle } from "@/utils/titleMatch";
const nanoid = customAlphabet("0123456789abcdef", 32);
@ -142,7 +142,7 @@ const convertSubtitles = (subtitleGroup: any): MWCaption | null => { @@ -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 }) {

2
src/components/SearchBar.tsx

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw";
import { DropdownButton } from "./buttons/DropdownButton";
import { Icon, Icons } from "./Icon";

23
src/components/media/MediaCard.tsx

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { JWMediaToId } from "@/backend/metadata/justwatch";
import { MWMediaMeta } from "@/backend/metadata/types";
import { TMDBMediaToId } from "@/backend/metadata/getmeta";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { DotList } from "@/components/text/DotList";
import { IconPatch } from "../buttons/IconPatch";
@ -13,7 +13,7 @@ export interface MediaCardProps { @@ -13,7 +13,7 @@ export interface MediaCardProps {
linkable?: boolean;
series?: {
episode: number;
season: number;
season?: number;
episodeId: string;
seasonId: string;
};
@ -72,7 +72,7 @@ function MediaCardContent({ @@ -72,7 +72,7 @@ function MediaCardContent({
].join(" ")}
>
{t("seasons.seasonAndEpisode", {
season: series.season,
season: series.season || 1,
episode: series.episode,
})}
</p>
@ -132,12 +132,17 @@ export function MediaCard(props: MediaCardProps) { @@ -132,12 +132,17 @@ export function MediaCard(props: MediaCardProps) {
const canLink = props.linkable && !props.closable;
let link = canLink
? `/media/${encodeURIComponent(JWMediaToId(props.media))}`
? `/media/${encodeURIComponent(TMDBMediaToId(props.media))}`
: "#";
if (canLink && props.series)
link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent(
props.series.episodeId
)}`;
if (canLink && props.series) {
if (props.series.season === 0 && !props.series.episodeId) {
link += `/${encodeURIComponent(props.series.seasonId)}`;
} else {
link += `/${encodeURIComponent(
props.series.seasonId
)}/${encodeURIComponent(props.series.episodeId)}`;
}
}
if (!props.linkable) return <span>{content}</span>;
return (

2
src/components/media/WatchedMediaCard.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { useMemo } from "react";
import { MWMediaMeta } from "@/backend/metadata/types";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { useWatchedContext } from "@/state/watched";
import { MediaCard } from "./MediaCard";

2
src/hooks/useScrape.ts

@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; @@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
import { findBestStream } from "@/backend/helpers/scrape";
import { MWStream } from "@/backend/helpers/streams";
import { DetailedMeta } from "@/backend/metadata/getmeta";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
export interface ScrapeEventLog {
type: "provider" | "embed";

2
src/hooks/useSearchQuery.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { useState } from "react";
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw";
function getInitialValue(params: { type: string; query: string }) {
const type =

7
src/index.tsx

@ -7,14 +7,15 @@ import { registerSW } from "virtual:pwa-register"; @@ -7,14 +7,15 @@ 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";
import "@/setup/sentry";
import "@/setup/i18n";
import "@/setup/index.css";
import "@/backend";
import { initializeChromecast } from "./setup/chromecast";
import { SettingsStore } from "./state/settings/store";
import { initializeStores } from "./utils/storage";
// initialize
@ -29,7 +30,9 @@ registerSW({ @@ -29,7 +30,9 @@ registerSW({
});
const LazyLoadedApp = React.lazy(async () => {
await assertConfig();
await initializeStores();
i18n.changeLanguage(SettingsStore.get().language ?? "en");
return {
default: App,
};

45
src/setup/App.tsx

@ -1,7 +1,14 @@ @@ -1,7 +1,14 @@
import { lazy } from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import { ReactElement, lazy, useEffect } from "react";
import {
Redirect,
Route,
Switch,
useHistory,
useLocation,
} from "react-router-dom";
import { MWMediaType } from "@/backend/metadata/types";
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { BannerContextProvider } from "@/hooks/useBanner";
import { Layout } from "@/setup/Layout";
import { BookmarkContextProvider } from "@/state/bookmark";
@ -12,6 +19,22 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView"; @@ -12,6 +19,22 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView";
import { V2MigrationView } from "@/views/other/v2Migration";
import { SearchView } from "@/views/search/SearchView";
function LegacyUrlView({ children }: { children: ReactElement }) {
const location = useLocation();
const { replace } = useHistory();
useEffect(() => {
const url = location.pathname;
if (!isLegacyUrl(url)) return;
convertLegacyUrl(location.pathname).then((convertedUrl) => {
replace(convertedUrl ?? "/");
});
}, [location.pathname, replace]);
if (isLegacyUrl(location.pathname)) return null;
return children;
}
function App() {
return (
<SettingsProvider>
@ -27,12 +50,16 @@ function App() { @@ -27,12 +50,16 @@ function App() {
</Route>
{/* pages */}
<Route exact path="/media/:media" component={MediaView} />
<Route
exact
path="/media/:media/:season/:episode"
component={MediaView}
/>
<Route exact path="/media/:media">
<LegacyUrlView>
<MediaView />
</LegacyUrlView>
</Route>
<Route exact path="/media/:media/:season/:episode">
<LegacyUrlView>
<MediaView />
</LegacyUrlView>
</Route>
<Route
exact
path="/search/:type/:query?"

43
src/setup/config.ts

@ -4,8 +4,7 @@ interface Config { @@ -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 { @@ -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> = { @@ -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;
}
function getKey(key: keyof Config, defaultString?: string): string {
return getKeyValue(key) ?? defaultString ?? "";
}
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 {
@ -56,8 +56,7 @@ 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()),

2
src/state/bookmark/context.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { ReactNode, createContext, useContext, useMemo } from "react";
import { MWMediaMeta } from "@/backend/metadata/types";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { useStore } from "@/utils/storage";
import { BookmarkStore } from "./store";

7
src/state/bookmark/store.ts

@ -2,6 +2,7 @@ import { createVersionedStore } from "@/utils/storage"; @@ -2,6 +2,7 @@ import { createVersionedStore } from "@/utils/storage";
import { BookmarkStoreData } from "./types";
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
import { migrateV2Bookmarks } from "../watched/migrations/v3";
export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
.setKey("mw-bookmarks")
@ -13,6 +14,12 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>() @@ -13,6 +14,12 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
})
.addVersion({
version: 1,
migrate(old: BookmarkStoreData) {
return migrateV2Bookmarks(old);
},
})
.addVersion({
version: 2,
create() {
return {
bookmarks: [],

2
src/state/bookmark/types.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { MWMediaMeta } from "@/backend/metadata/types";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
export interface BookmarkStoreData {
bookmarks: MWMediaMeta[];

2
src/state/watched/context.tsx

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
} from "react";
import { DetailedMeta } from "@/backend/metadata/getmeta";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { useStore } from "@/utils/storage";
import { VideoProgressStore } from "./store";

2
src/state/watched/migrations/v2.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
import { searchForMedia } from "@/backend/metadata/search";
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types";
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types/mw";
import { compareTitle } from "@/utils/titleMatch";
import { WatchedStoreData, WatchedStoreItem } from "../types";

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

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
import { getLegacyMetaFromId } from "@/backend/metadata/getmeta";
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: string,
type: MWMediaType
): Promise<string | undefined> {
const meta = await getLegacyMetaFromId(type, id);
if (!meta) return undefined;
const { tmdbId, imdbId } = meta;
if (!tmdbId && !imdbId) return undefined;
// movies always have an imdb id on tmdb
if (imdbId && type === MWMediaType.MOVIE) {
const movieId = await getMovieFromExternalId(imdbId);
if (movieId) return movieId;
}
if (tmdbId) {
return tmdbId;
}
}
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: WatchedStoreData
): Promise<WatchedStoreData> {
const updatedItems = await Promise.all(
old.items.map(async (progress) => {
try {
const migratedId = await migrateId(
progress.item.meta.id,
progress.item.meta.type
);
if (!migratedId) return null;
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;
}
})
);
return {
items: updatedItems.filter(isNotNull),
};
}

7
src/state/watched/store.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { createVersionedStore } from "@/utils/storage";
import { OldData, migrateV2Videos } from "./migrations/v2";
import { migrateV3Videos } from "./migrations/v3";
import { WatchedStoreData } from "./types";
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
@ -21,6 +22,12 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>() @@ -21,6 +22,12 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
})
.addVersion({
version: 2,
migrate(old: WatchedStoreData) {
return migrateV3Videos(old);
},
})
.addVersion({
version: 3,
create() {
return {
items: [],

2
src/state/watched/types.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { MWMediaMeta } from "@/backend/metadata/types";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
export interface StoreMediaItem {
meta: MWMediaMeta;

7
src/utils/storage.ts

@ -46,8 +46,13 @@ export async function initializeStores() { @@ -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);

3
src/utils/typeguard.ts

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

2
src/video/components/actions/DividerAction.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useMeta } from "@/video/state/logic/meta";

2
src/video/components/actions/SeriesSelectionAction.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { Icons } from "@/components/Icon";
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";

2
src/video/components/controllers/MetaController.tsx

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { useEffect } from "react";
import { MWCaption } from "@/backend/helpers/streams";
import { MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
import { MWSeasonWithEpisodeMeta } from "@/backend/metadata/types/mw";
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useControls } from "@/video/state/logic/controls";
import { VideoPlayerMeta } from "@/video/state/types";

2
src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { useMeta } from "@/video/state/logic/meta";
export function useCurrentSeriesEpisodeInfo(descriptor: string) {

2
src/video/components/parts/VideoErrorBoundary.tsx

@ -2,7 +2,7 @@ import { Component } from "react"; @@ -2,7 +2,7 @@ import { Component } from "react";
import { Trans } from "react-i18next";
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
import { MWMediaMeta } from "@/backend/metadata/types";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
import { Link } from "@/components/text/Link";
import { conf } from "@/setup/config";

2
src/video/components/parts/VideoPlayerHeader.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { MWMediaMeta } from "@/backend/metadata/types";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icon, Icons } from "@/components/Icon";
import { BrandPill } from "@/components/layout/BrandPill";

10
src/video/components/popouts/EpisodeSelectionPopout.tsx

@ -2,9 +2,11 @@ import { useCallback, useMemo, useState } from "react"; @@ -2,9 +2,11 @@ import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeJWId } from "@/backend/metadata/justwatch";
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
import { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta";
import {
MWMediaType,
MWSeasonWithEpisodeMeta,
} from "@/backend/metadata/types/mw";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icon, Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading";
@ -45,7 +47,7 @@ export function EpisodeSelectionPopout() { @@ -45,7 +47,7 @@ export function EpisodeSelectionPopout() {
seasonId: sId,
season: undefined,
});
reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => {
reqSeasonMeta(decodeTMDBId(params.media)?.id as string, sId).then((v) => {
if (v?.meta.type !== MWMediaType.SERIES) return;
setCurrentVisibleSeason({
seasonId: sId,

2
src/views/developer/VideoTesterView.tsx

@ -3,7 +3,7 @@ import { Helmet } from "react-helmet"; @@ -3,7 +3,7 @@ import { Helmet } from "react-helmet";
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
import { DetailedMeta } from "@/backend/metadata/getmeta";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { Button } from "@/components/Button";
import { Dropdown } from "@/components/Dropdown";
import { Navigation } from "@/components/layout/Navigation";

14
src/views/media/MediaView.tsx

@ -4,9 +4,15 @@ import { useTranslation } from "react-i18next"; @@ -4,9 +4,15 @@ import { useTranslation } from "react-i18next";
import { useHistory, useParams } from "react-router-dom";
import { MWStream } from "@/backend/helpers/streams";
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeJWId } from "@/backend/metadata/justwatch";
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
import {
DetailedMeta,
decodeTMDBId,
getMetaFromId,
} from "@/backend/metadata/getmeta";
import {
MWMediaType,
MWSeasonWithEpisodeMeta,
} from "@/backend/metadata/types/mw";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading";
@ -181,7 +187,7 @@ export function MediaView() { @@ -181,7 +187,7 @@ export function MediaView() {
const [selected, setSelected] = useState<SelectedMediaData | null>(null);
const [exec, loading, error] = useLoading(
async (mediaParams: string, seasonId?: string) => {
const data = decodeJWId(mediaParams);
const data = decodeTMDBId(mediaParams);
if (!data) return null;
return getMetaFromId(data.type, data.id, seasonId);
}

2
src/views/other/v2Migration.tsx

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import pako from "pako";
import { useEffect, useState } from "react";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { conf } from "@/setup/config";
function fromBinary(str: string): Uint8Array {

2
src/views/search/SearchResultsPartial.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { MWQuery } from "@/backend/metadata/types";
import { MWQuery } from "@/backend/metadata/types/mw";
import { useDebounce } from "@/hooks/useDebounce";
import { HomeView } from "./HomeView";

2
src/views/search/SearchResultsView.tsx

@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; @@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { searchForMedia } from "@/backend/metadata/search";
import { MWMediaMeta, MWQuery } from "@/backend/metadata/types";
import { MWMediaMeta, MWQuery } from "@/backend/metadata/types/mw";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading";

Loading…
Cancel
Save