270 lines
8.5 KiB
TypeScript
270 lines
8.5 KiB
TypeScript
import { useHistory, useParams } from "react-router-dom";
|
|
import { Helmet } from "react-helmet";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { MWStream } from "@/backend/helpers/streams";
|
|
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
|
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
|
import { decodeJWId } from "@/backend/metadata/justwatch";
|
|
import { Loading } from "@/components/layout/Loading";
|
|
import { useLoading } from "@/hooks/useLoading";
|
|
import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
|
|
import { useGoBack } from "@/hooks/useGoBack";
|
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
|
import { VideoPlayer } from "@/video/components/VideoPlayer";
|
|
import { MetaController } from "@/video/components/controllers/MetaController";
|
|
import { SourceController } from "@/video/components/controllers/SourceController";
|
|
import { Icons } from "@/components/Icon";
|
|
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
|
|
import { ProgressListenerController } from "@/video/components/controllers/ProgressListenerController";
|
|
import { VideoPlayerMeta } from "@/video/state/types";
|
|
import { SeriesController } from "@/video/components/controllers/SeriesController";
|
|
import { useWatchedItem } from "@/state/watched";
|
|
import { useTranslation } from "react-i18next";
|
|
import { MediaFetchErrorView } from "./MediaErrorView";
|
|
import { MediaScrapeLog } from "./MediaScrapeLog";
|
|
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
|
|
|
function MediaViewLoading(props: { onGoBack(): void }) {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<div className="relative flex flex-1 items-center justify-center">
|
|
<Helmet>
|
|
<title>{t("videoPlayer.loading")}</title>
|
|
</Helmet>
|
|
<div className="absolute inset-x-0 top-0 py-6 px-8">
|
|
<VideoPlayerHeader onClick={props.onGoBack} />
|
|
</div>
|
|
<div className="flex flex-col items-center">
|
|
<Loading className="mb-4" />
|
|
<p className="mb-8 text-denim-700">
|
|
{t("videoPlayer.findingBestVideo")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface MediaViewScrapingProps {
|
|
onStream(stream: MWStream): void;
|
|
onGoBack(): void;
|
|
meta: DetailedMeta;
|
|
selected: SelectedMediaData;
|
|
}
|
|
function MediaViewScraping(props: MediaViewScrapingProps) {
|
|
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
|
|
const { t } = useTranslation();
|
|
|
|
useEffect(() => {
|
|
if (stream) {
|
|
props.onStream(stream);
|
|
}
|
|
}, [stream, props]);
|
|
|
|
return (
|
|
<div className="relative flex flex-1 items-center justify-center">
|
|
<Helmet>
|
|
<title>{props.meta.meta.title}</title>
|
|
</Helmet>
|
|
<div className="absolute inset-x-0 top-0 py-6 px-8">
|
|
<VideoPlayerHeader onClick={props.onGoBack} media={props.meta.meta} />
|
|
</div>
|
|
<div className="flex flex-col items-center transition-opacity duration-200">
|
|
{pending ? (
|
|
<>
|
|
<Loading />
|
|
<p className="mb-8 text-denim-700">
|
|
{t("videoPlayer.findingBestVideo")}
|
|
</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
|
|
<p className="mb-8 text-denim-700">{t("videoPlayer.noVideos")}</p>
|
|
</>
|
|
)}
|
|
<div
|
|
className={`flex flex-col items-center transition-opacity duration-200 ${
|
|
pending ? "opacity-100" : "opacity-0"
|
|
}`}
|
|
>
|
|
<MediaScrapeLog events={eventLog} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface MediaViewPlayerProps {
|
|
meta: DetailedMeta;
|
|
stream: MWStream;
|
|
selected: SelectedMediaData;
|
|
onChangeStream: (sId: string, eId: string) => void;
|
|
}
|
|
export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
|
const goBack = useGoBack();
|
|
const { updateProgress, watchedItem } = useWatchedItem(
|
|
props.meta,
|
|
props.selected.episode
|
|
);
|
|
const firstStartTime = useRef(watchedItem?.progress);
|
|
useEffect(() => {
|
|
firstStartTime.current = watchedItem?.progress;
|
|
// only want it to change when stream changes
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [props.stream]);
|
|
|
|
const metaProps: VideoPlayerMeta = {
|
|
meta: props.meta,
|
|
captions: [],
|
|
};
|
|
let metaSeasonData: MWSeasonWithEpisodeMeta | undefined;
|
|
if (
|
|
props.selected.type === MWMediaType.SERIES &&
|
|
props.meta.meta.type === MWMediaType.SERIES
|
|
) {
|
|
metaProps.episode = {
|
|
seasonId: props.selected.season,
|
|
episodeId: props.selected.episode,
|
|
};
|
|
metaProps.seasons = props.meta.meta.seasons;
|
|
metaSeasonData = props.meta.meta.seasonData;
|
|
}
|
|
|
|
return (
|
|
<div className="fixed top-0 left-0 h-full w-screen">
|
|
<Helmet>
|
|
<html data-full="true" />
|
|
</Helmet>
|
|
<VideoPlayer includeSafeArea autoPlay onGoBack={goBack}>
|
|
<MetaController
|
|
data={metaProps}
|
|
seasonData={metaSeasonData}
|
|
linkedCaptions={props.stream.captions}
|
|
/>
|
|
<SourceController
|
|
source={props.stream.streamUrl}
|
|
type={props.stream.type}
|
|
quality={props.stream.quality}
|
|
embedId={props.stream.embedId}
|
|
providerId={props.stream.providerId}
|
|
/>
|
|
<ProgressListenerController
|
|
startAt={firstStartTime.current}
|
|
onProgress={updateProgress}
|
|
/>
|
|
<SeriesController
|
|
onSelect={(d) =>
|
|
d.seasonId &&
|
|
d.episodeId &&
|
|
props.onChangeStream?.(d.seasonId, d.episodeId)
|
|
}
|
|
/>
|
|
</VideoPlayer>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function MediaView() {
|
|
const params = useParams<{
|
|
media: string;
|
|
episode?: string;
|
|
season?: string;
|
|
}>();
|
|
const goBack = useGoBack();
|
|
const history = useHistory();
|
|
|
|
const [meta, setMeta] = useState<DetailedMeta | null>(null);
|
|
const [selected, setSelected] = useState<SelectedMediaData | null>(null);
|
|
const [exec, loading, error] = useLoading(
|
|
async (mediaParams: string, seasonId?: string) => {
|
|
const data = decodeJWId(mediaParams);
|
|
if (!data) return null;
|
|
return getMetaFromId(data.type, data.id, seasonId);
|
|
}
|
|
);
|
|
// TODO get stream from someplace that actually gets updated
|
|
const [stream, setStream] = useState<MWStream | null>(null);
|
|
|
|
const lastSearchValue = useRef<(string | undefined)[] | null>(null);
|
|
useEffect(() => {
|
|
const newValue = [params.media, params.season, params.episode];
|
|
const lastVal = lastSearchValue.current;
|
|
|
|
const isSame =
|
|
lastVal?.[0] === newValue[0] &&
|
|
(lastVal?.[1] === newValue[1] || !lastVal?.[1]) &&
|
|
(lastVal?.[2] === newValue[2] || !lastVal?.[2]);
|
|
|
|
lastSearchValue.current = newValue;
|
|
if (isSame && lastVal !== null) return;
|
|
|
|
setMeta(null);
|
|
setStream(null);
|
|
setSelected(null);
|
|
exec(params.media, params.season).then((v) => {
|
|
setMeta(v ?? null);
|
|
setStream(null);
|
|
if (v) {
|
|
if (v.meta.type !== MWMediaType.SERIES) {
|
|
setSelected({
|
|
type: v.meta.type,
|
|
season: undefined,
|
|
episode: undefined,
|
|
});
|
|
} 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,
|
|
});
|
|
if (season !== params.season || episode !== params.episode)
|
|
history.replace(
|
|
`/media/${encodeURIComponent(params.media)}/${encodeURIComponent(
|
|
season
|
|
)}/${encodeURIComponent(episode)}`
|
|
);
|
|
}
|
|
} else setSelected(null);
|
|
});
|
|
}, [exec, history, params]);
|
|
|
|
if (loading) return <MediaViewLoading onGoBack={goBack} />;
|
|
if (error) return <MediaFetchErrorView />;
|
|
if (!meta || !selected)
|
|
return (
|
|
<NotFoundWrapper video>
|
|
<NotFoundMedia />
|
|
</NotFoundWrapper>
|
|
);
|
|
|
|
// scraping view will start scraping and return with onStream
|
|
if (!stream)
|
|
return (
|
|
<MediaViewScraping
|
|
meta={meta}
|
|
selected={selected}
|
|
onGoBack={goBack}
|
|
onStream={setStream}
|
|
/>
|
|
);
|
|
|
|
// show stream once we have a stream
|
|
return (
|
|
<MediaViewPlayer
|
|
meta={meta}
|
|
stream={stream}
|
|
selected={selected}
|
|
onChangeStream={(sId, eId) => {
|
|
history.replace(
|
|
`/media/${encodeURIComponent(params.media)}/${encodeURIComponent(
|
|
sId
|
|
)}/${encodeURIComponent(eId)}`
|
|
);
|
|
}}
|
|
/>
|
|
);
|
|
}
|