You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
269 lines
8.5 KiB
269 lines
8.5 KiB
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-[100dvh] 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)}` |
|
); |
|
}} |
|
/> |
|
); |
|
}
|
|
|