diff --git a/src/components/player/hooks/usePlayerMeta.ts b/src/components/player/hooks/usePlayerMeta.ts new file mode 100644 index 00000000..2c5bb7cb --- /dev/null +++ b/src/components/player/hooks/usePlayerMeta.ts @@ -0,0 +1,61 @@ +import { useCallback, useMemo, useState } from "react"; + +import { DetailedMeta } from "@/backend/metadata/getmeta"; +import { MWMediaType } from "@/backend/metadata/types/mw"; +import { usePlayer } from "@/components/player/hooks/usePlayer"; +import { PlayerMeta, metaToScrapeMedia } from "@/stores/player/slices/source"; + +export function usePlayerMeta() { + const { setMeta, setScrapeStatus } = usePlayer(); + const [meta, _setPlayerMeta] = useState(null); + const scrapeMedia = useMemo( + () => (meta ? metaToScrapeMedia(meta) : null), + [meta] + ); + + const setPlayerMeta = useCallback( + (m: DetailedMeta, episodeId?: string) => { + let playerMeta: PlayerMeta; + if (m.meta.type === MWMediaType.SERIES) { + const ep = m.meta.seasonData.episodes.find((v) => v.id === episodeId); + if (!ep) return false; + playerMeta = { + type: "show", + releaseYear: +(m.meta.year ?? 0), + title: m.meta.title, + tmdbId: m.tmdbId ?? "", + imdbId: m.imdbId, + episode: { + number: ep.number, + title: ep.title, + tmdbId: ep.id, + }, + season: { + number: m.meta.seasonData.number, + title: m.meta.seasonData.title, + tmdbId: m.meta.seasonData.id, + }, + }; + } else { + playerMeta = { + type: "movie", + releaseYear: +(m.meta.year ?? 0), + title: m.meta.title, + tmdbId: m.tmdbId ?? "", + imdbId: m.imdbId, + }; + } + _setPlayerMeta(playerMeta); + setMeta(playerMeta); + setScrapeStatus(); + return true; + }, + [_setPlayerMeta, setMeta, setScrapeStatus] + ); + + return { + playerMeta: meta, + setPlayerMeta, + scrapeMedia, + }; +} diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 2f9f0eac..a73d5cca 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -1,123 +1,72 @@ -import { useEffect, useMemo } from "react"; +import { RunOutput } from "@movie-web/providers"; +import { useCallback } from "react"; +import { useParams } from "react-router-dom"; +import { useAsync } from "react-use"; import { MWStreamType } from "@/backend/helpers/streams"; -import { BrandPill } from "@/components/layout/BrandPill"; -import { Player } from "@/components/player"; -import { AutoPlayStart } from "@/components/player/atoms"; +import { getMetaFromId } from "@/backend/metadata/getmeta"; +import { decodeTMDBId } from "@/backend/metadata/tmdb"; import { usePlayer } from "@/components/player/hooks/usePlayer"; -import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; +import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; +import { PlayerPart } from "@/pages/parts/player/PlayerPart"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; -import { - PlayerMeta, - metaToScrapeMedia, - playerStatus, -} from "@/stores/player/slices/source"; +import { playerStatus } from "@/stores/player/slices/source"; export function PlayerView() { - const { status, setScrapeStatus, playMedia, setMeta } = usePlayer(); - const { showTargets, showTouchTargets } = useShouldShowControls(); + const params = useParams<{ + media: string; + episode?: string; + season?: string; + }>(); + const { status, playMedia } = usePlayer(); + const { setPlayerMeta, scrapeMedia } = usePlayerMeta(); - const meta = useMemo( - () => ({ - type: "show", - title: "Normal People", - releaseYear: 2020, - tmdbId: "89905", - episode: { number: 12, tmdbId: "2207576", title: "Episode 12" }, - season: { number: 1, tmdbId: "125160", title: "Season 1" }, - }), - [] + const { loading, error } = useAsync(async () => { + const data = decodeTMDBId(params.media); + if (!data) return; + + const meta = await getMetaFromId(data.type, data.id, params.season); + if (!meta) return; + + setPlayerMeta(meta); + }, []); + + const playAfterScrape = useCallback( + (out: RunOutput | null) => { + if (out?.stream.type !== "file") return; + const qualities = Object.keys(out.stream.qualities).sort( + (a, b) => Number(b) - Number(a) + ) as (keyof typeof out.stream.qualities)[]; + + let file; + for (const quality of qualities) { + if (out.stream.qualities[quality]?.url) { + file = out.stream.qualities[quality]; + break; + } + } + + if (!file) return; + + playMedia({ + type: MWStreamType.MP4, + url: file.url, + }); + }, + [playMedia] ); - useEffect(() => { - setMeta(meta); - }, [setMeta, meta]); - const scrapeMedia = useMemo(() => metaToScrapeMedia(meta), [meta]); - return ( - - {status === playerStatus.SCRAPING ? ( - { - if (out?.stream.type !== "file") return; - console.log(out.stream.qualities); - const qualities = Object.keys(out.stream.qualities).sort( - (a, b) => Number(b) - Number(a) - ) as (keyof typeof out.stream.qualities)[]; - - let file; - for (const quality of qualities) { - if (out.stream.qualities[quality]?.url) { - console.log(quality); - file = out.stream.qualities[quality]; - break; - } - } - - if (!file) return; - - playMedia({ - type: MWStreamType.MP4, - url: file.url, - }); - }} - /> + + {status === playerStatus.IDLE ? ( +
+ {loading ?

loading meta...

: null} + {error ?

failed to load meta!

: null} +
) : null} - - - - - - - - - - - - - - - -
-
- - / - - -
-
- -
-
- -
-
-
- - - -
- - - - - - - - - {/* Do mobile controls here :) */} - - -
- - -
-
-
-
+ {status === playerStatus.SCRAPING && scrapeMedia ? ( + + ) : null} + ); } diff --git a/src/pages/media/MediaErrorView.tsx b/src/pages/media/MediaErrorView.tsx deleted file mode 100644 index 220c686f..00000000 --- a/src/pages/media/MediaErrorView.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Helmet } from "react-helmet"; -import { useTranslation } from "react-i18next"; - -import { VideoPlayerHeader } from "@/_oldvideo/components/parts/VideoPlayerHeader"; -import { ErrorMessage } from "@/components/layout/ErrorBoundary"; -import { useGoBack } from "@/hooks/useGoBack"; - -export function MediaFetchErrorView() { - const { t } = useTranslation(); - const goBack = useGoBack(); - - return ( -
- - {t("media.errors.failedMeta")} - -
- -
- -

{t("media.errors.mediaFailed")}

-
-
- ); -} diff --git a/src/pages/media/MediaScrapeLog.tsx b/src/pages/media/MediaScrapeLog.tsx deleted file mode 100644 index db9da9e6..00000000 --- a/src/pages/media/MediaScrapeLog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Icon, Icons } from "@/components/Icon"; -import { ProgressRing } from "@/components/layout/ProgressRing"; -import { ScrapeEventLog } from "@/hooks/useScrape"; - -interface MediaScrapeLogProps { - events: ScrapeEventLog[]; -} - -interface MediaScrapePillProps { - event: ScrapeEventLog; -} - -function MediaScrapePillSkeleton() { - return
; -} - -function MediaScrapePill({ event }: MediaScrapePillProps) { - return ( -
-
- {!event.errored ? ( - - ) : ( - - )} -
-
-

- {event.id} -

-
-
- ); -} - -export function MediaScrapeLog(props: MediaScrapeLogProps) { - return ( -
-
-
-
- - {props.events.map((v) => ( - - ))} - -
-
-
-
-
-
- ); -} diff --git a/src/pages/media/MediaView.tsx b/src/pages/media/MediaView.tsx deleted file mode 100644 index 763ddc56..00000000 --- a/src/pages/media/MediaView.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { Helmet } from "react-helmet"; -import { useTranslation } from "react-i18next"; -import { useHistory, useParams } from "react-router-dom"; - -import { MetaController } from "@/_oldvideo/components/controllers/MetaController"; -import { ProgressListenerController } from "@/_oldvideo/components/controllers/ProgressListenerController"; -import { SeriesController } from "@/_oldvideo/components/controllers/SeriesController"; -import { SourceController } from "@/_oldvideo/components/controllers/SourceController"; -import { VideoPlayerHeader } from "@/_oldvideo/components/parts/VideoPlayerHeader"; -import { VideoPlayer } from "@/_oldvideo/components/VideoPlayer"; -import { VideoPlayerMeta } from "@/_oldvideo/state/types"; -import { MWStream } from "@/backend/helpers/streams"; -import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; -import { decodeTMDBId } from "@/backend/metadata/tmdb"; -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"; -import { useGoBack } from "@/hooks/useGoBack"; -import { useLoading } from "@/hooks/useLoading"; -import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; -import { ErrorWrapperPart } from "@/pages/parts/errors/ErrorWrapperPart"; -import { MediaNotFoundPart } from "@/pages/parts/errors/MediaNotFoundPart"; -import { useWatchedItem } from "@/state/watched"; - -import { MediaFetchErrorView } from "./MediaErrorView"; -import { MediaScrapeLog } from "./MediaScrapeLog"; - -function MediaViewLoading(props: { onGoBack(): void }) { - const { t } = useTranslation(); - - return ( -
- - {t("videoPlayer.loading")} - -
- -
-
- -

- {t("videoPlayer.findingBestVideo")} -

-
-
- ); -} - -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 ( -
- - {props.meta.meta.title} - -
- -
-
- {pending ? ( - <> - -

- {t("videoPlayer.findingBestVideo")} -

- - ) : ( - <> - -

{t("videoPlayer.noVideos")}

- - )} -
- -
-
-
- ); -} - -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 ( -
- - - - - - - - - d.seasonId && - d.episodeId && - props.onChangeStream?.(d.seasonId, d.episodeId) - } - /> - -
- ); -} - -export function MediaView() { - const params = useParams<{ - media: string; - episode?: string; - season?: string; - }>(); - const goBack = useGoBack(); - const history = useHistory(); - - const [meta, setMeta] = useState(null); - const [selected, setSelected] = useState(null); - const [exec, loading, error] = useLoading( - async (mediaParams: string, seasonId?: string) => { - const data = decodeTMDBId(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(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 ; - if (error) return ; - if (!meta || !selected) - return ( - - - - ); - - // scraping view will start scraping and return with onStream - if (!stream) - return ( - - ); - - // show stream once we have a stream - return ( - { - history.replace( - `/media/${encodeURIComponent(params.media)}/${encodeURIComponent( - sId - )}/${encodeURIComponent(eId)}` - ); - }} - /> - ); -} diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx new file mode 100644 index 00000000..62092009 --- /dev/null +++ b/src/pages/parts/player/PlayerPart.tsx @@ -0,0 +1,73 @@ +import { ReactNode } from "react"; + +import { BrandPill } from "@/components/layout/BrandPill"; +import { Player } from "@/components/player"; +import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; + +export interface PlayerPartProps { + children?: ReactNode; + onLoad?: () => void; +} + +export function PlayerPart(props: PlayerPartProps) { + const { showTargets, showTouchTargets } = useShouldShowControls(); + + return ( + + {props.children} + + + + + + + + + + + + + + +
+
+ + / + + +
+
+ +
+
+ +
+
+
+ + + +
+ + + + + + + + + {/* Do mobile controls here :) */} + + +
+ + +
+
+
+
+ ); +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index dc47e9e7..840b612b 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -15,7 +15,7 @@ import { AboutPage } from "@/pages/About"; import { DmcaPage } from "@/pages/Dmca"; import { NotFoundPage } from "@/pages/errors/NotFoundPage"; import { HomePage } from "@/pages/HomePage"; -import { MediaView } from "@/pages/media/MediaView"; +import { PlayerView } from "@/pages/PlayerView"; import { Layout } from "@/setup/Layout"; import { BookmarkContextProvider } from "@/state/bookmark"; import { SettingsProvider } from "@/state/settings"; @@ -66,24 +66,26 @@ function App() { - - {/* pages */} - - - - - - - - - - - - - + + {({ match }) => { + if (match?.params.query) + return ; + return ; + }} + + + {/* pages */} + + + + +