1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-29 16:07:40 +01:00

episode ids , shorter debounce and flixHQ provider

This commit is contained in:
Jelle van Snik 2023-01-22 19:26:08 +01:00
parent 5a01a68ce4
commit f472f04735
24 changed files with 337 additions and 82 deletions

View file

@ -6,6 +6,7 @@
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^1.0.0-beta.5", "@formkit/auto-animate": "^1.0.0-beta.5",
"@headlessui/react": "^1.5.0", "@headlessui/react": "^1.5.0",
"@types/react-helmet": "^6.1.6",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"fscreen": "^1.2.0", "fscreen": "^1.2.0",
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
@ -19,6 +20,7 @@
"ofetch": "^1.0.0", "ofetch": "^1.0.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-i18next": "^12.1.1", "react-i18next": "^12.1.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-stickynode": "^4.1.0", "react-stickynode": "^4.1.0",

View file

@ -21,7 +21,22 @@ export function mwFetch<T>(url: string, ops: P<T>[1]): R<T> {
} }
export function proxiedFetch<T>(url: string, ops: P<T>[1]): R<T> { export function proxiedFetch<T>(url: string, ops: P<T>[1]): R<T> {
const parsedUrl = new URL(url); let combinedUrl = ops?.baseURL ?? "";
if (
combinedUrl.length > 0 &&
combinedUrl.endsWith("/") &&
url.startsWith("/")
)
combinedUrl += url.slice(1);
else if (
combinedUrl.length > 0 &&
!combinedUrl.endsWith("/") &&
!url.startsWith("/")
)
combinedUrl += `/${url}`;
else combinedUrl += url;
const parsedUrl = new URL(combinedUrl);
Object.entries(ops?.params ?? {}).forEach(([k, v]) => { Object.entries(ops?.params ?? {}).forEach(([k, v]) => {
parsedUrl.searchParams.set(k, v); parsedUrl.searchParams.set(k, v);
}); });

View file

@ -20,8 +20,8 @@ type MWProviderTypeSpecific =
} }
| { | {
type: MWMediaType.SERIES; type: MWMediaType.SERIES;
episode: number; episode: string;
season: number; season: string;
}; };
export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase; export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase;

View file

@ -31,8 +31,8 @@ type MWProviderRunContextTypeSpecific =
} }
| { | {
type: MWMediaType.SERIES; type: MWMediaType.SERIES;
episode: number; episode: string;
season: number; season: string;
}; };
export type MWProviderRunContext = MWProviderRunContextBase & export type MWProviderRunContext = MWProviderRunContextBase &

View file

@ -2,6 +2,7 @@ import { initializeScraperStore } from "./helpers/register";
// providers // providers
import "./providers/gdriveplayer"; import "./providers/gdriveplayer";
import "./providers/flixhq";
// embeds // embeds
// -- nothing here yet // -- nothing here yet

View file

@ -3,6 +3,7 @@ import { makeUrl, mwFetch } from "../helpers/fetch";
import { import {
formatJWMeta, formatJWMeta,
JWMediaResult, JWMediaResult,
JWSeasonMetaResult,
JW_API_BASE, JW_API_BASE,
mediaTypeToJW, mediaTypeToJW,
} from "./justwatch"; } from "./justwatch";
@ -33,7 +34,8 @@ export interface DetailedMeta {
export async function getMetaFromId( export async function getMetaFromId(
type: MWMediaType, type: MWMediaType,
id: string id: string,
seasonId?: string
): Promise<DetailedMeta | null> { ): Promise<DetailedMeta | null> {
const queryType = mediaTypeToJW(type); const queryType = mediaTypeToJW(type);
@ -61,8 +63,17 @@ export async function getMetaFromId(
if (!imdbId || !tmdbId) throw new Error("not enough info"); if (!imdbId || !tmdbId) throw new Error("not enough info");
let seasonData: JWSeasonMetaResult | undefined;
if (data.object_type === "show") {
const seasonToScrape = seasonId ?? data.seasons?.[0].id.toString() ?? "";
const url = makeUrl("/content/titles/show_season/{id}/locale/en_US", {
id: seasonToScrape,
});
seasonData = await mwFetch<any>(url, { baseURL: JW_API_BASE });
}
return { return {
meta: formatJWMeta(data), meta: formatJWMeta(data, seasonData),
imdbId, imdbId,
tmdbId, tmdbId,
}; };

View file

@ -1,10 +1,22 @@
import { MWMediaType } from "./types"; import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types";
export const JW_API_BASE = "https://apis.justwatch.com"; export const JW_API_BASE = "https://apis.justwatch.com";
export const JW_IMAGE_BASE = "https://images.justwatch.com"; export const JW_IMAGE_BASE = "https://images.justwatch.com";
export type JWContentTypes = "movie" | "show"; 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 = { export type JWMediaResult = {
title: string; title: string;
poster?: string; poster?: string;
@ -12,6 +24,14 @@ export type JWMediaResult = {
original_release_year: number; original_release_year: number;
jw_entity_id: string; jw_entity_id: string;
object_type: JWContentTypes; 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 {
@ -26,8 +46,24 @@ export function JWMediaToMediaType(type: string): MWMediaType {
throw new Error("unsupported type"); throw new Error("unsupported type");
} }
export function formatJWMeta(media: JWMediaResult) { export function formatJWMeta(
media: JWMediaResult,
season?: JWSeasonMetaResult
): MWMediaMeta {
const type = JWMediaToMediaType(media.object_type); const type = JWMediaToMediaType(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 => ({
id: v.id.toString(),
number: v.season_number,
title: v.title,
})
);
}
return { return {
title: media.title, title: media.title,
id: media.id.toString(), id: media.id.toString(),
@ -36,5 +72,41 @@ export function formatJWMeta(media: JWMediaResult) {
? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}` ? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}`
: undefined, : undefined,
type, 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 JWMediaToId(media: MWMediaMeta): string {
return ["JW", mediaTypeToJW(media.type), media.id].join("-");
}
export function decodeJWId(
paramId: string
): { id: string; type: MWMediaType } | null {
const [prefix, type, id] = paramId.split("-", 3);
if (prefix !== "JW") return null;
let mediaType;
try {
mediaType = JWMediaToMediaType(type);
} catch {
return null;
}
return {
type: mediaType,
id,
}; };
} }

View file

@ -4,14 +4,43 @@ export enum MWMediaType {
ANIME = "anime", ANIME = "anime",
} }
export type MWMediaMeta = { export type MWSeasonMeta = {
id: string;
number: number;
title: string;
};
export type MWSeasonWithEpisodeMeta = {
id: string;
number: number;
title: string;
episodes: {
id: string;
number: number;
title: string;
}[];
};
type MWMediaMetaBase = {
title: string; title: string;
id: string; id: string;
year: string; year: string;
poster?: string; poster?: string;
type: MWMediaType;
}; };
type MWMediaMetaSpecific =
| {
type: MWMediaType.MOVIE | MWMediaType.ANIME;
seasons: undefined;
}
| {
type: MWMediaType.SERIES;
seasons: MWSeasonMeta[];
seasonData: MWSeasonWithEpisodeMeta;
};
export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific;
export interface MWQuery { export interface MWQuery {
searchQuery: string; searchQuery: string;
type: MWMediaType; type: MWMediaType;

View file

@ -1,37 +1,63 @@
import { proxiedFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register"; import { registerProvider } from "../helpers/register";
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types"; import { MWMediaType } from "../metadata/types";
const timeout = (time: number) => const flixHqBase = "https://api.consumet.org/movies/flixhq";
new Promise<void>((resolve) => {
setTimeout(() => resolve(), time);
});
registerProvider({ registerProvider({
id: "testprov", id: "flixhq",
rank: 42, displayName: "FlixHQ",
rank: 100,
type: [MWMediaType.MOVIE], type: [MWMediaType.MOVIE],
disabled: true,
async scrape({ progress }) { async scrape({ media, progress }) {
await timeout(1000); // search for relevant item
const searchResults = await proxiedFetch<any>(
`/${encodeURIComponent(media.meta.title)}`,
{
baseURL: flixHqBase,
}
);
// TODO fuzzy match or normalize title before comparison
const foundItem = searchResults.results.find((v: any) => {
return v.title === media.meta.title && v.releaseDate === media.meta.year;
});
if (!foundItem) throw new Error("No watchable item found");
const flixId = foundItem.id;
// get media info
progress(25); progress(25);
await timeout(1000); const mediaInfo = await proxiedFetch<any>("/info", {
progress(50); baseURL: flixHqBase,
await timeout(1000); params: {
id: flixId,
},
});
// get stream info from media
progress(75); progress(75);
await timeout(1000); const watchInfo = await proxiedFetch<any>("/watch", {
baseURL: flixHqBase,
params: {
episodeId: mediaInfo.episodes[0].id,
mediaId: flixId,
},
});
// get best quality source
const source = watchInfo.sources.reduce((p: any, c: any) =>
c.quality > p.quality ? c : p
);
return { return {
embeds: [ embeds: [],
// { stream: {
// type: MWEmbedType.OPENLOAD, streamUrl: source.url,
// url: "https://google.com", quality: MWStreamQuality.QUNKNOWN,
// }, type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
// { captions: [],
// type: MWEmbedType.ANOTHER, },
// url: "https://google.com",
// },
],
}; };
}, },
}); });

View file

@ -22,7 +22,7 @@ export function EditButton(props: EditButtonProps) {
> >
<span ref={parent}> <span ref={parent}>
{props.editing ? ( {props.editing ? (
<span className="mx-4">Stop editing</span> <span className="mx-4 whitespace-nowrap">Stop editing</span>
) : ( ) : (
<Icon icon={Icons.EDIT} /> <Icon icon={Icons.EDIT} />
)} )}

View file

@ -1,7 +1,7 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { DotList } from "@/components/text/DotList"; import { DotList } from "@/components/text/DotList";
import { MWMediaMeta } from "@/backend/metadata/types"; import { MWMediaMeta } from "@/backend/metadata/types";
import { mediaTypeToJW } from "@/backend/metadata/justwatch"; import { JWMediaToId } from "@/backend/metadata/justwatch";
import { Icons } from "../Icon"; import { Icons } from "../Icon";
import { IconPatch } from "../buttons/IconPatch"; import { IconPatch } from "../buttons/IconPatch";
@ -107,9 +107,7 @@ export function MediaCard(props: MediaCardProps) {
const canLink = props.linkable && !props.closable; const canLink = props.linkable && !props.closable;
const link = canLink const link = canLink
? `/media/${encodeURIComponent( ? `/media/${encodeURIComponent(JWMediaToId(props.media))}`
mediaTypeToJW(props.media.type)
)}-${encodeURIComponent(props.media.id)}`
: "#"; : "#";
if (!props.linkable) return <span>{content}</span>; if (!props.linkable) return <span>{content}</span>;

View file

@ -1,26 +1,46 @@
import { useEffect } from "react"; import { useEffect, useRef } from "react";
import { useVideoPlayerState } from "../VideoContext"; import { useVideoPlayerState } from "../VideoContext";
interface ShowControlProps { interface ShowControlProps {
series?: { series?: {
episode: number; episodeId: string;
season: number; seasonId: string;
}; };
title?: string; onSelect?: (state: { episodeId?: string; seasonId?: string }) => void;
} }
export function ShowControl(props: ShowControlProps) { export function ShowControl(props: ShowControlProps) {
const { videoState } = useVideoPlayerState(); const { videoState } = useVideoPlayerState();
const lastState = useRef<{
episodeId?: string;
seasonId?: string;
} | null>({
episodeId: props.series?.episodeId,
seasonId: props.series?.seasonId,
});
useEffect(() => { useEffect(() => {
videoState.setShowData({ videoState.setShowData({
current: props.series, current: props.series,
isSeries: !!props.series, isSeries: !!props.series,
title: props.title,
}); });
// we only want it to run when props change, not when videoState changes // we only want it to run when props change, not when videoState changes
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props]); }, [props]);
useEffect(() => {
const currentState = {
episodeId: videoState.seasonData.current?.episodeId,
seasonId: videoState.seasonData.current?.seasonId,
};
if (
currentState.episodeId !== lastState.current?.episodeId ||
currentState.seasonId !== lastState.current?.seasonId
) {
lastState.current = currentState;
props.onSelect?.(currentState);
}
}, [videoState, props]);
return null; return null;
} }

View file

@ -63,7 +63,7 @@ export function VolumeControl(props: Props) {
</div> </div>
<div <div
className={`linear -ml-2 w-0 overflow-hidden transition-[width,opacity] duration-300 ${ className={`linear -ml-2 w-0 overflow-hidden transition-[width,opacity] duration-300 ${
hoveredOnce ? "!w-24 opacity-100" : "w-4 opacity-0" hoveredOnce || dragging ? "!w-24 opacity-100" : "w-4 opacity-0"
}`} }`}
> >
<div <div

View file

@ -13,11 +13,10 @@ import { getStoredVolume, setStoredVolume } from "./volumeStore";
interface ShowData { interface ShowData {
current?: { current?: {
episode: number; episodeId: string;
season: number; seasonId: string;
}; };
isSeries: boolean; isSeries: boolean;
title?: string;
} }
export interface PlayerControls { export interface PlayerControls {

View file

@ -26,10 +26,9 @@ export type PlayerState = {
seasonData: { seasonData: {
isSeries: boolean; isSeries: boolean;
current?: { current?: {
episode: number; episodeId: string;
season: number; seasonId: string;
}; };
title?: string;
}; };
error: null | { error: null | {
name: string; name: string;

View file

@ -15,8 +15,8 @@ export interface ScrapeEventLog {
export type SelectedMediaData = export type SelectedMediaData =
| { | {
type: MWMediaType.SERIES; type: MWMediaType.SERIES;
episode: number; episode: string;
season: number; season: string;
} }
| { | {
type: MWMediaType.MOVIE | MWMediaType.ANIME; type: MWMediaType.MOVIE | MWMediaType.ANIME;

View file

@ -42,7 +42,6 @@ if (key) {
// TODO general todos: // TODO general todos:
// - localize everything // - localize everything
// - add titles to pages
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>

View file

@ -16,6 +16,11 @@ function App() {
<Redirect to={`/search/${MWMediaType.MOVIE}`} /> <Redirect to={`/search/${MWMediaType.MOVIE}`} />
</Route> </Route>
<Route exact path="/media/:media" component={MediaView} /> <Route exact path="/media/:media" component={MediaView} />
<Route
exact
path="/media/:media/:season/:episode"
component={MediaView}
/>
<Route exact path="/search/:type/:query?" component={SearchView} /> <Route exact path="/search/:type/:query?" component={SearchView} />
<Route path="*" component={NotFoundPage} /> <Route path="*" component={NotFoundPage} />
</Switch> </Switch>

View file

@ -4,12 +4,16 @@ import { Link } from "@/components/text/Link";
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
import { useGoBack } from "@/hooks/useGoBack"; import { useGoBack } from "@/hooks/useGoBack";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { Helmet } from "react-helmet";
export function MediaFetchErrorView() { export function MediaFetchErrorView() {
const goBack = useGoBack(); const goBack = useGoBack();
return ( return (
<div className="h-screen flex-1"> <div className="h-screen flex-1">
<Helmet>
<title>Failed to load meta</title>
</Helmet>
<div className="fixed inset-x-0 top-0 py-6 px-8"> <div className="fixed inset-x-0 top-0 py-6 px-8">
<VideoPlayerHeader onClick={goBack} /> <VideoPlayerHeader onClick={goBack} />
</div> </div>

View file

@ -1,11 +1,12 @@
import { useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { MWStream } from "@/backend/helpers/streams"; import { MWStream } from "@/backend/helpers/streams";
import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
import { JWMediaToMediaType } from "@/backend/metadata/justwatch"; import { decodeJWId } from "@/backend/metadata/justwatch";
import { SourceControl } from "@/components/video/controls/SourceControl"; import { SourceControl } from "@/components/video/controls/SourceControl";
import { Loading } from "@/components/layout/Loading"; import { Loading } from "@/components/layout/Loading";
import { useLoading } from "@/hooks/useLoading"; import { useLoading } from "@/hooks/useLoading";
@ -23,6 +24,9 @@ import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
function MediaViewLoading(props: { onGoBack(): void }) { function MediaViewLoading(props: { onGoBack(): void }) {
return ( return (
<div className="relative flex h-screen items-center justify-center"> <div className="relative flex h-screen items-center justify-center">
<Helmet>
<title>Loading...</title>
</Helmet>
<div className="absolute inset-x-0 top-0 p-6"> <div className="absolute inset-x-0 top-0 p-6">
<VideoPlayerHeader onClick={props.onGoBack} /> <VideoPlayerHeader onClick={props.onGoBack} />
</div> </div>
@ -51,6 +55,9 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
return ( return (
<div className="relative flex h-screen items-center justify-center"> <div className="relative flex h-screen items-center justify-center">
<Helmet>
<title>{props.meta.meta.title}</title>
</Helmet>
<div className="absolute inset-x-0 top-0 py-6 px-8"> <div className="absolute inset-x-0 top-0 py-6 px-8">
<VideoPlayerHeader onClick={props.onGoBack} media={props.meta.meta} /> <VideoPlayerHeader onClick={props.onGoBack} media={props.meta.meta} />
</div> </div>
@ -85,6 +92,7 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
interface MediaViewPlayerProps { interface MediaViewPlayerProps {
meta: DetailedMeta; meta: DetailedMeta;
stream: MWStream; stream: MWStream;
selected: SelectedMediaData;
} }
export function MediaViewPlayer(props: MediaViewPlayerProps) { export function MediaViewPlayer(props: MediaViewPlayerProps) {
const goBack = useGoBack(); const goBack = useGoBack();
@ -96,8 +104,13 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.stream]); }, [props.stream]);
// TODO show episode title
return ( return (
<div className="h-screen w-screen"> <div className="h-screen w-screen">
<Helmet>
<title>{props.meta.meta.title}</title>
</Helmet>
<DecoratedVideoPlayer media={props.meta.meta} onGoBack={goBack} autoPlay> <DecoratedVideoPlayer media={props.meta.meta} onGoBack={goBack} autoPlay>
<SourceControl <SourceControl
source={props.stream.streamUrl} source={props.stream.streamUrl}
@ -107,44 +120,71 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
startAt={firstStartTime.current} startAt={firstStartTime.current}
onProgress={updateProgress} onProgress={updateProgress}
/> />
<ShowControl series={{ episode: 5, season: 2 }} title="hello world" /> {props.selected.type === MWMediaType.SERIES ? (
<ShowControl
series={{
seasonId: props.selected.season,
episodeId: props.selected.episode,
}}
onSelect={(d) => console.log("selected stuff", d)}
/>
) : null}
</DecoratedVideoPlayer> </DecoratedVideoPlayer>
</div> </div>
); );
} }
export function MediaView() { export function MediaView() {
const params = useParams<{ media: string }>(); const params = useParams<{
media: string;
episode?: string;
season?: string;
}>();
const goBack = useGoBack(); const goBack = useGoBack();
const history = useHistory();
const [meta, setMeta] = useState<DetailedMeta | null>(null); const [meta, setMeta] = useState<DetailedMeta | null>(null);
const [selected, setSelected] = useState<SelectedMediaData | null>(null); const [selected, setSelected] = useState<SelectedMediaData | null>(null);
const [exec, loading, error] = useLoading(async (mediaParams: string) => { const [exec, loading, error] = useLoading(
let type: MWMediaType; async (mediaParams: string, seasonId?: string) => {
let id = ""; const data = decodeJWId(mediaParams);
try { if (!data) return null;
const [t, i] = mediaParams.split("-", 2); return getMetaFromId(data.type, data.id, seasonId);
type = JWMediaToMediaType(t);
id = i;
} catch (err) {
return null;
} }
return getMetaFromId(type, id); );
});
const [stream, setStream] = useState<MWStream | null>(null); const [stream, setStream] = useState<MWStream | null>(null);
useEffect(() => { useEffect(() => {
exec(params.media).then((v) => { console.log("I am being ran");
exec(params.media, params.season).then((v) => {
setMeta(v ?? null); setMeta(v ?? null);
if (v) if (v) {
if (v.meta.type !== MWMediaType.SERIES) {
setSelected({ setSelected({
type: v.meta.type, type: v.meta.type,
episode: 0 as any, season: undefined,
season: 0 as any, episode: undefined,
}); });
else setSelected(null); } else {
const season = params.season ?? v.meta.seasonData.id;
const episode = params.episode ?? v.meta.seasonData.episodes[0].id;
setSelected({
type: MWMediaType.SERIES,
season,
episode,
}); });
}, [exec, params.media]); if (season !== params.season || episode !== params.episode)
history.replace(
`/media/${encodeURIComponent(params.media)}/${encodeURIComponent(
season
)}/${encodeURIComponent(episode)}`
);
}
} else setSelected(null);
});
// dont rerender when params changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exec, history]);
if (loading) return <MediaViewLoading onGoBack={goBack} />; if (loading) return <MediaViewLoading onGoBack={goBack} />;
if (error) return <MediaFetchErrorView />; if (error) return <MediaFetchErrorView />;
@ -167,5 +207,5 @@ export function MediaView() {
); );
// show stream once we have a stream // show stream once we have a stream
return <MediaViewPlayer meta={meta} stream={stream} />; return <MediaViewPlayer meta={meta} stream={stream} selected={selected} />;
} }

View file

@ -7,6 +7,7 @@ import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title"; import { Title } from "@/components/text/Title";
import { useGoBack } from "@/hooks/useGoBack"; import { useGoBack } from "@/hooks/useGoBack";
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
import { Helmet } from "react-helmet";
export function NotFoundWrapper(props: { export function NotFoundWrapper(props: {
children?: ReactNode; children?: ReactNode;
@ -16,6 +17,9 @@ export function NotFoundWrapper(props: {
return ( return (
<div className="h-screen flex-1"> <div className="h-screen flex-1">
<Helmet>
<title>Not found</title>
</Helmet>
{props.video ? ( {props.video ? (
<div className="fixed inset-x-0 top-0 py-6 px-8"> <div className="fixed inset-x-0 top-0 py-6 px-8">
<VideoPlayerHeader onClick={goBack} /> <VideoPlayerHeader onClick={goBack} />

View file

@ -13,7 +13,7 @@ export function SearchResultsPartial({ search }: SearchResultsPartialProps) {
const [searching, setSearching] = useState<boolean>(false); const [searching, setSearching] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const debouncedSearch = useDebounce<MWQuery>(search, 2000); const debouncedSearch = useDebounce<MWQuery>(search, 500);
useEffect(() => { useEffect(() => {
setSearching(search.searchQuery !== ""); setSearching(search.searchQuery !== "");
setLoading(search.searchQuery !== ""); setLoading(search.searchQuery !== "");

View file

@ -7,6 +7,7 @@ import { SearchBarInput } from "@/components/SearchBar";
import { Title } from "@/components/text/Title"; import { Title } from "@/components/text/Title";
import { useSearchQuery } from "@/hooks/useSearchQuery"; import { useSearchQuery } from "@/hooks/useSearchQuery";
import { WideContainer } from "@/components/layout/WideContainer"; import { WideContainer } from "@/components/layout/WideContainer";
import { Helmet } from "react-helmet";
import { SearchResultsPartial } from "./SearchResultsPartial"; import { SearchResultsPartial } from "./SearchResultsPartial";
export function SearchView() { export function SearchView() {
@ -22,6 +23,9 @@ export function SearchView() {
return ( return (
<> <>
<div className="relative z-10 mb-24"> <div className="relative z-10 mb-24">
<Helmet>
<title>movie-web</title>
</Helmet>
<Navigation bg={showBg} /> <Navigation bg={showBg} />
<ThinContainer> <ThinContainer>
<div className="mt-44 space-y-16 text-center"> <div className="mt-44 space-y-16 text-center">

View file

@ -321,6 +321,13 @@
dependencies: dependencies:
"@types/react" "^17" "@types/react" "^17"
"@types/react-helmet@^6.1.6":
"integrity" "sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A=="
"resolved" "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.6.tgz"
"version" "6.1.6"
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.3.3": "@types/react-router-dom@^5.3.3":
"integrity" "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==" "integrity" "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw=="
"resolved" "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz" "resolved" "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz"
@ -2881,7 +2888,7 @@
dependencies: dependencies:
"read" "1" "read" "1"
"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.8.1": "prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1":
"integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==" "integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="
"resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" "resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
"version" "15.8.1" "version" "15.8.1"
@ -2924,6 +2931,21 @@
"object-assign" "^4.1.1" "object-assign" "^4.1.1"
"scheduler" "^0.20.2" "scheduler" "^0.20.2"
"react-fast-compare@^3.1.1":
"integrity" "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
"resolved" "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz"
"version" "3.2.0"
"react-helmet@^6.1.0":
"integrity" "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw=="
"resolved" "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz"
"version" "6.1.0"
dependencies:
"object-assign" "^4.1.1"
"prop-types" "^15.7.2"
"react-fast-compare" "^3.1.1"
"react-side-effect" "^2.1.0"
"react-i18next@^12.1.1": "react-i18next@^12.1.1":
"integrity" "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==" "integrity" "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA=="
"resolved" "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz" "resolved" "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz"
@ -2965,6 +2987,11 @@
"tiny-invariant" "^1.0.2" "tiny-invariant" "^1.0.2"
"tiny-warning" "^1.0.0" "tiny-warning" "^1.0.0"
"react-side-effect@^2.1.0":
"integrity" "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw=="
"resolved" "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz"
"version" "2.1.2"
"react-stickynode@^4.1.0": "react-stickynode@^4.1.0":
"integrity" "sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==" "integrity" "sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ=="
"resolved" "https://registry.npmjs.org/react-stickynode/-/react-stickynode-4.1.0.tgz" "resolved" "https://registry.npmjs.org/react-stickynode/-/react-stickynode-4.1.0.tgz"
@ -2986,7 +3013,7 @@
"loose-envify" "^1.4.0" "loose-envify" "^1.4.0"
"prop-types" "^15.6.2" "prop-types" "^15.6.2"
"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.6.0", "react@17.0.2": "react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.3.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.3.0", "react@>=16.6.0", "react@17.0.2":
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
"version" "17.0.2" "version" "17.0.2"