From 0a3155d39986f588085d0f0aa410d07fa271f877 Mon Sep 17 00:00:00 2001 From: mrjvs <jellevs@gmail.com> Date: Sun, 8 Oct 2023 18:16:30 +0200 Subject: [PATCH] meta data shown correctly Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com> --- src/components/player/atoms/EpisodeTitle.tsx | 24 +++++++++ src/components/player/atoms/Title.tsx | 6 +++ src/components/player/atoms/index.ts | 2 + src/components/player/base/BackLink.tsx | 2 - src/components/player/base/Container.tsx | 2 + src/components/player/hooks/usePlayer.ts | 6 ++- .../player/internals/HeadUpdater.tsx | 31 +++++++++++ src/pages/PlayerView.tsx | 48 ++++++++++++----- src/stores/player/slices/source.ts | 51 +++++++++++++++++++ 9 files changed, 157 insertions(+), 15 deletions(-) create mode 100644 src/components/player/atoms/EpisodeTitle.tsx create mode 100644 src/components/player/atoms/Title.tsx create mode 100644 src/components/player/internals/HeadUpdater.tsx 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 ( + <div> + <span className="text-white font-medium mr-3"> + {t("seasons.seasonAndEpisode", { + season: meta?.season?.number, + episode: meta?.episode?.number, + })} + </span> + <span className="text-type-secondary font-medium"> + {meta?.episode?.title} + </span> + </div> + ); +} 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 <p>{title || "Beep beep, Richie!"}</p>; +} 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() { <Icon className="mr-2" icon={Icons.ARROW_LEFT} /> <span>{t("videoPlayer.backToHomeShort")}</span> </span> - <span className="text mx-3 text-type-secondary">/</span> - <span>Mr Jeebaloo's Big Ocean Adventure</span> </div> ); } 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) { <BaseContainer> <VideoContainer /> <VideoClickTarget /> + <HeadUpdater /> {props.children} </BaseContainer> ); 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 ( + <Helmet> + <title>{meta.title}</title> + </Helmet> + ); + } + + const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", { + season: meta.season?.number, + episode: meta.episode?.number, + }); + + return ( + <Helmet> + <title> + {meta.title} - {humanizedEpisodeId} + </title> + </Helmet> + ); +} 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<PlayerMeta>( + () => ({ + 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 ( <Player.Container onLoad={setScrapeStatus}> {status === playerStatus.SCRAPING ? ( <ScrapingPart - media={{ - type: "movie", - title: "Everything Everywhere All At Once", - tmdbId: "545611", - releaseYear: 2022, - }} + media={scrapeMedia} onGetStream={(out) => { if (out?.stream.type !== "file") return; const qualities = Object.keys( @@ -47,13 +72,12 @@ export function PlayerView() { <div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center"> <div className="flex space-x-3 items-center"> <Player.BackLink /> + <span className="text mx-3 text-type-secondary">/</span> + <Player.Title /> <Player.BookmarkButton /> </div> <div className="text-center hidden xl:flex justify-center items-center"> - <span className="text-white font-medium mr-3">S1 E5</span> - <span className="text-type-secondary font-medium"> - Mr. Jeebaloo discovers Atlantis - </span> + <Player.EpisodeTitle /> </div> <div className="flex items-center justify-end"> <BrandPill /> 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<SourceSlice> = (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 = {