1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

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

This commit is contained in:
William Oldham 2023-07-03 17:59:45 +01:00 committed by GitHub
commit d2d710ad37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1021 additions and 179 deletions

View file

@ -31,10 +31,11 @@ Your proxy is now hosted on Cloudflare. Note the url of your worker as you will
1. Download the file `movie-web.zip` from the latest release: [https://github.com/movie-web/movie-web/releases/latest](https://github.com/movie-web/movie-web/releases/latest).
2. Extract the zip file so you can edit the files.
3. Open `config.js` in Notepad, Visual Studio Code or similar.
4. Put your Cloudflare proxy URL in-between the double quotes of `VITE_CORS_PROXY_URL: "",`. Make sure to not have a slash at the end of your URL.
4. Put your Cloudflare proxy URL in-between the double quotes of `VITE_CORS_PROXY_URL: ""`. Make sure to not have a slash at the end of your URL.
Example (THIS IS MINE, IT WONT WORK FOR YOU): `VITE_CORS_PROXY_URL: "https://test-proxy.test.workers.dev",`
5. Save the file.
5. Put your TMDB read access token inside the quotes of `VITE_TMDB_READ_API_KEY: ""`. You can generate it for free at [https://www.themoviedb.org/settings/api](https://www.themoviedb.org/settings/api).
6. Save the file
Your client has now been prepared, you can now host it with any static website hosting (Common ones include [GitHub Pages](https://pages.github.com/), [Netlify](https://www.netlify.com/) and [Vercel](https://vercel.com/) but any will work!).
It doesn't require PHP, it's just a standard static page.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,28 @@
import { FetchError } from "ofetch";
import { formatJWMeta, mediaTypeToJW } from "./justwatch";
import {
TMDBMediaToMediaType,
formatTMDBMeta,
getEpisodes,
getExternalIds,
getMediaDetails,
getMediaPoster,
getMovieFromExternalId,
mediaTypeToTMDB,
} from "./tmdb";
import {
JWMediaResult,
JWSeasonMetaResult,
JW_API_BASE,
formatJWMeta,
mediaTypeToJW,
} from "./justwatch";
import { MWMediaMeta, MWMediaType } from "./types";
} from "./types/justwatch";
import { MWMediaMeta, MWMediaType } from "./types/mw";
import {
TMDBMediaResult,
TMDBMovieData,
TMDBSeasonMetaResult,
TMDBShowData,
} from "./types/tmdb";
import { makeUrl, proxiedFetch } from "../helpers/fetch";
type JWExternalIdType =
@ -33,10 +48,92 @@ export interface DetailedMeta {
tmdbId?: string;
}
export function formatTMDBMetaResult(
details: TMDBShowData | TMDBMovieData,
type: MWMediaType
): TMDBMediaResult {
if (type === MWMediaType.MOVIE) {
const movie = details as TMDBMovieData;
return {
id: details.id,
title: movie.title,
object_type: mediaTypeToTMDB(type),
poster: getMediaPoster(movie.poster_path) ?? undefined,
original_release_year: new Date(movie.release_date).getFullYear(),
};
}
if (type === MWMediaType.SERIES) {
const show = details as TMDBShowData;
return {
id: details.id,
title: show.name,
object_type: mediaTypeToTMDB(type),
seasons: show.seasons.map((v) => ({
id: v.id,
season_number: v.season_number,
title: v.name,
})),
poster: getMediaPoster(show.poster_path) ?? undefined,
original_release_year: new Date(show.first_air_date).getFullYear(),
};
}
throw new Error("unsupported type");
}
export async function getMetaFromId(
type: MWMediaType,
id: string,
seasonId?: string
): Promise<DetailedMeta | null> {
const details = await getMediaDetails(id, mediaTypeToTMDB(type));
if (!details) return null;
const externalIds = await getExternalIds(id, mediaTypeToTMDB(type));
const imdbId = externalIds.imdb_id ?? undefined;
let seasonData: TMDBSeasonMetaResult | undefined;
if (type === MWMediaType.SERIES) {
const seasons = (details as TMDBShowData).seasons;
let selectedSeason = seasons.find((v) => v.id.toString() === seasonId);
if (!selectedSeason) {
selectedSeason = seasons.find((v) => v.season_number === 1);
}
if (selectedSeason) {
const episodes = await getEpisodes(
details.id.toString(),
selectedSeason.season_number
);
seasonData = {
id: selectedSeason.id.toString(),
season_number: selectedSeason.season_number,
title: selectedSeason.name,
episodes,
};
}
}
const tmdbmeta = formatTMDBMetaResult(details, type);
if (!tmdbmeta) return null;
const meta = formatTMDBMeta(tmdbmeta, seasonData);
if (!meta) return null;
return {
meta,
imdbId,
tmdbId: id,
};
}
export async function getLegacyMetaFromId(
type: MWMediaType,
id: string,
seasonId?: string
): Promise<DetailedMeta | null> {
const queryType = mediaTypeToJW(type);
@ -82,3 +179,55 @@ export async function getMetaFromId(
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}`;
}
}

View file

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

View file

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

View 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,
};
}

View 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[];
};

View file

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

View 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[];
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ import {
MWStreamQuality,
MWStreamType,
} from "@/backend/helpers/streams";
import { MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { compareTitle } from "@/utils/titleMatch";
const nanoid = customAlphabet("0123456789abcdef", 32);
@ -142,7 +142,7 @@ const convertSubtitles = (subtitleGroup: any): MWCaption | null => {
registerProvider({
id: "superstream",
displayName: "Superstream",
rank: 200,
rank: 300,
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, episode, progress }) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,7 @@ interface Config {
APP_VERSION: string;
GITHUB_LINK: string;
DISCORD_LINK: string;
OMDB_API_KEY: string;
TMDB_API_KEY: string;
TMDB_READ_API_KEY: string;
CORS_PROXY_URL: string;
NORMAL_ROUTER: boolean;
}
@ -14,15 +13,13 @@ export interface RuntimeConfig {
APP_VERSION: string;
GITHUB_LINK: string;
DISCORD_LINK: string;
OMDB_API_KEY: string;
TMDB_API_KEY: string;
TMDB_READ_API_KEY: string;
NORMAL_ROUTER: boolean;
PROXY_URLS: string[];
}
const env: Record<keyof Config, undefined | string> = {
OMDB_API_KEY: import.meta.env.VITE_OMDB_API_KEY,
TMDB_API_KEY: import.meta.env.VITE_TMDB_API_KEY,
TMDB_READ_API_KEY: import.meta.env.VITE_TMDB_READ_API_KEY,
APP_VERSION: undefined,
GITHUB_LINK: undefined,
DISCORD_LINK: undefined,
@ -30,25 +27,28 @@ const env: Record<keyof Config, undefined | string> = {
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
};
const alerts = [] as string[];
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
function getKey(key: keyof Config, defaultString?: string): string {
function getKeyValue(key: keyof Config): string | undefined {
let windowValue = (window as any)?.__CONFIG__?.[`VITE_${key}`];
if (windowValue !== undefined && windowValue.length === 0)
windowValue = undefined;
const value = env[key] ?? windowValue ?? undefined;
if (value === undefined) {
if (defaultString) return defaultString;
if (!alerts.includes(key)) {
return env[key] ?? windowValue ?? undefined;
}
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}`);
alerts.push(key);
}
return "";
}
return value;
return val;
});
if (values.includes(undefined)) throw new Error("Misconfigured instance");
}
export function conf(): RuntimeConfig {
@ -56,8 +56,7 @@ export function conf(): RuntimeConfig {
APP_VERSION,
GITHUB_LINK,
DISCORD_LINK,
OMDB_API_KEY: getKey("OMDB_API_KEY"),
TMDB_API_KEY: getKey("TMDB_API_KEY"),
TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
PROXY_URLS: getKey("CORS_PROXY_URL")
.split(",")
.map((v) => v.trim()),

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -46,9 +46,14 @@ export async function initializeStores() {
let mostRecentData = data;
try {
for (const version of relevantVersions) {
if (version.migrate)
if (version.migrate) {
localStorage.setItem(
`BACKUP-v${version.version}-${internal.key}`,
JSON.stringify(mostRecentData)
);
mostRecentData = await version.migrate(mostRecentData);
}
}
} catch (err) {
console.error(`FAILED TO MIGRATE STORE ${internal.key}`, err);
// reset store to lastest version create

3
src/utils/typeguard.ts Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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