diff options
| author | Factiven <[email protected]> | 2024-01-17 21:13:02 +0700 |
|---|---|---|
| committer | Factiven <[email protected]> | 2024-01-17 21:13:02 +0700 |
| commit | 4402665b79fcdf2e1a10f75bfd6aab86c4890de9 (patch) | |
| tree | a86f4161f8d7115eb1457f57a47104f15a81414a | |
| parent | Refactor episode fetching and data formatting (diff) | |
| download | moopa-4402665b79fcdf2e1a10f75bfd6aab86c4890de9.tar.xz moopa-4402665b79fcdf2e1a10f75bfd6aab86c4890de9.zip | |
Refactor code
| -rw-r--r-- | components/secret.js | 36 | ||||
| -rw-r--r-- | components/watch/new-player/player.tsx | 62 | ||||
| -rw-r--r-- | components/watch/secondary/episodeLists.tsx | 6 | ||||
| -rw-r--r-- | lib/context/WatchPageProvider.tsx | 88 | ||||
| -rw-r--r-- | lib/context/watchPageProvider.js | 58 | ||||
| -rw-r--r-- | lib/hooks/useChapterTracks.ts | 62 | ||||
| -rw-r--r-- | pages/_app.tsx | 25 | ||||
| -rw-r--r-- | pages/api/v2/episode/[id].tsx | 14 | ||||
| -rw-r--r-- | pages/en/anime/watch/[...info].js | 669 | ||||
| -rw-r--r-- | pages/en/anime/watch/index.tsx | 166 |
10 files changed, 348 insertions, 838 deletions
diff --git a/components/secret.js b/components/secret.js deleted file mode 100644 index 782fcf5..0000000 --- a/components/secret.js +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useState } from "react"; - -export default function SecretPage({ cheatCode, onCheatCodeEntered }) { - const [typedCode, setTypedCode] = useState(""); - const [timer, setTimer] = useState(null); - - const handleKeyPress = (e) => { - const newTypedCode = typedCode + e.key; - - if (newTypedCode === cheatCode) { - onCheatCodeEntered(); - setTypedCode(""); - } else { - setTypedCode(newTypedCode); - - // Reset the timer if the user stops typing for 2 seconds - clearTimeout(timer); - const newTimer = setTimeout(() => { - setTypedCode(""); - }, 2000); - setTimer(newTimer); - } - }; - - useEffect(() => { - window.addEventListener("keydown", handleKeyPress); - - return () => { - window.removeEventListener("keydown", handleKeyPress); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [typedCode]); - - return; -} diff --git a/components/watch/new-player/player.tsx b/components/watch/new-player/player.tsx index 5e25a61..1bbfd40 100644 --- a/components/watch/new-player/player.tsx +++ b/components/watch/new-player/player.tsx @@ -20,6 +20,7 @@ import { Subtitle } from "types/episodes/TrackData"; import useWatchStorage from "@/lib/hooks/useWatchStorage"; import { Sessions } from "types/episodes/Sessions"; import { useAniList } from "@/lib/anilist/useAnilist"; +import useChapterTracks from "@/lib/hooks/useChapterTracks"; export interface Navigation { prev: Prev; @@ -115,6 +116,8 @@ export default function VidStack({ const router = useRouter(); + useChapterTracks(track, duration, setChapters); + useEffect(() => { if (qualities.length > 0) { const sourceQuality = qualities.reduce( @@ -255,58 +258,13 @@ export default function VidStack({ }); }, [playerState?.currentTime, playerState?.isPlaying]); - useEffect(() => { - const chapter = track?.skip; - const videoDuration = Math.round(duration); - - if (!chapter || chapter.length === 0 || !player.current) { - // Handle cases where there's no chapter data or player is not ready - setChapters(""); - return; - } - - let vtt = "WEBVTT\n\n"; - - chapter.forEach((item: { endTime: any; startTime: any; text: any }) => { - if (!item.endTime) { - // Handle missing endTime gracefully - console.warn("Skipping item with missing endTime:", item); - return; - } - - const [startMinutes, startSeconds] = formatTime(item.startTime); - const [endMinutes, endSeconds] = formatTime(item.endTime); - - vtt += `${startMinutes}:${startSeconds} --> ${endMinutes}:${endSeconds}\n${item.text}\n\n`; - }); - - if (chapter[chapter.length - 1].endTime < videoDuration) { - // Add a final chapter if needed - const [startMinutes, startSeconds] = formatTime( - chapter[chapter.length - 1].endTime - ); - const [endMinutes, endSeconds] = formatTime(videoDuration); - vtt += `${startMinutes}:${startSeconds} --> ${endMinutes}:${endSeconds}\n\n\n`; - } - - const vttBlob = new Blob([vtt], { type: "text/vtt" }); - const vttUrl = URL.createObjectURL(vttBlob); - - setChapters(vttUrl); - - return () => { - setChapters(""); - URL.revokeObjectURL(vttUrl); // Clean up VTT URL - }; - }, [track?.skip, duration, player.current]); - - useEffect(() => { - return () => { - if (player.current) { - player.current.destroy(); - } - }; - }, [id]); + // useEffect(() => { + // return () => { + // if (player.current) { + // player.current.destroy(); + // } + // }; + // }, [id]); function onEnded() { if (!navigation?.next?.id) return; diff --git a/components/watch/secondary/episodeLists.tsx b/components/watch/secondary/episodeLists.tsx index 2c23f25..48f0222 100644 --- a/components/watch/secondary/episodeLists.tsx +++ b/components/watch/secondary/episodeLists.tsx @@ -25,7 +25,7 @@ export default function EpisodeLists({ episode, artStorage, track, - dub, + dub }: EpisodeListsProps) { const progress = info.mediaListEntry?.progress; @@ -108,6 +108,7 @@ export default function EpisodeLists({ ? "pointer-events-none ring-1 ring-action" : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" }`} + replace > <div className="w-[43%] lg:w-[42%] h-[110px] relative rounded-lg z-40 shrink-0 overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> <div className="relative"> @@ -133,7 +134,7 @@ export default function EpisodeLists({ ? "100%" : artStorage?.[item?.id] !== undefined ? `${prog}%` - : "0%", + : "0%" }} /> <span className="absolute bottom-2 left-2 font-karla font-bold text-sm text-white"> @@ -183,6 +184,7 @@ export default function EpisodeLists({ ? "pointer-events-none ring-1 ring-action text-[#5d5d5d]" : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" }`} + replace > Episode {item.number} </Link> diff --git a/lib/context/WatchPageProvider.tsx b/lib/context/WatchPageProvider.tsx new file mode 100644 index 0000000..c4ff5b4 --- /dev/null +++ b/lib/context/WatchPageProvider.tsx @@ -0,0 +1,88 @@ +import { ReactNode, createContext, useContext, useState } from "react"; + +interface PageState { + theaterMode: boolean; + aspectRatio: string; +} + +interface PlayerState { + currentTime: number; + isPlaying: boolean; + autoPlay: boolean; + autoNext: boolean; +} + +interface RatingModalState { + isOpen: boolean; + isFullscreen: boolean; +} + +interface WatchPageContextProps { + pageState: PageState; + setPageState: React.Dispatch<React.SetStateAction<PageState>>; + playerState: PlayerState; + setPlayerState: React.Dispatch<React.SetStateAction<PlayerState>>; + userData: object; + setUserData: React.Dispatch<React.SetStateAction<object>>; + dataMedia: object; + setDataMedia: React.Dispatch<React.SetStateAction<object>>; + ratingModalState: RatingModalState; + setRatingModalState: React.Dispatch<React.SetStateAction<RatingModalState>>; + track: object; + setTrack: React.Dispatch<React.SetStateAction<object>>; +} + +interface WatchPageProviderProps { + children: ReactNode; +} + +export const WatchPageContext = createContext<WatchPageContextProps>( + {} as WatchPageContextProps +); + +export const WatchPageProvider = ({ children }: WatchPageProviderProps) => { + const [pageState, setPageState] = useState({ + theaterMode: false, + aspectRatio: "16/9" + }); + const [playerState, setPlayerState] = useState({ + currentTime: 0, + isPlaying: false, + autoPlay: false, + autoNext: false + }); + + const [userData, setUserData] = useState({}); + const [dataMedia, setDataMedia] = useState({}); + const [ratingModalState, setRatingModalState] = useState({ + isOpen: false, + isFullscreen: false + }); + + const [track, setTrack] = useState({}); + + return ( + <WatchPageContext.Provider + value={{ + pageState, + setPageState, + playerState, + setPlayerState, + userData, + setUserData, + dataMedia, + setDataMedia, + ratingModalState, + setRatingModalState, + track, + setTrack + }} + > + {children} + </WatchPageContext.Provider> + ); +}; + +export function useWatchProvider() { + return useContext(WatchPageContext); +} diff --git a/lib/context/watchPageProvider.js b/lib/context/watchPageProvider.js deleted file mode 100644 index b7d78b3..0000000 --- a/lib/context/watchPageProvider.js +++ /dev/null @@ -1,58 +0,0 @@ -import React, { createContext, useContext, useState } from "react"; - -export const WatchPageContext = createContext(); - -export const WatchPageProvider = ({ children }) => { - const [theaterMode, setTheaterMode] = useState(false); - const [aspectRatio, setAspectRatio] = useState("16/9"); - const [playerState, setPlayerState] = useState({ - currentTime: 0, - isPlaying: false, - }); - const [autoplay, setAutoPlay] = useState(null); - const [autoNext, setAutoNext] = useState(null); - const [marked, setMarked] = useState(0); - - const [userData, setUserData] = useState(null); - const [dataMedia, setDataMedia] = useState(null); - - const [ratingModalState, setRatingModalState] = useState({ - isOpen: false, - isFullscreen: false, - }); - - const [track, setTrack] = useState(null); - - return ( - <WatchPageContext.Provider - value={{ - theaterMode, - setTheaterMode, - aspectRatio, - setAspectRatio, - playerState, - setPlayerState, - userData, - setUserData, - autoplay, - setAutoPlay, - marked, - setMarked, - track, - setTrack, - dataMedia, - setDataMedia, - autoNext, - setAutoNext, - ratingModalState, - setRatingModalState, - }} - > - {children} - </WatchPageContext.Provider> - ); -}; - -export function useWatchProvider() { - return useContext(WatchPageContext); -} diff --git a/lib/hooks/useChapterTracks.ts b/lib/hooks/useChapterTracks.ts new file mode 100644 index 0000000..d20bfef --- /dev/null +++ b/lib/hooks/useChapterTracks.ts @@ -0,0 +1,62 @@ +import { useEffect } from "react"; + +interface SkipData { + startTime: number; + endTime: number; + text: string; +} + +const formatTime = (totalSeconds: number): string => { + const minutes = Math.floor(totalSeconds / 60) + .toString() + .padStart(2, "0"); + const seconds = (totalSeconds % 60).toString().padStart(2, "0"); + return `${minutes}:${seconds}`; +}; + +const generateVTTContent = ( + chapters: SkipData[], + videoDuration: number +): string => { + let vtt = "WEBVTT\n\n"; + let lastEndTime = 0; + + chapters.forEach((item) => { + const start = formatTime(item.startTime); + const end = formatTime(item.endTime); + vtt += `${start} --> ${end}\n${item.text}\n\n`; + lastEndTime = Math.max(lastEndTime, item.endTime); + }); + + if (lastEndTime < videoDuration) { + const start = formatTime(lastEndTime); + const end = formatTime(videoDuration); + vtt += `${start} --> ${end}\n\n`; + } + + return vtt; +}; + +const useChapterTracks = ( + track: { skip?: SkipData[] }, + duration: number, + setChapters: (url: string) => void +) => { + useEffect(() => { + if (track?.skip && track.skip.length > 0) { + const videoDuration = Math.round(duration); + const vttContent = generateVTTContent(track.skip, videoDuration); + const vttBlob = new Blob([vttContent], { type: "text/vtt" }); + const vttUrl = URL.createObjectURL(vttBlob); + + setChapters(vttUrl); + + return () => { + URL.revokeObjectURL(vttUrl); + setChapters(""); + }; + } + }, [track?.skip, duration, setChapters]); +}; + +export default useChapterTracks; diff --git a/pages/_app.tsx b/pages/_app.tsx index c6b72ca..52eed08 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -7,17 +7,16 @@ import { SessionProvider } from "next-auth/react"; import { SkeletonTheme } from "react-loading-skeleton"; import SearchPalette from "@/components/searchPalette"; import { SearchProvider } from "@/lib/context/isOpenState"; -import { WatchPageProvider } from "@/lib/context/watchPageProvider"; import { useEffect } from "react"; import { unixTimestampToRelativeTime } from "@/utils/getTimes"; -// import SecretPage from "@/components/secret"; import { Toaster, toast } from "sonner"; import ChangeLogs from "../components/shared/changelogs"; import type { AppProps } from "next/app"; +import { WatchPageProvider } from "@/lib/context/watchPageProvider"; export default function App({ Component, - pageProps: { session, ...pageProps }, + pageProps: { session, ...pageProps } }: AppProps) { const router = useRouter(); @@ -28,8 +27,8 @@ export default function App({ method: "GET", headers: { "Content-Type": "application/json", - "X-Broadcast-Key": "get-broadcast", - }, + "X-Broadcast-Key": "get-broadcast" + } }); const data = await res.json(); if (data?.show === true) { @@ -40,7 +39,7 @@ export default function App({ className: "font-karla", description: `${data.message} ${ data?.startAt ? unixTimestampToRelativeTime(data.startAt) : "" - }`, + }` }); } } catch (err) { @@ -50,10 +49,6 @@ export default function App({ getBroadcast(); }, []); - const handleCheatCodeEntered = () => { - alert("Cheat code entered!"); // You can replace this with your desired action - }; - return ( <> <SessionProvider session={session}> @@ -62,10 +57,6 @@ export default function App({ <AnimatePresence mode="wait"> <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> <Toaster richColors theme="dark" closeButton /> - {/* <SecretPage - cheatCode={"aofienaef"} - onCheatCodeEntered={handleCheatCodeEntered} - /> */} <ChangeLogs /> <m.div key={`route-${router.route}`} @@ -75,12 +66,12 @@ export default function App({ exit="exitState" variants={{ initialState: { - opacity: 0, + opacity: 0 }, animateState: { - opacity: 1, + opacity: 1 }, - exitState: {}, + exitState: {} }} className="z-50 w-screen" > diff --git a/pages/api/v2/episode/[id].tsx b/pages/api/v2/episode/[id].tsx index 6837038..4afb27a 100644 --- a/pages/api/v2/episode/[id].tsx +++ b/pages/api/v2/episode/[id].tsx @@ -25,7 +25,7 @@ const isAscending = (data: Episode[]) => { }; export interface RawEpisodeData { - map?: boolean; + from?: boolean; providerId: string; episodes: { sub: Episode[]; @@ -36,7 +36,7 @@ export interface RawEpisodeData { function filterData(data: RawEpisodeData[], type: "sub" | "dub") { // Filter the data based on the type (sub or dub) and providerId const filteredData = data.map((item) => { - if (item?.map === true) { + if (item?.from === "consumet") { if (item.episodes[type].length === 0) { return null; } else { @@ -100,7 +100,7 @@ async function fetchConsumet(id?: string | string[] | undefined) { const array = [ { - map: true, + from: "consumet", providerId: "gogoanime", episodes: { sub: isAscending(subData) ? subData : subData.reverse(), @@ -130,7 +130,13 @@ async function fetchAnify(id?: string) { (item) => item.providerId !== "9anime" && item.providerId !== "kass" ); - return filtered; + const array = filtered?.map((item) => ({ + from: "anify", + providerId: item.providerId, + episodes: item.episodes + })); + + return array; } catch (error: any) { console.error("Error fetching and processing data:", error.message); return []; diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js deleted file mode 100644 index 445d220..0000000 --- a/pages/en/anime/watch/[...info].js +++ /dev/null @@ -1,669 +0,0 @@ -import { useEffect, useState } from "react"; -import { FlagIcon, ShareIcon } from "@heroicons/react/24/solid"; -import Details from "@/components/watch/primary/details"; -import EpisodeLists from "@/components/watch/secondary/episodeLists"; -import { getServerSession } from "next-auth"; -import { useWatchProvider } from "@/lib/context/watchPageProvider"; -import { authOptions } from "../../../api/auth/[...nextauth]"; -import { getRemovedMedia } from "@/prisma/removed"; -import { createList, createUser, getEpisode } from "@/prisma/user"; -import Link from "next/link"; -import MobileNav from "@/components/shared/MobileNav"; -import { Navbar } from "@/components/shared/NavBar"; -import Modal from "@/components/modal"; -import AniList from "@/components/media/aniList"; -import { signIn } from "next-auth/react"; -import BugReportForm from "@/components/shared/bugReport"; -import Skeleton from "react-loading-skeleton"; -import Head from "next/head"; -import VidStack from "@/components/watch/new-player/player"; -import { useRouter } from "next/router"; -import { Spinner } from "@vidstack/react"; -import RateModal from "@/components/shared/RateModal"; - -export async function getServerSideProps(context) { - let userData = null; - const session = await getServerSession(context.req, context.res, authOptions); - const accessToken = session?.user?.token || null; - - const query = context?.query; - if (!query) { - return { - notFound: true - }; - } - - let proxy; - proxy = process.env.PROXY_URI || null; - if (proxy && proxy.endsWith("/")) { - proxy = proxy.slice(0, -1); - } - const disqus = process.env.DISQUS_SHORTNAME || null; - - const [aniId, provider] = query?.info; - const watchId = query?.id; - const epiNumber = query?.num; - const dub = query?.dub; - - const removed = await getRemovedMedia(); - - const isRemoved = removed?.find((i) => +i?.aniId === +aniId); - - if (isRemoved) { - return { - redirect: { - destination: "/en/removed", - permanent: false - } - }; - } - - const ress = await fetch(`https://graphql.anilist.co`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(accessToken && { Authorization: `Bearer ${accessToken}` }) - }, - body: JSON.stringify({ - query: `query ($id: Int) { - Media (id: $id) { - mediaListEntry { - progress - status - customLists - repeat - } - id - idMal - title { - romaji - english - native - } - status - genres - episodes - studios { - edges { - node { - id - name - } - } - } - bannerImage - description - coverImage { - extraLarge - color - } - synonyms - - } - } - `, - variables: { - id: aniId - } - }) - }); - const data = await ress.json(); - // const variables = { id: aniId }; - // const data = await getAnilistMediaInfo(variables, context.req); - - try { - if (session) { - await createUser(session.user.name); - await createList(session.user.name, watchId); - const data = await getEpisode(session.user.name, watchId); - userData = JSON.parse( - JSON.stringify(data, (key, value) => { - if (key === "createdDate") { - return String(value); - } - return value; - }) - ); - } - } catch (error) { - console.error(error); - // Handle the error here - } - return { - props: { - sessions: session, - provider: provider || null, - watchId: watchId || null, - epiNumber: epiNumber || null, - dub: dub || null, - userData: userData?.[0] || null, - info: data?.data?.Media || null, - proxy, - disqus - } - }; -} - -export default function Watch({ - info, - watchId, - disqus, - proxy, - dub, - userData, - sessions, - provider, - epiNumber -}) { - const [artStorage, setArtStorage] = useState(null); - - const [episodeNavigation, setEpisodeNavigation] = useState(null); - const [episodesList, setepisodesList] = useState(); - const [mapEpisode, setMapEpisode] = useState(null); - - const [open, setOpen] = useState(false); - const [isOpen, setIsOpen] = useState(false); - - const { setAutoNext, ratingModalState, setRatingModalState } = - useWatchProvider(); - - const [onList, setOnList] = useState(false); - - const router = useRouter(); - - const { - theaterMode, - setPlayerState, - setAutoPlay, - setMarked, - setTrack, - aspectRatio, - setDataMedia - } = useWatchProvider(); - - useEffect(() => { - async function getInfo() { - if (info.mediaListEntry) { - setOnList(true); - } - - setDataMedia(info); - - const response = await fetch( - `/api/v2/episode/${info.id}?releasing=${ - info.status === "RELEASING" ? "true" : "false" - }${dub ? "&dub=true" : ""}` - ).then((res) => res.json()); - const getMap = response.find((i) => i?.map === true) || response[0]; - let episodes = response; - - if (getMap) { - if (provider === "gogoanime" && !watchId.startsWith("/")) { - episodes = episodes.filter((i) => { - if (i?.providerId === "gogoanime" && i?.map !== true) { - return null; - } - return i; - }); - } - - setMapEpisode(getMap?.episodes); - } - - if (episodes) { - const getProvider = episodes?.find((i) => i.providerId === provider); - const episodeList = getProvider?.episodes.slice( - 0, - getMap?.episodes.length - ); - const playingData = getMap?.episodes.find( - (i) => i.number === Number(epiNumber) - ); - - if (getProvider) { - setepisodesList(episodeList); - const currentEpisode = episodeList?.find( - (i) => i.number === parseInt(epiNumber) - ); - const nextEpisode = episodeList?.find( - (i) => i.number === parseInt(epiNumber) + 1 - ); - const previousEpisode = episodeList?.find( - (i) => i.number === parseInt(epiNumber) - 1 - ); - const vidNav = { - prev: previousEpisode, - playing: { - id: currentEpisode.id, - title: playingData?.title || info?.title?.romaji, - description: playingData?.description, - img: playingData?.img || playingData?.image, - number: currentEpisode.number - }, - next: nextEpisode - }; - setEpisodeNavigation(vidNav); - } - } - - setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); - // setEpiData(episodes); - } - getInfo(); - - return () => { - setEpisodeNavigation(null); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessions?.user?.name, epiNumber, dub]); - - useEffect(() => { - const autoNext = localStorage.getItem("autoNext"), - autoPlay = localStorage.getItem("autoplay"); - if (autoNext) { - setAutoNext(autoNext); - } - if (autoPlay) { - setAutoPlay(autoPlay); - } - - async function fetchData() { - if (info) { - const anify = await fetch("/api/v2/source", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - source: - provider === "gogoanime" && !watchId.startsWith("/") - ? "consumet" - : "anify", - providerId: provider, - watchId: watchId, - episode: epiNumber, - id: info.id, - sub: dub ? "dub" : "sub" - }) - }).then((res) => res.json()); - - if (!anify?.sources?.length > 0) { - router.push(`/en/anime/${info.id}?notfound=true`); - return; - } - - const skip = await fetch( - `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt( - epiNumber - )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=` - ).then((res) => { - if (!res.ok) { - switch (res.status) { - case 404: { - return null; - } - } - } - return res.json(); - }); - - let getOp = - skip?.results?.find((item) => item.skipType === "op") || null, - getEd = skip?.results?.find((item) => item.skipType === "ed") || null; - - const op = getOp - ? { - startTime: - anify?.intro?.start ?? Math.round(getOp?.interval.startTime), - endTime: - anify?.intro?.end ?? Math.round(getOp?.interval.endTime), - text: "Opening" - } - : null, - ed = { - startTime: - anify?.outro?.start ?? Math.round(getEd?.interval.startTime), - endTime: anify?.outro?.end ?? Math.round(getEd?.interval.endTime), - text: "Ending" - }; - const skipData = [op, ed].filter((i) => i !== null); - - const quality = - anify?.sources?.find( - (i) => i.quality === "default" || i.quality === "auto" - ) || anify?.sources[0]; - - const reFormSubtitles = anify?.subtitles?.map((i) => { - return { - src: proxy + "/" + i.url, - label: i.lang, - kind: i.lang === "Thumbnails" ? "thumbnails" : "subtitles", - ...(i.lang === "English" && { default: true }) - }; - }); - - const thumbnails = reFormSubtitles?.find( - (i) => i.kind === "thumbnails" - ); - - const subtitles = reFormSubtitles?.filter( - (i) => i.kind !== "thumbnails" - ); - - const episode = { - provider, - isDub: dub, - defaultQuality: { - // url: quality?.url, - url: `${proxy}/proxy/m3u8/${encodeURIComponent( - String(quality?.url) - )}/${encodeURIComponent(JSON.stringify(anify?.headers))}`, - headers: anify?.headers - }, - subtitles: subtitles, - thumbnails: thumbnails?.src, - epiData: anify, - skip: skipData - }; - - setTrack(episode); - } - } - - fetchData(); - return () => { - setPlayerState({ - currentTime: 0, - isPlaying: false - }); - setMarked(0); - setTrack(null); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [provider, watchId, info?.id]); - - useEffect(() => { - const mediaSession = navigator.mediaSession; - if (!mediaSession) return; - - const now = episodeNavigation?.playing; - const poster = now?.img || info?.bannerImage; - const title = now?.title || info?.title?.romaji; - - const artwork = poster - ? [{ src: poster, sizes: "512x512", type: "image/jpeg" }] - : undefined; - - mediaSession.metadata = new MediaMetadata({ - title: title, - artist: `Moopa ${ - title === info?.title?.romaji - ? "- Episode " + epiNumber - : `- ${info?.title?.romaji || info?.title?.english}` - }`, - artwork - }); - }, [episodeNavigation, info, epiNumber]); - - const handleShareClick = async () => { - try { - if (navigator.share) { - await navigator.share({ - title: `Watch Now - ${info?.title?.english || info.title.romaji}`, - // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, - url: window.location.href - }); - } else { - // Web Share API is not supported, provide a fallback or show a message - alert("Web Share API is not supported in this browser."); - } - } catch (error) { - console.error("Error sharing:", error); - } - }; - - function handleOpen() { - setOpen(true); - document.body.style.overflow = "hidden"; - } - - function handleClose() { - setOpen(false); - document.body.style.overflow = "auto"; - } - - return ( - <> - <Head> - <title> - {episodeNavigation?.playing?.title || - `${info?.title?.romaji} - Episode ${epiNumber}`} - </title> - <meta - name="title" - data-title-romaji={info?.title?.romaji} - data-title-english={info?.title?.english} - data-title-native={info?.title?.native} - /> - <meta name="twitter:card" content="summary_large_image" /> - <meta - name="description" - content={episodeNavigation?.playing?.description || info?.description} - /> - <meta - name="keywords" - content="anime, anime streaming, anime streaming website, anime streaming free, anime streaming website free, anime streaming website free english subbed, anime streaming website free english dubbed, anime streaming website free english subbed and dubbed, anime streaming webs - ite free english subbed and dubbed download, anime streaming website free english subbed and dubbed" - /> - <meta name="robots" content="index, follow" /> - - <meta property="og:type" content="website" /> - <meta - property="og:title" - content={`Watch - ${ - episodeNavigation?.playing?.title || info?.title?.english - }`} - /> - <meta - property="og:description" - content={episodeNavigation?.playing?.description || info?.description} - /> - <meta - property="og:image" - content={episodeNavigation?.playing?.img || info?.bannerImage} - /> - <meta property="og:site_name" content="Moopa" /> - <meta name="twitter:card" content="summary_large_image" /> - <meta - name="twitter:image" - content={episodeNavigation?.playing?.img || info?.bannerImage} - /> - <meta - name="twitter:title" - content={`Watch - ${ - episodeNavigation?.playing?.title || info?.title?.english - }`} - /> - <meta - name="twitter:description" - content={episodeNavigation?.playing?.description || info?.description} - /> - </Head> - <Modal open={open} onClose={() => handleClose()}> - {!sessions && ( - <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> - <h1 className="text-md font-extrabold font-karla"> - Edit your list - </h1> - <button - className="flex items-center bg-[#363642] rounded-md text-white p-1" - onClick={() => signIn("AniListProvider")} - > - <h1 className="px-1 font-bold font-karla">Login with AniList</h1> - <div className="scale-[60%] pb-[1px]"> - <AniList /> - </div> - </button> - </div> - )} - </Modal> - <BugReportForm isOpen={isOpen} setIsOpen={setIsOpen} /> - <main className="w-screen h-full"> - {!ratingModalState.isFullscreen && ( - <RateModal - toggle={ratingModalState.isOpen} - setToggle={setRatingModalState} - position="bottom" - session={sessions} - /> - )} - <Navbar - scrollP={20} - withNav={true} - shrink={true} - paddingY={`py-2 ${theaterMode ? "" : "lg:py-4"}`} - /> - <MobileNav hideProfile={true} sessions={sessions} /> - <div - className={`mx-auto pt-16 ${theaterMode ? "lg:pt-16" : "lg:pt-20"}`} - > - {theaterMode && ( - <div - className={`bg-black w-full max-h-[84dvh] h-full flex-center rounded-md`} - style={{ aspectRatio: aspectRatio }} - > - {episodeNavigation ? ( - <VidStack - id={`${watchId}-theater`} - navigation={episodeNavigation} - sessions={sessions} - userData={userData} - /> - ) : ( - <div className="flex-center aspect-video w-full h-full relative"> - <SpinLoader /> - </div> - )} - </div> - )} - <div - id="default" - className={`${ - theaterMode ? "lg:max-w-[95%] xl:max-w-[80%]" : "lg:max-w-[95%]" - } w-full flex flex-col lg:flex-row mx-auto`} - > - <div id="primary" className="w-full"> - {!theaterMode && ( - <div - className={`bg-black w-full flex-center rounded-md overflow-hidden ${ - aspectRatio === "4/3" ? "aspect-video" : "" - }`} - // style={{ aspectRatio: aspectRatio }} - > - {episodeNavigation ? ( - <VidStack - id={`${watchId}-default`} - navigation={episodeNavigation} - sessions={sessions} - userData={userData} - /> - ) : ( - <div className="flex-center aspect-video w-full h-full relative"> - <SpinLoader /> - </div> - )} - </div> - )} - <div - id="details" - className="flex flex-col gap-5 w-full px-3 lg:px-0" - > - <div className="flex items-end justify-between pt-3 border-b-2 border-secondary pb-2"> - <div className="w-[55%]"> - <div className="flex font-outfit font-semibold text-lg lg:text-2xl text-white line-clamp-1"> - <Link - href={`/en/anime/${info?.id}`} - className="hover:underline line-clamp-1" - > - {(episodeNavigation?.playing?.title || - info.title.romaji) ?? - "Loading..."} - </Link> - </div> - <h3 className="font-karla"> - {episodeNavigation?.playing?.number ? ( - `Episode ${episodeNavigation?.playing?.number}` - ) : ( - <Skeleton width={120} height={16} /> - )} - </h3> - </div> - <div> - <div className="flex gap-2 text-sm"> - <button - type="button" - onClick={handleShareClick} - className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden" - > - <ShareIcon className="w-5 h-5" /> - <span className="hidden lg:block">share</span> - </button> - <button - type="button" - onClick={() => setIsOpen(true)} - className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden" - > - <FlagIcon className="w-5 h-5" /> - <span className="hidden lg:block">report</span> - </button> - </div> - </div> - </div> - - <Details - info={info} - session={sessions} - description={info?.description} - epiNumber={epiNumber} - id={info} - onList={onList} - setOnList={setOnList} - handleOpen={() => handleOpen()} - disqus={disqus} - /> - </div> - </div> - <div - id="secondary" - className={`relative ${theaterMode ? "pt-5" : "pt-4 lg:pt-0"}`} - > - <EpisodeLists - info={info} - session={sessions} - map={mapEpisode} - providerId={provider} - watchId={watchId} - episode={episodesList} - artStorage={artStorage} - track={episodeNavigation} - dub={dub} - /> - </div> - </div> - </div> - </main> - </> - ); -} - -function SpinLoader() { - return ( - <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center"> - <Spinner.Root className="text-white animate-spin opacity-100" size={84}> - <Spinner.Track className="opacity-25" width={8} /> - <Spinner.TrackFill className="opacity-75" width={8} /> - </Spinner.Root> - </div> - ); -} diff --git a/pages/en/anime/watch/index.tsx b/pages/en/anime/watch/index.tsx new file mode 100644 index 0000000..ad6235b --- /dev/null +++ b/pages/en/anime/watch/index.tsx @@ -0,0 +1,166 @@ +import MobileNav from "@/components/shared/MobileNav"; +import { Navbar } from "@/components/shared/NavBar"; +import { useWatchProvider } from "@/lib/context/watchPageProvider"; +import pls from "@/utils/request"; +import { GetServerSidePropsContext } from "next"; +import Head from "next/head"; +import { useEffect, useState } from "react"; + +type QueryProps = { + v: string; + id: string; + n: string; + prv: string; + dub: string; + t: string; +}; + +type WatchPageProps = { + watchId: string; + aniId: string; + provider: string; + dub: string; + epiNumber: string; + seekTo: string; +}; + +type EpisodeData = { + from: string; + providerId: string; + episodes: { + id: string; + isFiller: boolean; + number: number; + title: string; + img: string; + hasDub: boolean; + description: string; + }[]; +}; + +export async function getServerSideProps({ query }: GetServerSidePropsContext) { + const { v, id, prv, dub, n, t } = query as QueryProps; + return { + props: { + watchId: v || null, + aniId: id || null, + provider: prv || null, + dub: dub || null, + epiNumber: n || null, + seekTo: t || null + } + }; +} + +function filterGogoanime(providers: any[]) { + let gogoanimeCount = 0; + if (providers.some((p) => p.from === "consumet")) { + return providers.filter((provider) => { + if (provider.provider !== "gogoanime") { + return true; + } else if (provider.from === "consumet") { + gogoanimeCount++; + return gogoanimeCount <= 1; + } else { + return false; + } + }); + } else { + return providers; + } +} + +function getEpisodeNavigation( + episodesData: EpisodeData[], + current: number, + provider: string +) { + // todo get previous current and next episodes from providers + + console.log({ + episodesData, + current, + provider + }); + + const currentEpisode = episodesData + .find((ep) => ep.providerId === provider) + ?.episodes.find((ep) => ep.number === current); + return currentEpisode; +} + +export default function WatchPage({ + watchId, + aniId, + provider, + dub, + epiNumber, + seekTo +}: WatchPageProps) { + const [data, setData] = useState([]); + + const { track } = useWatchProvider(); + + useEffect(() => { + async function getEpisodeSource() { + const episodes = await pls.get(`/api/v2/episode/${aniId}`); + let getAllProvider = []; + + getAllProvider = episodes.map((ep: any) => ({ + provider: ep.providerId, + from: ep.from, + episode: ep.episodes.find( + (eps: any) => eps.number === parseInt(epiNumber) + ) + })); + + const filteredGogo = filterGogoanime(episodes); + + console.log( + getEpisodeNavigation(filteredGogo, parseInt(epiNumber), provider) + ); + + setData(episodes); + } + getEpisodeSource(); + }, [provider, epiNumber, aniId]); + + return ( + <> + <Head> + <title>Watch Anime</title> + </Head> + <main className="w-screen h-full"> + <Navbar + scrollP={20} + withNav + shrink + // todo add padding condition for theater mode + paddingY={`py-2`} + /> + {/* // todo add session */} + <MobileNav hideProfile /> + <div className="mx-auto pt-16"> + {/* <div>Player on fullscreen / theater not fullscreen</div> */} + <div className="w-full flex flex-col lg:flex-row mx-auto max-w-[95%]"> + {/* Default mode */} + <div className="w-full border"> + {/* Primary */} + <div className="aspect-video border border-yellow-300"> + Video Player + </div> + <div className="border border-green-300"> + <div>video details</div> + <div>more info</div> + </div> + </div> + <div className="border w-[25%] shrink-0"> + {/* Secondary */} + Episode lists + </div> + </div> + </div> + </main> + </> + ); +} |