mirror of
https://github.com/movie-web/movie-web.git
synced 2025-01-14 14:39:06 +01:00
Merge branch 'dev' into meta/code-of-conduct
This commit is contained in:
commit
d2d710ad37
@ -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).
|
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, Visual Studio Code or similar.
|
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",`
|
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!).
|
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.
|
It doesn't require PHP, it's just a standard static page.
|
||||||
|
@ -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=...
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "movie-web",
|
"name": "movie-web",
|
||||||
"version": "3.0.15",
|
"version": "3.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie-web.app",
|
"homepage": "https://movie-web.app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -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",
|
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import "@/backend";
|
|||||||
import { testData } from "@/__tests__/providers/testdata";
|
import { testData } from "@/__tests__/providers/testdata";
|
||||||
import { getProviders } from "@/backend/helpers/register";
|
import { getProviders } from "@/backend/helpers/register";
|
||||||
import { runProvider } from "@/backend/helpers/run";
|
import { runProvider } from "@/backend/helpers/run";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
|
||||||
describe("providers", () => {
|
describe("providers", () => {
|
||||||
const providers = getProviders();
|
const providers = getProviders();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
|
||||||
export const testData: DetailedMeta[] = [
|
export const testData: DetailedMeta[] = [
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MWEmbed } from "./embed";
|
import { MWEmbed } from "./embed";
|
||||||
import { MWStream } from "./streams";
|
import { MWStream } from "./streams";
|
||||||
import { DetailedMeta } from "../metadata/getmeta";
|
import { DetailedMeta } from "../metadata/getmeta";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
export type MWProviderScrapeResult = {
|
export type MWProviderScrapeResult = {
|
||||||
stream?: MWStream;
|
stream?: MWStream;
|
||||||
|
@ -3,7 +3,7 @@ import { getEmbedScraperByType, getProviders } from "./register";
|
|||||||
import { runEmbedScraper, runProvider } from "./run";
|
import { runEmbedScraper, runProvider } from "./run";
|
||||||
import { MWStream } from "./streams";
|
import { MWStream } from "./streams";
|
||||||
import { DetailedMeta } from "../metadata/getmeta";
|
import { DetailedMeta } from "../metadata/getmeta";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
interface MWProgressData {
|
interface MWProgressData {
|
||||||
type: "embed" | "provider";
|
type: "embed" | "provider";
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
import { FetchError } from "ofetch";
|
import { FetchError } from "ofetch";
|
||||||
|
|
||||||
|
import { formatJWMeta, mediaTypeToJW } from "./justwatch";
|
||||||
|
import {
|
||||||
|
TMDBMediaToMediaType,
|
||||||
|
formatTMDBMeta,
|
||||||
|
getEpisodes,
|
||||||
|
getExternalIds,
|
||||||
|
getMediaDetails,
|
||||||
|
getMediaPoster,
|
||||||
|
getMovieFromExternalId,
|
||||||
|
mediaTypeToTMDB,
|
||||||
|
} from "./tmdb";
|
||||||
import {
|
import {
|
||||||
JWMediaResult,
|
JWMediaResult,
|
||||||
JWSeasonMetaResult,
|
JWSeasonMetaResult,
|
||||||
JW_API_BASE,
|
JW_API_BASE,
|
||||||
formatJWMeta,
|
} from "./types/justwatch";
|
||||||
mediaTypeToJW,
|
import { MWMediaMeta, MWMediaType } from "./types/mw";
|
||||||
} from "./justwatch";
|
import {
|
||||||
import { MWMediaMeta, MWMediaType } from "./types";
|
TMDBMediaResult,
|
||||||
|
TMDBMovieData,
|
||||||
|
TMDBSeasonMetaResult,
|
||||||
|
TMDBShowData,
|
||||||
|
} from "./types/tmdb";
|
||||||
import { makeUrl, proxiedFetch } from "../helpers/fetch";
|
import { makeUrl, proxiedFetch } from "../helpers/fetch";
|
||||||
|
|
||||||
type JWExternalIdType =
|
type JWExternalIdType =
|
||||||
@ -33,10 +48,92 @@ export interface DetailedMeta {
|
|||||||
tmdbId?: string;
|
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(
|
export async function getMetaFromId(
|
||||||
type: MWMediaType,
|
type: MWMediaType,
|
||||||
id: string,
|
id: string,
|
||||||
seasonId?: 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> {
|
): Promise<DetailedMeta | null> {
|
||||||
const queryType = mediaTypeToJW(type);
|
const queryType = mediaTypeToJW(type);
|
||||||
|
|
||||||
@ -82,3 +179,55 @@ export async function getMetaFromId(
|
|||||||
tmdbId,
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,38 +1,10 @@
|
|||||||
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types";
|
import {
|
||||||
|
JWContentTypes,
|
||||||
export const JW_API_BASE = "https://apis.justwatch.com";
|
JWMediaResult,
|
||||||
export const JW_IMAGE_BASE = "https://images.justwatch.com";
|
JWSeasonMetaResult,
|
||||||
|
JW_IMAGE_BASE,
|
||||||
export type JWContentTypes = "movie" | "show";
|
} from "./types/justwatch";
|
||||||
|
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
|
||||||
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[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function mediaTypeToJW(type: MWMediaType): JWContentTypes {
|
export function mediaTypeToJW(type: MWMediaType): JWContentTypes {
|
||||||
if (type === MWMediaType.MOVIE) return "movie";
|
if (type === MWMediaType.MOVIE) return "movie";
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { SimpleCache } from "@/utils/cache";
|
import { SimpleCache } from "@/utils/cache";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
JWContentTypes,
|
formatTMDBMeta,
|
||||||
JWMediaResult,
|
formatTMDBSearchResult,
|
||||||
JW_API_BASE,
|
mediaTypeToTMDB,
|
||||||
formatJWMeta,
|
searchMedia,
|
||||||
mediaTypeToJW,
|
} from "./tmdb";
|
||||||
} from "./justwatch";
|
import { MWMediaMeta, MWQuery } from "./types/mw";
|
||||||
import { MWMediaMeta, MWQuery } from "./types";
|
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
|
||||||
|
|
||||||
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
|
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
|
||||||
cache.setCompare((a, b) => {
|
cache.setCompare((a, b) => {
|
||||||
@ -16,44 +14,16 @@ cache.setCompare((a, b) => {
|
|||||||
});
|
});
|
||||||
cache.initialize();
|
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[]> {
|
export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
|
||||||
if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
|
if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
|
||||||
const { searchQuery, type } = query;
|
const { searchQuery, type } = query;
|
||||||
|
|
||||||
const contentType = mediaTypeToJW(type);
|
const data = await searchMedia(searchQuery, mediaTypeToTMDB(type));
|
||||||
const body: JWSearchQuery = {
|
const results = data.results.map((v) => {
|
||||||
content_types: [contentType],
|
const formattedResult = formatTMDBSearchResult(v, mediaTypeToTMDB(type));
|
||||||
page: 1,
|
return formatTMDBMeta(formattedResult);
|
||||||
query: searchQuery,
|
});
|
||||||
page_size: 40,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await proxiedFetch<JWPage<JWMediaResult>>(
|
cache.set(query, results, 3600); // cache results for 1 hour
|
||||||
"/content/titles/en_US/popular",
|
return results;
|
||||||
{
|
|
||||||
baseURL: JW_API_BASE,
|
|
||||||
params: {
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const returnData = data.items.map<MWMediaMeta>((v) => formatJWMeta(v));
|
|
||||||
cache.set(query, returnData, 3600); // cache for an hour
|
|
||||||
return returnData;
|
|
||||||
}
|
}
|
||||||
|
239
src/backend/metadata/tmdb.ts
Normal file
239
src/backend/metadata/tmdb.ts
Normal file
@ -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
Normal file
48
src/backend/metadata/types/justwatch.ts
Normal file
@ -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[];
|
||||||
|
};
|
@ -45,3 +45,9 @@ export interface MWQuery {
|
|||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
type: MWMediaType;
|
type: MWMediaType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DetailedMeta {
|
||||||
|
meta: MWMediaMeta;
|
||||||
|
imdbId?: string;
|
||||||
|
tmdbId?: string;
|
||||||
|
}
|
308
src/backend/metadata/types/tmdb.ts
Normal file
308
src/backend/metadata/types/tmdb.ts
Normal file
@ -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[];
|
||||||
|
}
|
@ -8,7 +8,7 @@ import {
|
|||||||
MWStreamQuality,
|
MWStreamQuality,
|
||||||
MWStreamType,
|
MWStreamType,
|
||||||
} from "../helpers/streams";
|
} from "../helpers/streams";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
const twoEmbedBase = "https://www.2embed.to";
|
const twoEmbedBase = "https://www.2embed.to";
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
import { mwFetch } from "../helpers/fetch";
|
import { mwFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams";
|
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 :)
|
const flixHqBase = "https://consumet-api-clone.vercel.app/meta/tmdb"; // instance stolen from streaminal :)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { unpack } from "unpacker";
|
|||||||
|
|
||||||
import { registerProvider } from "@/backend/helpers/register";
|
import { registerProvider } from "@/backend/helpers/register";
|
||||||
import { MWStreamQuality } from "@/backend/helpers/streams";
|
import { MWStreamQuality } from "@/backend/helpers/streams";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { MWEmbedType } from "../helpers/embed";
|
import { MWEmbedType } from "../helpers/embed";
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
const gomoviesBase = "https://gomovies.sx";
|
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,7 +2,7 @@ import { proxiedFetch } from "../helpers/fetch";
|
|||||||
import { MWProviderContext } from "../helpers/provider";
|
import { MWProviderContext } from "../helpers/provider";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
const hdwatchedBase = "https://www.hdwatched.xyz";
|
const hdwatchedBase = "https://www.hdwatched.xyz";
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MWEmbedType } from "../helpers/embed";
|
import { MWEmbedType } from "../helpers/embed";
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
const kissasianBase = "https://kissasian.li";
|
const kissasianBase = "https://kissasian.li";
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
|
|||||||
|
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
const HOST = "m4ufree.com";
|
const HOST = "m4ufree.com";
|
||||||
const URL_BASE = `https://${HOST}`;
|
const URL_BASE = `https://${HOST}`;
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
MWStreamQuality,
|
MWStreamQuality,
|
||||||
MWStreamType,
|
MWStreamType,
|
||||||
} from "../helpers/streams";
|
} from "../helpers/streams";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
const netfilmBase = "https://net-film.vercel.app";
|
const netfilmBase = "https://net-film.vercel.app";
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { mwFetch } from "@/backend/helpers/fetch";
|
import { mwFetch } from "@/backend/helpers/fetch";
|
||||||
import { registerProvider } from "@/backend/helpers/register";
|
import { registerProvider } from "@/backend/helpers/register";
|
||||||
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
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`;
|
const remotestreamBase = `https://fsa.remotestre.am`;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types/mw";
|
||||||
|
|
||||||
const sflixBase = "https://sflix.video";
|
const sflixBase = "https://sflix.video";
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
MWStreamQuality,
|
MWStreamQuality,
|
||||||
MWStreamType,
|
MWStreamType,
|
||||||
} from "@/backend/helpers/streams";
|
} 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";
|
const streamflixBase = "https://us-west2-compute-proxied.streamflix.one";
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
MWStreamQuality,
|
MWStreamQuality,
|
||||||
MWStreamType,
|
MWStreamType,
|
||||||
} from "@/backend/helpers/streams";
|
} from "@/backend/helpers/streams";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { compareTitle } from "@/utils/titleMatch";
|
||||||
|
|
||||||
const nanoid = customAlphabet("0123456789abcdef", 32);
|
const nanoid = customAlphabet("0123456789abcdef", 32);
|
||||||
@ -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 }) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { DropdownButton } from "./buttons/DropdownButton";
|
||||||
import { Icon, Icons } from "./Icon";
|
import { Icon, Icons } from "./Icon";
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { JWMediaToId } from "@/backend/metadata/justwatch";
|
import { TMDBMediaToId } from "@/backend/metadata/getmeta";
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types/mw";
|
||||||
import { DotList } from "@/components/text/DotList";
|
import { DotList } from "@/components/text/DotList";
|
||||||
|
|
||||||
import { IconPatch } from "../buttons/IconPatch";
|
import { IconPatch } from "../buttons/IconPatch";
|
||||||
@ -13,7 +13,7 @@ export interface MediaCardProps {
|
|||||||
linkable?: boolean;
|
linkable?: boolean;
|
||||||
series?: {
|
series?: {
|
||||||
episode: number;
|
episode: number;
|
||||||
season: number;
|
season?: number;
|
||||||
episodeId: string;
|
episodeId: string;
|
||||||
seasonId: string;
|
seasonId: string;
|
||||||
};
|
};
|
||||||
@ -72,7 +72,7 @@ function MediaCardContent({
|
|||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{t("seasons.seasonAndEpisode", {
|
{t("seasons.seasonAndEpisode", {
|
||||||
season: series.season,
|
season: series.season || 1,
|
||||||
episode: series.episode,
|
episode: series.episode,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
@ -132,12 +132,17 @@ export function MediaCard(props: MediaCardProps) {
|
|||||||
const canLink = props.linkable && !props.closable;
|
const canLink = props.linkable && !props.closable;
|
||||||
|
|
||||||
let link = canLink
|
let link = canLink
|
||||||
? `/media/${encodeURIComponent(JWMediaToId(props.media))}`
|
? `/media/${encodeURIComponent(TMDBMediaToId(props.media))}`
|
||||||
: "#";
|
: "#";
|
||||||
if (canLink && props.series)
|
if (canLink && props.series) {
|
||||||
link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent(
|
if (props.series.season === 0 && !props.series.episodeId) {
|
||||||
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>;
|
if (!props.linkable) return <span>{content}</span>;
|
||||||
return (
|
return (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types/mw";
|
||||||
import { useWatchedContext } from "@/state/watched";
|
import { useWatchedContext } from "@/state/watched";
|
||||||
|
|
||||||
import { MediaCard } from "./MediaCard";
|
import { MediaCard } from "./MediaCard";
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { findBestStream } from "@/backend/helpers/scrape";
|
import { findBestStream } from "@/backend/helpers/scrape";
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
|
||||||
export interface ScrapeEventLog {
|
export interface ScrapeEventLog {
|
||||||
type: "provider" | "embed";
|
type: "provider" | "embed";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
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 }) {
|
function getInitialValue(params: { type: string; query: string }) {
|
||||||
const type =
|
const type =
|
||||||
|
@ -7,14 +7,15 @@ 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 "@/setup/ga";
|
import "@/setup/ga";
|
||||||
import "@/setup/sentry";
|
import "@/setup/sentry";
|
||||||
import "@/setup/i18n";
|
|
||||||
import "@/setup/index.css";
|
import "@/setup/index.css";
|
||||||
import "@/backend";
|
import "@/backend";
|
||||||
import { initializeChromecast } from "./setup/chromecast";
|
import { initializeChromecast } from "./setup/chromecast";
|
||||||
|
import { SettingsStore } from "./state/settings/store";
|
||||||
import { initializeStores } from "./utils/storage";
|
import { initializeStores } from "./utils/storage";
|
||||||
|
|
||||||
// initialize
|
// initialize
|
||||||
@ -29,7 +30,9 @@ registerSW({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const LazyLoadedApp = React.lazy(async () => {
|
const LazyLoadedApp = React.lazy(async () => {
|
||||||
|
await assertConfig();
|
||||||
await initializeStores();
|
await initializeStores();
|
||||||
|
i18n.changeLanguage(SettingsStore.get().language ?? "en");
|
||||||
return {
|
return {
|
||||||
default: App,
|
default: App,
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { lazy } from "react";
|
import { ReactElement, lazy, useEffect } from "react";
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
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 { BannerContextProvider } from "@/hooks/useBanner";
|
||||||
import { Layout } from "@/setup/Layout";
|
import { Layout } from "@/setup/Layout";
|
||||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||||
@ -12,6 +19,22 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
|||||||
import { V2MigrationView } from "@/views/other/v2Migration";
|
import { V2MigrationView } from "@/views/other/v2Migration";
|
||||||
import { SearchView } from "@/views/search/SearchView";
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
@ -27,12 +50,16 @@ function App() {
|
|||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* pages */}
|
{/* pages */}
|
||||||
<Route exact path="/media/:media" component={MediaView} />
|
<Route exact path="/media/:media">
|
||||||
<Route
|
<LegacyUrlView>
|
||||||
exact
|
<MediaView />
|
||||||
path="/media/:media/:season/:episode"
|
</LegacyUrlView>
|
||||||
component={MediaView}
|
</Route>
|
||||||
/>
|
<Route exact path="/media/:media/:season/:episode">
|
||||||
|
<LegacyUrlView>
|
||||||
|
<MediaView />
|
||||||
|
</LegacyUrlView>
|
||||||
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/search/:type/:query?"
|
path="/search/:type/:query?"
|
||||||
|
@ -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()),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ReactNode, createContext, useContext, useMemo } from "react";
|
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 { useStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { BookmarkStore } from "./store";
|
import { BookmarkStore } from "./store";
|
||||||
|
@ -2,6 +2,7 @@ import { createVersionedStore } from "@/utils/storage";
|
|||||||
|
|
||||||
import { BookmarkStoreData } from "./types";
|
import { BookmarkStoreData } from "./types";
|
||||||
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
||||||
|
import { migrateV2Bookmarks } from "../watched/migrations/v3";
|
||||||
|
|
||||||
export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
||||||
.setKey("mw-bookmarks")
|
.setKey("mw-bookmarks")
|
||||||
@ -13,6 +14,12 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
|||||||
})
|
})
|
||||||
.addVersion({
|
.addVersion({
|
||||||
version: 1,
|
version: 1,
|
||||||
|
migrate(old: BookmarkStoreData) {
|
||||||
|
return migrateV2Bookmarks(old);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addVersion({
|
||||||
|
version: 2,
|
||||||
create() {
|
create() {
|
||||||
return {
|
return {
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types/mw";
|
||||||
|
|
||||||
export interface BookmarkStoreData {
|
export interface BookmarkStoreData {
|
||||||
bookmarks: MWMediaMeta[];
|
bookmarks: MWMediaMeta[];
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
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 { useStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { VideoProgressStore } from "./store";
|
import { VideoProgressStore } from "./store";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { searchForMedia } from "@/backend/metadata/search";
|
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 { compareTitle } from "@/utils/titleMatch";
|
||||||
|
|
||||||
import { WatchedStoreData, WatchedStoreItem } from "../types";
|
import { WatchedStoreData, WatchedStoreItem } from "../types";
|
||||||
|
89
src/state/watched/migrations/v3.ts
Normal file
89
src/state/watched/migrations/v3.ts
Normal file
@ -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),
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { createVersionedStore } from "@/utils/storage";
|
import { createVersionedStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { OldData, migrateV2Videos } from "./migrations/v2";
|
import { OldData, migrateV2Videos } from "./migrations/v2";
|
||||||
|
import { migrateV3Videos } from "./migrations/v3";
|
||||||
import { WatchedStoreData } from "./types";
|
import { WatchedStoreData } from "./types";
|
||||||
|
|
||||||
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
||||||
@ -21,6 +22,12 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
|||||||
})
|
})
|
||||||
.addVersion({
|
.addVersion({
|
||||||
version: 2,
|
version: 2,
|
||||||
|
migrate(old: WatchedStoreData) {
|
||||||
|
return migrateV3Videos(old);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addVersion({
|
||||||
|
version: 3,
|
||||||
create() {
|
create() {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types/mw";
|
||||||
|
|
||||||
export interface StoreMediaItem {
|
export interface StoreMediaItem {
|
||||||
meta: MWMediaMeta;
|
meta: MWMediaMeta;
|
||||||
|
@ -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
3
src/utils/typeguard.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isNotNull<T>(obj: T | null): obj is T {
|
||||||
|
return obj != null;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
||||||
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { MWCaption } from "@/backend/helpers/streams";
|
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 { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { VideoPlayerMeta } from "@/video/state/types";
|
import { VideoPlayerMeta } from "@/video/state/types";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
|
|
||||||
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
||||||
|
@ -2,7 +2,7 @@ import { Component } from "react";
|
|||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
|
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 { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
import { Link } from "@/components/text/Link";
|
import { Link } from "@/components/text/Link";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
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 { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { BrandPill } from "@/components/layout/BrandPill";
|
import { BrandPill } from "@/components/layout/BrandPill";
|
||||||
|
@ -2,9 +2,11 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { getMetaFromId } from "@/backend/metadata/getmeta";
|
import { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { decodeJWId } from "@/backend/metadata/justwatch";
|
import {
|
||||||
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
|
MWMediaType,
|
||||||
|
MWSeasonWithEpisodeMeta,
|
||||||
|
} from "@/backend/metadata/types/mw";
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
@ -45,7 +47,7 @@ export function EpisodeSelectionPopout() {
|
|||||||
seasonId: sId,
|
seasonId: sId,
|
||||||
season: undefined,
|
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;
|
if (v?.meta.type !== MWMediaType.SERIES) return;
|
||||||
setCurrentVisibleSeason({
|
setCurrentVisibleSeason({
|
||||||
seasonId: sId,
|
seasonId: sId,
|
||||||
|
@ -3,7 +3,7 @@ import { Helmet } from "react-helmet";
|
|||||||
|
|
||||||
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
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 { Button } from "@/components/Button";
|
||||||
import { Dropdown } from "@/components/Dropdown";
|
import { Dropdown } from "@/components/Dropdown";
|
||||||
import { Navigation } from "@/components/layout/Navigation";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
|
@ -4,9 +4,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
import {
|
||||||
import { decodeJWId } from "@/backend/metadata/justwatch";
|
DetailedMeta,
|
||||||
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
|
decodeTMDBId,
|
||||||
|
getMetaFromId,
|
||||||
|
} from "@/backend/metadata/getmeta";
|
||||||
|
import {
|
||||||
|
MWMediaType,
|
||||||
|
MWSeasonWithEpisodeMeta,
|
||||||
|
} from "@/backend/metadata/types/mw";
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
@ -181,7 +187,7 @@ export function MediaView() {
|
|||||||
const [selected, setSelected] = useState<SelectedMediaData | null>(null);
|
const [selected, setSelected] = useState<SelectedMediaData | null>(null);
|
||||||
const [exec, loading, error] = useLoading(
|
const [exec, loading, error] = useLoading(
|
||||||
async (mediaParams: string, seasonId?: string) => {
|
async (mediaParams: string, seasonId?: string) => {
|
||||||
const data = decodeJWId(mediaParams);
|
const data = decodeTMDBId(mediaParams);
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
return getMetaFromId(data.type, data.id, seasonId);
|
return getMetaFromId(data.type, data.id, seasonId);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pako from "pako";
|
import pako from "pako";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
function fromBinary(str: string): Uint8Array {
|
function fromBinary(str: string): Uint8Array {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
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 { useDebounce } from "@/hooks/useDebounce";
|
||||||
|
|
||||||
import { HomeView } from "./HomeView";
|
import { HomeView } from "./HomeView";
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { searchForMedia } from "@/backend/metadata/search";
|
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 { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user