Merge branch 'v4' into thumbnails

This commit is contained in:
mrjvs 2023-07-23 11:50:59 +02:00 committed by GitHub
commit d29436e816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 169 additions and 78 deletions

View File

@ -32,6 +32,7 @@
"react-stickynode": "^4.1.0", "react-stickynode": "^4.1.0",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"slugify": "^1.6.6",
"subsrt-ts": "^2.1.1", "subsrt-ts": "^2.1.1",
"unpacker": "^1.0.1" "unpacker": "^1.0.1"
}, },

View File

@ -2,6 +2,7 @@ import { FetchError } from "ofetch";
import { formatJWMeta, mediaTypeToJW } from "./justwatch"; import { formatJWMeta, mediaTypeToJW } from "./justwatch";
import { import {
TMDBIdToUrlId,
TMDBMediaToMediaType, TMDBMediaToMediaType,
formatTMDBMeta, formatTMDBMeta,
getEpisodes, getEpisodes,
@ -12,7 +13,7 @@ import {
mediaTypeToTMDB, mediaTypeToTMDB,
} from "./tmdb"; } from "./tmdb";
import { import {
JWMediaResult, JWDetailedMeta,
JWSeasonMetaResult, JWSeasonMetaResult,
JW_API_BASE, JW_API_BASE,
} from "./types/justwatch"; } from "./types/justwatch";
@ -25,23 +26,6 @@ import {
} from "./types/tmdb"; } from "./types/tmdb";
import { makeUrl, proxiedFetch } from "../helpers/fetch"; import { makeUrl, proxiedFetch } from "../helpers/fetch";
type JWExternalIdType =
| "eidr"
| "imdb_latest"
| "imdb"
| "tmdb_latest"
| "tmdb"
| "tms";
interface JWExternalId {
provider: JWExternalIdType;
external_id: string;
}
interface JWDetailedMeta extends JWMediaResult {
external_ids: JWExternalId[];
}
export interface DetailedMeta { export interface DetailedMeta {
meta: MWMediaMeta; meta: MWMediaMeta;
imdbId?: string; imdbId?: string;
@ -180,27 +164,6 @@ export async function getLegacyMetaFromId(
}; };
} }
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 { export function isLegacyUrl(url: string): boolean {
if (url.startsWith("/media/JW")) return true; if (url.startsWith("/media/JW")) return true;
return false; return false;
@ -224,10 +187,12 @@ export async function convertLegacyUrl(
// movies always have an imdb id on tmdb // movies always have an imdb id on tmdb
if (imdbId && mediaType === MWMediaType.MOVIE) { if (imdbId && mediaType === MWMediaType.MOVIE) {
const movieId = await getMovieFromExternalId(imdbId); const movieId = await getMovieFromExternalId(imdbId);
if (movieId) return `/media/tmdb-movie-${movieId}`; if (movieId) {
return `/media/${TMDBIdToUrlId(mediaType, movieId, meta.meta.title)}`;
} }
if (tmdbId) { if (tmdbId) {
return `/media/tmdb-${type}-${tmdbId}`; return `/media/${TMDBIdToUrlId(mediaType, tmdbId, meta.meta.title)}`;
}
} }
} }

View File

@ -1,3 +1,5 @@
import slugify from "slugify";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw"; import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
@ -11,12 +13,15 @@ import {
TMDBMovieExternalIds, TMDBMovieExternalIds,
TMDBMovieResponse, TMDBMovieResponse,
TMDBMovieResult, TMDBMovieResult,
TMDBMovieSearchResult,
TMDBSearchResult,
TMDBSeason, TMDBSeason,
TMDBSeasonMetaResult, TMDBSeasonMetaResult,
TMDBShowData, TMDBShowData,
TMDBShowExternalIds, TMDBShowExternalIds,
TMDBShowResponse, TMDBShowResponse,
TMDBShowResult, TMDBShowResult,
TMDBShowSearchResult,
} from "./types/tmdb"; } from "./types/tmdb";
import { mwFetch } from "../helpers/fetch"; import { mwFetch } from "../helpers/fetch";
@ -74,8 +79,21 @@ export function formatTMDBMeta(
}; };
} }
export function TMDBIdToUrlId(
type: MWMediaType,
tmdbId: string,
title: string
) {
return [
"tmdb",
mediaTypeToTMDB(type),
tmdbId,
slugify(title, { lower: true, strict: true }),
].join("-");
}
export function TMDBMediaToId(media: MWMediaMeta): string { export function TMDBMediaToId(media: MWMediaMeta): string {
return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-"); return TMDBIdToUrlId(media.type, media.id, media.title);
} }
export function decodeTMDBId( export function decodeTMDBId(
@ -143,6 +161,38 @@ export async function searchMedia(
return data; return data;
} }
export async function multiSearch(
query: string
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
const data = await get<TMDBSearchResult>(`search/multi`, {
query,
include_adult: false,
language: "en-US",
page: 1,
});
// filter out results that aren't movies or shows
const results = data.results.filter(
(r) => r.media_type === "movie" || r.media_type === "tv"
);
return results;
}
export async function generateQuickSearchMediaUrl(
query: string
): Promise<string | undefined> {
const data = await multiSearch(query);
if (data.length === 0) return undefined;
const result = data[0];
const type = result.media_type === "movie" ? "movie" : "show";
const title = result.media_type === "movie" ? result.title : result.name;
return `/media/${TMDBIdToUrlId(
TMDBMediaToMediaType(type),
result.id.toString(),
title
)}`;
}
// Conditional type which for inferring the return type based on the content type // Conditional type which for inferring the return type based on the content type
type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie" type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
? TMDBMovieData ? TMDBMovieData

View File

@ -46,3 +46,20 @@ export type JWSeasonMetaResult = {
season_number: number; season_number: number;
episodes: JWEpisodeShort[]; episodes: JWEpisodeShort[];
}; };
export type JWExternalIdType =
| "eidr"
| "imdb_latest"
| "imdb"
| "tmdb_latest"
| "tmdb"
| "tms";
export interface JWExternalId {
provider: JWExternalIdType;
external_id: string;
}
export interface JWDetailedMeta extends JWMediaResult {
external_ids: JWExternalId[];
}

View File

@ -306,3 +306,46 @@ export interface ExternalIdMovieSearchResult {
tv_episode_results: any[]; tv_episode_results: any[];
tv_season_results: any[]; tv_season_results: any[];
} }
export interface TMDBMovieSearchResult {
adult: boolean;
backdrop_path: string;
id: number;
title: string;
original_language: string;
original_title: string;
overview: string;
poster_path: string;
media_type: "movie";
genre_ids: number[];
popularity: number;
release_date: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface TMDBShowSearchResult {
adult: boolean;
backdrop_path: string;
id: number;
name: string;
original_language: string;
original_name: string;
overview: string;
poster_path: string;
media_type: "tv";
genre_ids: number[];
popularity: number;
first_air_date: string;
vote_average: number;
vote_count: number;
origin_country: string[];
}
export interface TMDBSearchResult {
page: number;
results: (TMDBMovieSearchResult | TMDBShowSearchResult)[];
total_pages: number;
total_results: number;
}

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TMDBMediaToId } from "@/backend/metadata/getmeta"; import { TMDBMediaToId } from "@/backend/metadata/tmdb";
import { MWMediaMeta } from "@/backend/metadata/types/mw"; import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { DotList } from "@/components/text/DotList"; import { DotList } from "@/components/text/DotList";

View File

@ -5,9 +5,11 @@ import {
Switch, Switch,
useHistory, useHistory,
useLocation, useLocation,
useParams,
} from "react-router-dom"; } from "react-router-dom";
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta"; import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
import { MWMediaType } from "@/backend/metadata/types/mw"; 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";
@ -35,6 +37,23 @@ function LegacyUrlView({ children }: { children: ReactElement }) {
return children; return children;
} }
function QuickSearch() {
const { query } = useParams<{ query: string }>();
const { replace } = useHistory();
useEffect(() => {
if (query) {
generateQuickSearchMediaUrl(query).then((url) => {
replace(url ?? "/");
});
} else {
replace("/");
}
}, [query, replace]);
return null;
}
function App() { function App() {
return ( return (
<SettingsProvider> <SettingsProvider>
@ -48,6 +67,9 @@ function App() {
<Route exact path="/"> <Route exact path="/">
<Redirect to={`/search/${MWMediaType.MOVIE}`} /> <Redirect to={`/search/${MWMediaType.MOVIE}`} />
</Route> </Route>
<Route exact path="/s/:query">
<QuickSearch />
</Route>
{/* pages */} {/* pages */}
<Route exact path="/media/:media"> <Route exact path="/media/:media">

View File

@ -1,29 +1,21 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { MWCaption } from "@/backend/helpers/streams";
import { DetailedMeta } from "@/backend/metadata/getmeta";
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import {
VideoMediaPlayingEvent,
useMediaPlaying,
} from "@/video/state/logic/mediaplaying";
import { useMeta } from "@/video/state/logic/meta"; import { useMeta } from "@/video/state/logic/meta";
import { useProgress } from "@/video/state/logic/progress"; import { VideoProgressEvent, useProgress } from "@/video/state/logic/progress";
import { VideoPlayerMeta } from "@/video/state/types";
export type WindowMeta = { export type WindowMeta = {
meta: DetailedMeta; media: VideoPlayerMeta;
captions: MWCaption[]; state: {
episode?: { mediaPlaying: VideoMediaPlayingEvent;
episodeId: string; progress: VideoProgressEvent;
seasonId: string;
}; };
seasons?: {
id: string;
number: number;
title: string;
episodes?: { id: string; number: number; title: string }[];
}[];
progress: {
time: number;
duration: number;
}; };
} | null;
declare global { declare global {
interface Window { interface Window {
@ -35,18 +27,16 @@ export function MetaAction() {
const descriptor = useVideoPlayerDescriptor(); const descriptor = useVideoPlayerDescriptor();
const meta = useMeta(descriptor); const meta = useMeta(descriptor);
const progress = useProgress(descriptor); const progress = useProgress(descriptor);
const mediaPlaying = useMediaPlaying(descriptor);
useEffect(() => { useEffect(() => {
if (!window.meta) window.meta = {}; if (!window.meta) window.meta = {};
if (meta) { if (meta) {
window.meta[descriptor] = { window.meta[descriptor] = {
meta: meta.meta, media: meta,
captions: meta.captions, state: {
seasons: meta.seasons, mediaPlaying,
episode: meta.episode, progress,
progress: {
time: progress.time,
duration: progress.duration,
}, },
}; };
} }
@ -54,7 +44,7 @@ export function MetaAction() {
return () => { return () => {
if (window.meta) delete window.meta[descriptor]; if (window.meta) delete window.meta[descriptor];
}; };
}, [meta, descriptor, progress]); }, [meta, descriptor, mediaPlaying, progress]);
return null; return null;
} }

View File

@ -2,7 +2,8 @@ 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 { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta"; import { getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeTMDBId } from "@/backend/metadata/tmdb";
import { import {
MWMediaType, MWMediaType,
MWSeasonWithEpisodeMeta, MWSeasonWithEpisodeMeta,

View File

@ -4,11 +4,8 @@ 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 { import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
DetailedMeta, import { decodeTMDBId } from "@/backend/metadata/tmdb";
decodeTMDBId,
getMetaFromId,
} from "@/backend/metadata/getmeta";
import { import {
MWMediaType, MWMediaType,
MWSeasonWithEpisodeMeta, MWSeasonWithEpisodeMeta,

View File

@ -4764,6 +4764,11 @@ slice-ansi@^5.0.0:
ansi-styles "^6.0.0" ansi-styles "^6.0.0"
is-fullwidth-code-point "^4.0.0" is-fullwidth-code-point "^4.0.0"
slugify@^1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
source-map-js@^1.0.2: source-map-js@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"