mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 09:51:51 +01:00
Merge branch 'v4' into thumbnails
This commit is contained in:
commit
1f1f5d779b
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "movie-web",
|
"name": "movie-web",
|
||||||
"version": "3.0.15",
|
"version": "4.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie-web.app",
|
"homepage": "https://movie-web.app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -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: (details as TMDBMovieData).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;
|
|
||||||
}
|
}
|
||||||
|
236
src/backend/metadata/tmdb.ts
Normal file
236
src/backend/metadata/tmdb.ts
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
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_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMediaDetails(id: string, type: TMDBContentTypes) {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "movie":
|
||||||
|
data = await get<TMDBMovieData>(`/movie/${id}`);
|
||||||
|
break;
|
||||||
|
case "show":
|
||||||
|
data = await get<TMDBShowData>(`/tv/${id}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid media type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,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 gomoviesBase = "https://gomovies.sx";
|
const gomoviesBase = "https://gomovies.sx";
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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 =
|
||||||
|
@ -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?"
|
||||||
|
@ -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: OldBookmarks) {
|
||||||
|
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";
|
||||||
|
81
src/state/watched/migrations/v3.ts
Normal file
81
src/state/watched/migrations/v3.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { getLegacyMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
|
import { getMovieFromExternalId } from "@/backend/metadata/tmdb";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
|
||||||
|
import { WatchedStoreData } from "../types";
|
||||||
|
|
||||||
|
async function migrateId(
|
||||||
|
id: number,
|
||||||
|
type: MWMediaType
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const meta = await getLegacyMetaFromId(type, id.toString());
|
||||||
|
|
||||||
|
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: any) {
|
||||||
|
const oldData = old;
|
||||||
|
if (!oldData) return;
|
||||||
|
|
||||||
|
const updatedBookmarks = oldData.bookmarks.map(
|
||||||
|
async (item: { id: number; type: MWMediaType }) => ({
|
||||||
|
...item,
|
||||||
|
id: await migrateId(item.id, item.type),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bookmarks: (await Promise.all(updatedBookmarks)).filter((item) => item.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function migrateV3Videos(old: any) {
|
||||||
|
const oldData = old;
|
||||||
|
if (!oldData) return;
|
||||||
|
|
||||||
|
const updatedItems = await Promise.all(
|
||||||
|
oldData.items.map(async (item: any) => {
|
||||||
|
const migratedId = await migrateId(
|
||||||
|
item.item.meta.id,
|
||||||
|
item.item.meta.type
|
||||||
|
);
|
||||||
|
|
||||||
|
const migratedItem = {
|
||||||
|
...item,
|
||||||
|
item: {
|
||||||
|
...item.item,
|
||||||
|
meta: {
|
||||||
|
...item.item.meta,
|
||||||
|
id: migratedId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
item: migratedId ? migratedItem : item.item,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const newData: WatchedStoreData = {
|
||||||
|
items: updatedItems.map((item) => item.item),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
items: newData.items,
|
||||||
|
};
|
||||||
|
}
|
@ -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: OldData) {
|
||||||
|
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;
|
||||||
|
@ -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…
Reference in New Issue
Block a user