diff --git a/src/components/player/atoms/EpisodeTitle.tsx b/src/components/player/atoms/EpisodeTitle.tsx new file mode 100644 index 00000000..f3cefcef --- /dev/null +++ b/src/components/player/atoms/EpisodeTitle.tsx @@ -0,0 +1,24 @@ +import { useTranslation } from "react-i18next"; + +import { usePlayerStore } from "@/stores/player/store"; + +export function EpisodeTitle() { + const { t } = useTranslation(); + const meta = usePlayerStore((s) => s.meta); + + if (meta?.type !== "show") return null; + + return ( +
+ + {t("seasons.seasonAndEpisode", { + season: meta?.season?.number, + episode: meta?.episode?.number, + })} + + + {meta?.episode?.title} + +
+ ); +} diff --git a/src/components/player/atoms/Title.tsx b/src/components/player/atoms/Title.tsx new file mode 100644 index 00000000..1fcf79b0 --- /dev/null +++ b/src/components/player/atoms/Title.tsx @@ -0,0 +1,6 @@ +import { usePlayerStore } from "@/stores/player/store"; + +export function Title() { + const title = usePlayerStore((s) => s.meta?.title); + return

{title || "Beep beep, Richie!"}

; +} diff --git a/src/components/player/atoms/index.ts b/src/components/player/atoms/index.ts index 80d87ffa..3487d9de 100644 --- a/src/components/player/atoms/index.ts +++ b/src/components/player/atoms/index.ts @@ -6,3 +6,5 @@ export * from "./Time"; export * from "./LoadingSpinner"; export * from "./AutoPlayStart"; export * from "./Volume"; +export * from "./Title"; +export * from "./EpisodeTitle"; diff --git a/src/components/player/base/BackLink.tsx b/src/components/player/base/BackLink.tsx index d4a3db62..31f4791d 100644 --- a/src/components/player/base/BackLink.tsx +++ b/src/components/player/base/BackLink.tsx @@ -16,8 +16,6 @@ export function BackLink() { {t("videoPlayer.backToHomeShort")} - / - Mr Jeebaloo's Big Ocean Adventure ); } diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index 63f25efb..472c9d0b 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -1,5 +1,6 @@ import { ReactNode, RefObject, useEffect, useRef } from "react"; +import { HeadUpdater } from "@/components/player/internals/HeadUpdater"; import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget"; import { VideoContainer } from "@/components/player/internals/VideoContainer"; import { PlayerHoverState } from "@/stores/player/slices/interface"; @@ -79,6 +80,7 @@ export function Container(props: PlayerProps) { + {props.children} ); diff --git a/src/components/player/hooks/usePlayer.ts b/src/components/player/hooks/usePlayer.ts index ea248fe5..8da2664c 100644 --- a/src/components/player/hooks/usePlayer.ts +++ b/src/components/player/hooks/usePlayer.ts @@ -1,6 +1,6 @@ import { MWStreamType } from "@/backend/helpers/streams"; import { useInitializePlayer } from "@/components/player/hooks/useInitializePlayer"; -import { playerStatus } from "@/stores/player/slices/source"; +import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; export interface Source { @@ -10,12 +10,16 @@ export interface Source { export function usePlayer() { const setStatus = usePlayerStore((s) => s.setStatus); + const setMeta = usePlayerStore((s) => s.setMeta); const status = usePlayerStore((s) => s.status); const display = usePlayerStore((s) => s.display); const { init } = useInitializePlayer(); return { status, + setMeta(meta: PlayerMeta) { + setMeta(meta); + }, playMedia(source: Source) { display?.load(source); setStatus(playerStatus.PLAYING); diff --git a/src/components/player/internals/HeadUpdater.tsx b/src/components/player/internals/HeadUpdater.tsx new file mode 100644 index 00000000..da62978e --- /dev/null +++ b/src/components/player/internals/HeadUpdater.tsx @@ -0,0 +1,31 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { usePlayerStore } from "@/stores/player/store"; + +export function HeadUpdater() { + const { t } = useTranslation(); + const meta = usePlayerStore((s) => s.meta); + + if (!meta) return null; + if (meta.type !== "show") { + return ( + + {meta.title} + + ); + } + + const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", { + season: meta.season?.number, + episode: meta.episode?.number, + }); + + return ( + + + {meta.title} - {humanizedEpisodeId} + + + ); +} diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 53bb059f..459d2c6a 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -1,3 +1,5 @@ +import { useEffect, useMemo } from "react"; + import { MWStreamType } from "@/backend/helpers/streams"; import { BrandPill } from "@/components/layout/BrandPill"; import { Player } from "@/components/player"; @@ -5,22 +7,45 @@ import { AutoPlayStart } from "@/components/player/atoms"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; -import { playerStatus } from "@/stores/player/slices/source"; +import { + PlayerMeta, + metaToScrapeMedia, + playerStatus, +} from "@/stores/player/slices/source"; export function PlayerView() { - const { status, setScrapeStatus, playMedia } = usePlayer(); + const { status, setScrapeStatus, playMedia, setMeta } = usePlayer(); const desktopControlsVisible = useShouldShowControls(); + const meta = useMemo( + () => ({ + type: "show", + title: "House", + tmdbId: "1408", + releaseYear: 2004, + episode: { + number: 1, + title: "Pilot", + tmdbId: "63738", + }, + season: { + number: 1, + tmdbId: "3674", + title: "Season 1", + }, + }), + [] + ); + + useEffect(() => { + setMeta(meta); + }, [setMeta, meta]); + const scrapeMedia = useMemo(() => metaToScrapeMedia(meta), [meta]); return ( {status === playerStatus.SCRAPING ? ( { if (out?.stream.type !== "file") return; const qualities = Object.keys( @@ -47,13 +72,12 @@ export function PlayerView() {
+ / +
- S1 E5 - - Mr. Jeebaloo discovers Atlantis - +
diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index d62466ed..36404952 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -1,3 +1,5 @@ +import { ScrapeMedia } from "@movie-web/providers"; + import { MWStreamType } from "@/backend/helpers/streams"; import { MakeSlice } from "@/stores/player/slices/types"; import { ValuesOf } from "@/utils/typeguard"; @@ -15,21 +17,70 @@ export interface SourceSliceSource { type: MWStreamType; } +export interface PlayerMeta { + type: "movie" | "show"; + title: string; + tmdbId: string; + imdbId?: string; + releaseYear: number; + episode?: { + number: number; + tmdbId: string; + title: string; + }; + season?: { + number: number; + tmdbId: string; + title: string; + }; +} + export interface SourceSlice { status: PlayerStatus; source: SourceSliceSource | null; + meta: PlayerMeta | null; setStatus(status: PlayerStatus): void; setSource(url: string, type: MWStreamType): void; + setMeta(meta: PlayerMeta): void; +} + +export function metaToScrapeMedia(meta: PlayerMeta): ScrapeMedia { + if (meta.type === "show") { + if (!meta.episode || !meta.season) throw new Error("missing show data"); + return { + title: meta.title, + releaseYear: meta.releaseYear, + tmdbId: meta.tmdbId, + type: "show", + imdbId: meta.imdbId, + episode: meta.episode, + season: meta.season, + }; + } + + return { + title: meta.title, + releaseYear: meta.releaseYear, + tmdbId: meta.tmdbId, + type: "movie", + imdbId: meta.imdbId, + }; } export const createSourceSlice: MakeSlice = (set) => ({ source: null, status: playerStatus.IDLE, + meta: null, setStatus(status: PlayerStatus) { set((s) => { s.status = status; }); }, + setMeta(meta) { + set((s) => { + s.meta = meta; + }); + }, setSource(url: string, type: MWStreamType) { set((s) => { s.source = {