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&apos;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 = {