aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-09-26 23:35:35 +0700
committerFactiven <[email protected]>2023-09-26 23:35:35 +0700
commit20b8a7267827e3a07c1eef668c3b9c22fda43765 (patch)
tree2fec9006dfac5737d8b227bf5ccce73880800cc2
parentUpdate release.md (diff)
downloadmoopa-20b8a7267827e3a07c1eef668c3b9c22fda43765.tar.xz
moopa-20b8a7267827e3a07c1eef668c3b9c22fda43765.zip
Update v4.1.2v4.1.2
-rw-r--r--README.md4
-rw-r--r--components/admin/dashboard/index.js134
-rw-r--r--components/admin/layout.js75
-rw-r--r--components/admin/meta/AppendMeta.js252
-rw-r--r--components/anime/episode.js30
-rw-r--r--components/anime/mobile/reused/description.js2
-rw-r--r--components/anime/viewMode/thumbnailDetail.js4
-rw-r--r--components/anime/viewMode/thumbnailOnly.js2
-rw-r--r--components/anime/viewSelector.js4
-rw-r--r--components/home/schedule.js2
-rw-r--r--components/searchPalette.js2
-rw-r--r--components/shared/NavBar.js2
-rw-r--r--components/watch/player/artplayer.js43
-rw-r--r--components/watch/player/playerComponent.js5
-rw-r--r--components/watch/secondary/episodeLists.js7
-rw-r--r--lib/context/isOpenState.js (renamed from lib/hooks/isOpenState.js)0
-rw-r--r--lib/context/watchPageProvider.js (renamed from lib/hooks/watchPageProvider.js)0
-rw-r--r--package-lock.json45
-rw-r--r--package.json7
-rw-r--r--pages/_app.js4
-rw-r--r--pages/admin/index.js261
-rw-r--r--pages/api/v2/admin/meta/index.js6
-rw-r--r--pages/en/anime/[...id].js4
-rw-r--r--pages/en/anime/watch/[...info].js38
-rw-r--r--release.md7
-rw-r--r--utils/getRedisWithPrefix.js71
26 files changed, 716 insertions, 295 deletions
diff --git a/README.md b/README.md
index 17dda87..6d3abd6 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@
## Introduction
-<p><a href="https://moopa.live">Moopa</a> is an anime streaming website made possible by <a href="https://github.com/consumet">Consumet API</a> build with <a href="https://github.com/vercel/next.js/">NextJs</a> and <a href="https://github.com/tailwindlabs/tailwindcss">Tailwind</a> with a sleek and modern design that offers Anilist integration to help you keep track of your favorite anime series. Moopa is entirely free and does not feature any ads, making it a great option for you who want an uninterrupted viewing experience.</p>
+<p><a href="https://moopa.live">Moopa</a> is an anime streaming website made possible by the <a href="https://github.com/consumet">Consumet API</a>, built with <a href="https://github.com/vercel/next.js/">Next.js</a> and <a href="https://github.com/tailwindlabs/tailwindcss">Tailwind</a>, featuring a sleek and modern design. It offers Anilist integration to help you keep track of your favorite anime series. Moopa is entirely free and does not display any ads, making it a great option for those who want an uninterrupted viewing experience.</p>
## Features
@@ -80,7 +80,7 @@ If you encounter any issues or bug on the site please head to [issues](https://g
## For Local Development
-> If you host this site for personal use, please refrain from cloning it or adding ads. This project is non-profit and ads may violate its terms, leading to legal action or site takedown. Uphold these guidelines to maintain its integrity and mission.
+> If you want to self-host this app, please note that it is only allowed for personal use. Commercial use is not permitted, and including ads on your self-hosted site may result in actions such as site takedown.
1. Clone this repository using :
diff --git a/components/admin/dashboard/index.js b/components/admin/dashboard/index.js
new file mode 100644
index 0000000..64a1d6f
--- /dev/null
+++ b/components/admin/dashboard/index.js
@@ -0,0 +1,134 @@
+import React, { useState } from "react";
+
+export default function AdminDashboard({
+ animeCount,
+ infoCount,
+ metaCount,
+ report,
+}) {
+ const [message, setMessage] = useState("");
+ const [selectedTime, setSelectedTime] = useState("");
+ const [unixTimestamp, setUnixTimestamp] = useState(null);
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+ if (selectedTime) {
+ const unixTime = Math.floor(new Date(selectedTime).getTime() / 1000);
+ setUnixTimestamp(unixTime);
+ }
+ };
+ return (
+ <div className="flex flex-col gap-5 px-5 py-10 h-full">
+ <div className="flex flex-col gap-2">
+ <p className="font-semibold">Stats</p>
+ <div className="grid grid-cols-3 gap-5">
+ <div className="flex-center flex-col bg-secondary rounded p-5">
+ <p className="font-karla text-4xl">{animeCount}</p>
+ <p className="font-karla text-xl">Anime</p>
+ </div>
+ <div className="flex-center flex-col bg-secondary rounded p-5">
+ <p className="font-karla text-4xl">{infoCount}</p>
+ <p className="font-karla text-xl">detail info</p>
+ </div>
+ <div className="flex-center flex-col bg-secondary rounded p-5">
+ <p className="font-karla text-4xl">{metaCount}</p>
+ <p className="font-karla text-xl">Metadata</p>
+ </div>
+ </div>
+ </div>
+ <div className="grid grid-cols-2 gap-5 h-full">
+ <div className="flex flex-col gap-2">
+ <p className="font-semibold">Broadcast</p>
+ <div className="flex flex-col justify-between bg-secondary rounded p-5 h-full">
+ <form onSubmit={handleSubmit}>
+ <div className="mb-4">
+ <label
+ htmlFor="message"
+ className="block text-txt font-light mb-2"
+ >
+ Message
+ </label>
+ <input
+ type="text"
+ id="message"
+ value={message}
+ onChange={(e) => setMessage(e.target.value)}
+ required
+ className="w-full px-3 py-2 border rounded-md focus:outline-none text-black"
+ />
+ </div>
+ <div className="mb-4">
+ <label
+ htmlFor="selectedTime"
+ className="block text-txt font-light mb-2"
+ >
+ Select Time
+ </label>
+ <input
+ type="datetime-local"
+ id="selectedTime"
+ value={selectedTime}
+ onChange={(e) => setSelectedTime(e.target.value)}
+ required
+ className="w-full px-3 py-2 border rounded-md focus:outline-none text-black"
+ />
+ </div>
+ <button
+ type="submit"
+ className="bg-image text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300"
+ >
+ Submit
+ </button>
+ </form>
+ {unixTimestamp && (
+ <p>
+ Unix Timestamp: <strong>{unixTimestamp}</strong>
+ </p>
+ )}
+ </div>
+ </div>
+ <div className="flex flex-col gap-2">
+ <p className="font-semibold">Recent Reports</p>
+ <div className="bg-secondary rounded p-5 h-full">
+ <div className="rounded overflow-hidden w-full h-full">
+ {report?.map((i, index) => (
+ <div
+ key={index}
+ className="odd:bg-primary/80 even:bg-primary/40 p-2 flex justify-between items-center"
+ >
+ {i.desc}{" "}
+ {i.severity === "Low" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
+ </span>
+ )}
+ {i.severity === "Medium" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span>
+ </span>
+ )}
+ {i.severity === "High" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative animate-pulse inline-flex rounded-full h-3 w-3 bg-rose-500"></span>
+ </span>
+ )}
+ {i.severity === "Critical" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-900 opacity-75"></span>
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-red-900"></span>
+ </span>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="w-full h-full">a</div>
+ </div>
+ );
+}
diff --git a/components/admin/layout.js b/components/admin/layout.js
new file mode 100644
index 0000000..3209dcf
--- /dev/null
+++ b/components/admin/layout.js
@@ -0,0 +1,75 @@
+import {
+ CloudArrowUpIcon,
+ Cog6ToothIcon,
+ HomeIcon,
+ UserIcon,
+} from "@heroicons/react/24/outline";
+import Link from "next/link";
+import { useRouter } from "next/router";
+import React from "react";
+
+const Navigation = [
+ {
+ name: "Dashboard",
+ page: 1,
+ icon: <HomeIcon />,
+ current: false,
+ },
+ {
+ name: "Metadata",
+ page: 2,
+ icon: <CloudArrowUpIcon />,
+ current: false,
+ },
+ {
+ name: "Users",
+ page: 3,
+ icon: <UserIcon />,
+ current: false,
+ },
+ {
+ name: "Settings",
+ page: 4,
+ icon: <Cog6ToothIcon />,
+ current: false,
+ },
+];
+
+export default function AdminLayout({ children, page, setPage }) {
+ return (
+ <div className="relative w-screen h-screen">
+ <div className="absolute flex flex-col gap-5 top-0 left-0 py-2 bg-secondary w-[14rem] h-full">
+ <div className="flex flex-col px-3">
+ <p className="text-sm font-light text-action font-outfit">moopa</p>
+ <h1 className="text-2xl font-bold text-white">
+ Admin <br />
+ Dashboard
+ </h1>
+ </div>
+ <div className="flex flex-col px-1">
+ {Navigation.map((item, index) => (
+ <button
+ key={item.name}
+ onClick={() => {
+ setPage(item.page);
+ }}
+ className={`flex items-center gap-2 p-2 group ${
+ page == item.page ? "bg-image/50" : "text-txt"
+ } hover:bg-image rounded transition-colors duration-200 ease-in-out`}
+ >
+ <div
+ className={`w-5 h-5 ${
+ page == item.page ? "text-action" : "text-txt"
+ } group-hover:text-action`}
+ >
+ {item.icon}
+ </div>
+ <p>{item.name}</p>
+ </button>
+ ))}
+ </div>
+ </div>
+ <div className="ml-[14rem] overflow-x-hidden h-full">{children}</div>
+ </div>
+ );
+}
diff --git a/components/admin/meta/AppendMeta.js b/components/admin/meta/AppendMeta.js
new file mode 100644
index 0000000..1707ed2
--- /dev/null
+++ b/components/admin/meta/AppendMeta.js
@@ -0,0 +1,252 @@
+import Loading from "@/components/shared/loading";
+import Image from "next/image";
+import { useState } from "react";
+import { toast } from "react-toastify";
+
+// Define a function to convert the data
+function convertData(episodes) {
+ const convertedData = episodes.map((episode) => ({
+ episode: episode.episode,
+ title: episode?.title,
+ description: episode?.description || null,
+ img: episode?.img?.hd || episode?.img?.mobile || null, // Use hd if available, otherwise use mobile
+ }));
+
+ return convertedData;
+}
+
+export default function AppendMeta({ api }) {
+ const [id, setId] = useState();
+ const [resultData, setResultData] = useState(null);
+
+ const [query, setQuery] = useState("");
+ const [tmdbId, setTmdbId] = useState();
+ const [hasilQuery, setHasilQuery] = useState([]);
+ const [season, setSeason] = useState();
+
+ const [override, setOverride] = useState();
+
+ const [loading, setLoading] = useState(false);
+
+ const handleSearch = async () => {
+ try {
+ setLoading(true);
+ setResultData(null);
+ const res = await fetch(`${api}/meta/tmdb/${query}`);
+ const json = await res.json();
+ const data = json.results.filter((i) => i.type === "TV Series");
+ setHasilQuery(data);
+ setLoading(false);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleDetail = async () => {
+ try {
+ setLoading(true);
+ const res = await fetch(`${api}/meta/tmdb/info/${tmdbId}?type=TV%20Series
+`);
+ const json = await res.json();
+ const data = json.seasons;
+ setHasilQuery(data);
+ setLoading(false);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleStore = async () => {
+ try {
+ setLoading(true);
+ if (!resultData && !id) {
+ console.log("No data to store");
+ setLoading(false);
+ return;
+ }
+ const data = await fetch("/api/v2/admin/meta", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ id: id,
+ data: resultData,
+ }),
+ });
+ if (data.status === 200) {
+ const json = await data.json();
+ toast.success(json.message);
+ setLoading(false);
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleOverride = async () => {
+ setResultData(JSON.parse(override));
+ };
+
+ return (
+ <>
+ <div className="container mx-auto p-4 scrol">
+ <h1 className="text-3xl font-semibold mb-4">Append Data Page</h1>
+ <div>
+ <div className="space-y-3 mb-4">
+ <label>Search Anime:</label>
+ <input
+ type="text"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={query}
+ onChange={(e) => setQuery(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleSearch}
+ >
+ Find Anime{" "}
+ </button>
+ </div>
+ <div className="space-y-3 mb-4">
+ <label>Get Episodes:</label>
+ <input
+ type="number"
+ placeholder="TMDB ID"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={tmdbId}
+ onChange={(e) => setTmdbId(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleDetail}
+ >
+ Get Details
+ </button>
+ </div>
+
+ <div className="space-y-3 mb-4">
+ <label>Override Result:</label>
+ <textarea
+ rows="5"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={override}
+ onChange={(e) => setOverride(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleOverride}
+ >
+ Override{" "}
+ </button>
+ </div>
+
+ <div className="space-y-3 mb-4">
+ <label className="block text-sm font-medium text-gray-300">
+ Anime ID:
+ </label>
+ <input
+ type="number"
+ placeholder="AniList ID"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={id}
+ onChange={(e) => setId(e.target.value)}
+ />
+ </div>
+ <div className="mb-4">
+ <button
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleStore}
+ >
+ Store Data {season && `Season ${season}`}
+ </button>
+ </div>
+
+ {!loading && hasilQuery?.some((i) => i?.season) && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">
+ Which season do you want to format?
+ </h2>
+ <div className="w-full flex gap-2">
+ {hasilQuery?.map((season, index) => (
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ key={index}
+ onClick={() => {
+ setLoading(true);
+ const data = hasilQuery[index].episodes;
+ const convertedData = convertData(data);
+ setSeason(index + 1);
+ setResultData(convertedData);
+ console.log(convertedData);
+ setLoading(false);
+ }}
+ >
+ <p>{season.season} </p>
+ </button>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {!loading && resultData && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">Season {season}</h2>
+ <pre>{JSON.stringify(resultData, null, 2)}</pre>
+ </div>
+ )}
+ {!loading && hasilQuery && (
+ <div className="border rounded-md p-4 mt-4">
+ {/* <h2 className="text-lg font-semibold mb-2">
+ Result Data,{" "}
+ {hasilQuery.length > 0 && `${hasilQuery.length} Seasons`}:
+ </h2> */}
+ <div className="flex flex-wrap gap-10">
+ {!hasilQuery.every((i) => i?.episodes) &&
+ hasilQuery?.map((i, index) => (
+ <div
+ key={i.id}
+ className="flex flex-col items-center gap-2"
+ >
+ <p className="font-karla font-semibold">
+ {i.releaseDate}
+ </p>
+ <Image
+ src={i.image}
+ width={500}
+ height={500}
+ className="w-[160px] h-[210px] object-cover"
+ />
+ <button
+ className="bg-blue-500 text-white w-[160px] py-1 rounded-md hover:bg-blue-600 text-sm"
+ onClick={() => {
+ setTmdbId(i.id);
+ }}
+ >
+ <p className="line-clamp-1 px-1">{i.title}</p>
+ </button>
+ </div>
+ ))}
+ </div>
+ <pre>{JSON.stringify(hasilQuery, null, 2)}</pre>
+ </div>
+ )}
+
+ {loading && <Loading />}
+ </div>
+ <div>
+ {/* {resultData && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">Result Data:</h2>
+ <pre>{JSON.stringify(resultData, null, 2)}</pre>
+ </div>
+ )} */}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/components/anime/episode.js b/components/anime/episode.js
index 6f96c98..25ed997 100644
--- a/components/anime/episode.js
+++ b/components/anime/episode.js
@@ -34,12 +34,16 @@ export default function AnimeEpisode({
info.status === "RELEASING" ? "true" : "false"
}${isDub ? "&dub=true" : ""}`
).then((res) => res.json());
- const getMap = response.find((i) => i?.map === true);
+ const getMap = response.find((i) => i?.map === true) || response[0];
let allProvider = response;
if (getMap) {
allProvider = response.filter((i) => {
- if (i?.providerId === "gogoanime" && i?.map !== true) {
+ if (
+ i?.providerId === "gogoanime" &&
+ i?.providerId === "9anime" &&
+ i?.map !== true
+ ) {
return null;
}
return i;
@@ -122,7 +126,6 @@ export default function AnimeEpisode({
useEffect(() => {
if (artStorage) {
- // console.log({ artStorage });
const currentData =
JSON.parse(localStorage.getItem("artplayer_settings")) || {};
@@ -138,15 +141,18 @@ export default function AnimeEpisode({
}
if (!session?.user?.name) {
- setProgress(
- Object.keys(updatedData).length > 0
- ? Math.max(
- ...Object.keys(updatedData).map(
- (key) => updatedData[key].episode
- )
- )
- : 0
+ const maxWatchedEpisode = Object.keys(updatedData).reduce(
+ (maxEpisode, key) => {
+ const episodeData = updatedData[key];
+ if (episodeData.timeWatched >= episodeData.duration * 0.9) {
+ return Math.max(maxEpisode, episodeData.episode);
+ }
+ return maxEpisode;
+ },
+ 0
);
+
+ setProgress(maxWatchedEpisode);
} else {
return;
}
@@ -177,7 +183,7 @@ export default function AnimeEpisode({
setLoading(false);
} else {
const data = await res.json();
- const getMap = data.find((i) => i?.map === true);
+ const getMap = data.find((i) => i?.map === true) || data[0];
let allProvider = data;
if (getMap) {
diff --git a/components/anime/mobile/reused/description.js b/components/anime/mobile/reused/description.js
index 99973d3..3b61c80 100644
--- a/components/anime/mobile/reused/description.js
+++ b/components/anime/mobile/reused/description.js
@@ -10,7 +10,7 @@ export default function Description({
className={`${
info?.description?.replace(/<[^>]*>/g, "").length > 240
? ""
- : "pointer-events-none"
+ : "pointer-events-none hidden"
} ${
readMore ? "hidden" : ""
} absolute z-30 flex items-end justify-center top-0 w-full h-full transition-all duration-200 ease-linear md:opacity-0 md:hover:opacity-100 bg-gradient-to-b from-transparent to-primary to-95%`}
diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js
index c7d55a0..494a89f 100644
--- a/components/anime/viewMode/thumbnailDetail.js
+++ b/components/anime/viewMode/thumbnailDetail.js
@@ -41,9 +41,9 @@ export default function ThumbnailDetail({
className={`absolute bottom-0 left-0 h-[2px] bg-red-700`}
style={{
width:
- progress || (artStorage && epi?.number <= progress)
+ progress !== undefined && progress >= epi?.number
? "100%"
- : artStorage?.[epi?.id]
+ : artStorage?.[epi?.id] !== undefined
? `${prog}%`
: "0%",
}}
diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js
index 7259beb..1b403fa 100644
--- a/components/anime/viewMode/thumbnailOnly.js
+++ b/components/anime/viewMode/thumbnailOnly.js
@@ -29,7 +29,7 @@ export default function ThumbnailOnly({
className={`absolute bottom-7 left-0 h-[2px] bg-red-600`}
style={{
width:
- progress && artStorage && episode?.number <= progress
+ progress && episode?.number <= progress
? "100%"
: artStorage?.[episode?.id]
? `${prog}%`
diff --git a/components/anime/viewSelector.js b/components/anime/viewSelector.js
index f114a8b..baa13b2 100644
--- a/components/anime/viewSelector.js
+++ b/components/anime/viewSelector.js
@@ -6,6 +6,7 @@ export default function ViewSelector({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img === null ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
@@ -33,6 +34,7 @@ export default function ViewSelector({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img === null ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
@@ -52,6 +54,7 @@ export default function ViewSelector({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img === null ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
@@ -74,6 +77,7 @@ export default function ViewSelector({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img === null ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
diff --git a/components/home/schedule.js b/components/home/schedule.js
index a9846a7..d618412 100644
--- a/components/home/schedule.js
+++ b/components/home/schedule.js
@@ -55,7 +55,7 @@ export default function Schedule({ data, scheduleData, anime, update }) {
<div className="w-1/2 lg:w-2/5 hidden lg:block font-medium font-karla leading-[2.9rem] text-white line-clamp-1">
<Link
href={`/en/anime/${data.id}`}
- className="hover:underline underline-offset-4 decoration-2 lg:text-[1.7vw] "
+ className="hover:underline underline-offset-4 decoration-2 leading-3 lg:text-[1.5vw] "
>
{data.title.romaji || data.title.english || data.title.native}
</Link>
diff --git a/components/searchPalette.js b/components/searchPalette.js
index 07c8f89..38a0bc0 100644
--- a/components/searchPalette.js
+++ b/components/searchPalette.js
@@ -3,7 +3,7 @@ import { Combobox, Dialog, Menu, Transition } from "@headlessui/react";
import useDebounce from "../lib/hooks/useDebounce";
import Image from "next/image";
import { useRouter } from "next/router";
-import { useSearch } from "../lib/hooks/isOpenState";
+import { useSearch } from "../lib/context/isOpenState";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { BookOpenIcon, PlayIcon } from "@heroicons/react/20/solid";
import { useAniList } from "../lib/anilist/useAnilist";
diff --git a/components/shared/NavBar.js b/components/shared/NavBar.js
index 42fcff0..7bbd617 100644
--- a/components/shared/NavBar.js
+++ b/components/shared/NavBar.js
@@ -1,4 +1,4 @@
-import { useSearch } from "@/lib/hooks/isOpenState";
+import { useSearch } from "@/lib/context/isOpenState";
import { getCurrentSeason } from "@/utils/getTimes";
import { ArrowLeftIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid";
import { UserIcon } from "@heroicons/react/24/solid";
diff --git a/components/watch/player/artplayer.js b/components/watch/player/artplayer.js
index 4eb766d..2ab4ded 100644
--- a/components/watch/player/artplayer.js
+++ b/components/watch/player/artplayer.js
@@ -1,7 +1,7 @@
import { useEffect, useRef } from "react";
import Artplayer from "artplayer";
import Hls from "hls.js";
-import { useWatchProvider } from "../../../lib/hooks/watchPageProvider";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
import { seekBackward, seekForward } from "./component/overlay";
import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
@@ -10,6 +10,7 @@ export default function NewPlayer({
option,
getInstance,
provider,
+ track,
defSub,
defSize,
subtitles,
@@ -274,6 +275,46 @@ export default function NewPlayer({
],
});
+ if ("mediaSession" in navigator) {
+ art.on("video:timeupdate", () => {
+ const session = navigator.mediaSession;
+ if (!session) return;
+ session.setPositionState({
+ duration: art.duration,
+ playbackRate: art.playbackRate,
+ position: art.currentTime,
+ });
+ });
+
+ navigator.mediaSession.setActionHandler("play", () => {
+ art.play();
+ });
+
+ navigator.mediaSession.setActionHandler("pause", () => {
+ art.pause();
+ });
+
+ navigator.mediaSession.setActionHandler("previoustrack", () => {
+ if (track?.prev) {
+ router.push(
+ `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent(
+ track?.prev?.id
+ )}&num=${track?.prev?.number}`
+ );
+ }
+ });
+
+ navigator.mediaSession.setActionHandler("nexttrack", () => {
+ if (track?.next) {
+ router.push(
+ `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent(
+ track?.next?.id
+ )}&num=${track?.next?.number}`
+ );
+ }
+ });
+ }
+
playerRef.current = art;
art.events.proxy(document, "keydown", (event) => {
diff --git a/components/watch/player/playerComponent.js b/components/watch/player/playerComponent.js
index 9fe9cd3..a524b79 100644
--- a/components/watch/player/playerComponent.js
+++ b/components/watch/player/playerComponent.js
@@ -1,9 +1,9 @@
import React, { useEffect, useState } from "react";
import NewPlayer from "./artplayer";
import { icons } from "./component/overlay";
-import { useWatchProvider } from "../../../lib/hooks/watchPageProvider";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
import { useRouter } from "next/router";
-import { useAniList } from "../../../lib/anilist/useAnilist";
+import { useAniList } from "@/lib/anilist/useAnilist";
export function calculateAspectRatio(width, height) {
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
@@ -475,6 +475,7 @@ export default function PlayerComponent({
quality={source}
option={option}
provider={provider}
+ track={track}
defSize={defSize}
defSub={defSub}
subSize={subSize}
diff --git a/components/watch/secondary/episodeLists.js b/components/watch/secondary/episodeLists.js
index 8a057ce..5fa21ad 100644
--- a/components/watch/secondary/episodeLists.js
+++ b/components/watch/secondary/episodeLists.js
@@ -12,6 +12,7 @@ export default function EpisodeLists({
dub,
}) {
const progress = info.mediaListEntry?.progress;
+
return (
<div className="w-screen lg:max-w-sm xl:max-w-xl">
<h1 className="text-xl font-karla pl-5 pb-5 font-semibold">Up Next</h1>
@@ -67,11 +68,11 @@ export default function EpisodeLists({
className={`absolute bottom-0 left-0 h-[2px] bg-red-700`}
style={{
width:
- progress && artStorage && item?.number <= progress
+ progress !== undefined && progress >= item?.number
? "100%"
- : artStorage?.[item?.id]
+ : artStorage?.[item?.id] !== undefined
? `${prog}%`
- : "0",
+ : "0%",
}}
/>
<span className="absolute bottom-2 left-2 font-karla font-bold text-sm">
diff --git a/lib/hooks/isOpenState.js b/lib/context/isOpenState.js
index 6aade61..6aade61 100644
--- a/lib/hooks/isOpenState.js
+++ b/lib/context/isOpenState.js
diff --git a/lib/hooks/watchPageProvider.js b/lib/context/watchPageProvider.js
index a9d707b..a9d707b 100644
--- a/lib/hooks/watchPageProvider.js
+++ b/lib/context/watchPageProvider.js
diff --git a/package-lock.json b/package-lock.json
index 6315554..0632d17 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "moopa",
- "version": "4.1.1",
+ "version": "4.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "moopa",
- "version": "4.1.1",
+ "version": "4.1.2",
"dependencies": {
"@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.17",
- "@prisma/client": "^5.1.1",
+ "@prisma/client": "^5.3.1",
"@vercel/og": "^0.5.4",
"artplayer": "^5.0.9",
"artplayer-plugin-hls-quality": "^2.0.0",
@@ -26,7 +26,6 @@
"next": "^13.5.2",
"next-auth": "^4.22.0",
"next-pwa": "^5.6.0",
- "next-safe": "^3.4.1",
"nextjs-progressbar": "^0.0.16",
"nookies": "^2.5.2",
"rate-limiter-flexible": "^3.0.0",
@@ -44,7 +43,7 @@
"depcheck": "^1.4.3",
"eslint": "^8.38.0",
"eslint-config-next": "^13.5.2",
- "prisma": "^5.1.1",
+ "prisma": "^5.3.1",
"tailwind-scrollbar": "^2.1.0",
"tailwindcss": "^3.3.1"
}
@@ -2218,12 +2217,12 @@
}
},
"node_modules/@prisma/client": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.1.1.tgz",
- "integrity": "sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz",
+ "integrity": "sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==",
"hasInstallScript": true,
"dependencies": {
- "@prisma/engines-version": "5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e"
+ "@prisma/engines-version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59"
},
"engines": {
"node": ">=16.13"
@@ -2238,16 +2237,16 @@
}
},
"node_modules/@prisma/engines": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.1.1.tgz",
- "integrity": "sha512-NV/4nVNWFZSJCCIA3HIFJbbDKO/NARc9ej0tX5S9k2EVbkrFJC4Xt9b0u4rNZWL4V+F5LAjvta8vzEUw0rw+HA==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz",
+ "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==",
"devOptional": true,
"hasInstallScript": true
},
"node_modules/@prisma/engines-version": {
- "version": "5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e.tgz",
- "integrity": "sha512-owZqbY/wucbr65bXJ/ljrHPgQU5xXTSkmcE/JcbqE1kusuAXV/TLN3/exmz21SZ5rJ7WDkyk70J2G/n68iogbQ=="
+ "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz",
+ "integrity": "sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w=="
},
"node_modules/@resvg/resvg-wasm": {
"version": "2.4.1",
@@ -6748,14 +6747,6 @@
"webpack": "^4.4.0 || ^5.9.0"
}
},
- "node_modules/next-safe": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/next-safe/-/next-safe-3.4.1.tgz",
- "integrity": "sha512-GOam3DYMHUIKwxHeqVg9pkuYFhvtUeivHexdbL3lg0mjibsnIB3NOD81dKDgaeX+cReWbugdCtWYllzXmmpE+Q==",
- "peerDependencies": {
- "next": "^9.5.0 || ^10.2.1 || ^11.1.0 || ^12.1.0 || ^13.0.0"
- }
- },
"node_modules/next/node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
@@ -7461,13 +7452,13 @@
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
},
"node_modules/prisma": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.1.1.tgz",
- "integrity": "sha512-WJFG/U7sMmcc6TjJTTifTfpI6Wjoh55xl4AzopVwAdyK68L9/ogNo8QQ2cxuUjJf/Wa82z/uhyh3wMzvRIBphg==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.3.1.tgz",
+ "integrity": "sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
- "@prisma/engines": "5.1.1"
+ "@prisma/engines": "5.3.1"
},
"bin": {
"prisma": "build/index.js"
diff --git a/package.json b/package.json
index 61ba395..116d84a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "moopa",
- "version": "4.1.1",
+ "version": "4.1.2",
"private": true,
"founder": "Factiven",
"scripts": {
@@ -14,7 +14,7 @@
"@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.17",
- "@prisma/client": "^5.1.1",
+ "@prisma/client": "^5.3.1",
"@vercel/og": "^0.5.4",
"artplayer": "^5.0.9",
"artplayer-plugin-hls-quality": "^2.0.0",
@@ -29,7 +29,6 @@
"next": "^13.5.2",
"next-auth": "^4.22.0",
"next-pwa": "^5.6.0",
- "next-safe": "^3.4.1",
"nextjs-progressbar": "^0.0.16",
"nookies": "^2.5.2",
"rate-limiter-flexible": "^3.0.0",
@@ -47,7 +46,7 @@
"depcheck": "^1.4.3",
"eslint": "^8.38.0",
"eslint-config-next": "^13.5.2",
- "prisma": "^5.1.1",
+ "prisma": "^5.3.1",
"tailwind-scrollbar": "^2.1.0",
"tailwindcss": "^3.3.1"
}
diff --git a/pages/_app.js b/pages/_app.js
index 9e07d22..f553a98 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -7,9 +7,9 @@ import "react-toastify/dist/ReactToastify.css";
import "react-loading-skeleton/dist/skeleton.css";
import { SkeletonTheme } from "react-loading-skeleton";
import SearchPalette from "@/components/searchPalette";
-import { SearchProvider } from "@/lib/hooks/isOpenState";
+import { SearchProvider } from "@/lib/context/isOpenState";
import Head from "next/head";
-import { WatchPageProvider } from "@/lib/hooks/watchPageProvider";
+import { WatchPageProvider } from "@/lib/context/watchPageProvider";
import { ToastContainer, toast } from "react-toastify";
import { useEffect } from "react";
import { unixTimestampToRelativeTime } from "@/utils/getTimes";
diff --git a/pages/admin/index.js b/pages/admin/index.js
index 4fdc8c2..cbb5086 100644
--- a/pages/admin/index.js
+++ b/pages/admin/index.js
@@ -1,19 +1,14 @@
+import AdminDashboard from "@/components/admin/dashboard";
+import AdminLayout from "@/components/admin/layout";
+import AppendMeta from "@/components/admin/meta/AppendMeta";
+import {
+ countKeysWithPrefix,
+ countNumericKeys,
+ getValuesWithPrefix,
+} from "@/utils/getRedisWithPrefix";
import { getServerSession } from "next-auth";
import { authOptions } from "pages/api/auth/[...nextauth]";
-import { useState } from "react";
-import { toast } from "react-toastify";
-
-// Define a function to convert the data
-function convertData(episodes) {
- const convertedData = episodes.map((episode) => ({
- episode: episode.episode,
- title: episode?.title,
- description: episode?.description || null,
- img: episode?.img?.hd || episode?.img?.mobile || null, // Use hd if available, otherwise use mobile
- }));
-
- return convertedData;
-}
+import React, { useState } from "react";
export async function getServerSideProps(context) {
const sessions = await getServerSession(
@@ -43,221 +38,49 @@ export async function getServerSideProps(context) {
};
}
+ const [anime, info, meta, report] = await Promise.all([
+ countNumericKeys(),
+ countKeysWithPrefix("anime:"),
+ countKeysWithPrefix("meta:"),
+ getValuesWithPrefix("report:"),
+ ]);
+
return {
props: {
session: sessions,
+ animeCount: anime || 0,
+ infoCount: info || 0,
+ metaCount: meta || 0,
+ report: report || [],
api,
},
};
}
-export default function Admin({ api }) {
- const [id, setId] = useState();
- const [resultData, setResultData] = useState(null);
-
- const [query, setQuery] = useState("");
- const [tmdbId, setTmdbId] = useState();
- const [hasilQuery, setHasilQuery] = useState([]);
- const [season, setSeason] = useState();
-
- const [override, setOverride] = useState();
-
- const [loading, setLoading] = useState(false);
-
- const handleSearch = async () => {
- try {
- setLoading(true);
- setResultData(null);
- const res = await fetch(`${api}/meta/tmdb/${query}`);
- const json = await res.json();
- const data = json.results;
- setHasilQuery(data);
- setLoading(false);
- } catch (err) {
- console.log(err);
- }
- };
-
- const handleDetail = async () => {
- try {
- setLoading(true);
- const res = await fetch(`${api}/meta/tmdb/info/${tmdbId}?type=TV%20Series
-`);
- const json = await res.json();
- const data = json.seasons;
- setHasilQuery(data);
- setLoading(false);
- } catch (err) {
- console.log(err);
- }
- };
-
- const handleStore = async () => {
- try {
- setLoading(true);
- if (!resultData && !id) {
- console.log("No data to store");
- setLoading(false);
- return;
- }
- const data = await fetch("/api/v2/admin/meta", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- id: id,
- data: resultData,
- }),
- });
- if (data.status === 200) {
- const json = await data.json();
- toast.success(json.message);
- setLoading(false);
- }
- } catch (err) {
- console.log(err);
- }
- };
-
- const handleOverride = async () => {
- setResultData(JSON.parse(override));
- };
+export default function Admin({
+ animeCount,
+ infoCount,
+ metaCount,
+ report,
+ api,
+}) {
+ const [page, setPage] = useState(1);
return (
- <>
- <div className="container mx-auto p-4">
- <h1 className="text-3xl font-semibold mb-4">Append Data Page</h1>
- <div>
- <div className="space-y-3 mb-4">
- <label>Search Anime:</label>
- <input
- type="text"
- className="w-full px-3 py-2 border rounded-md text-black"
- value={query}
- onChange={(e) => setQuery(e.target.value)}
- />
- <button
- type="button"
- className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
- onClick={handleSearch}
- >
- Find Anime{" "}
- {loading && <span className="animate-spin ml-2">🔄</span>}
- </button>
- </div>
- <div className="space-y-3 mb-4">
- <label>Get Episodes:</label>
- <input
- type="number"
- className="w-full px-3 py-2 border rounded-md text-black"
- value={tmdbId}
- onChange={(e) => setTmdbId(e.target.value)}
- />
- <button
- type="button"
- className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
- onClick={handleDetail}
- >
- Get Details{" "}
- {loading && <span className="animate-spin ml-2">🔄</span>}
- </button>
- </div>
-
- <div className="space-y-3 mb-4">
- <label>Override Result:</label>
- <textarea
- rows="5"
- className="w-full px-3 py-2 border rounded-md text-black"
- value={override}
- onChange={(e) => setOverride(e.target.value)}
- />
- <button
- type="button"
- className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
- onClick={handleOverride}
- >
- Override{" "}
- {loading && <span className="animate-spin ml-2">🔄</span>}
- </button>
- </div>
-
- <div className="space-y-3 mb-4">
- <label className="block text-sm font-medium text-gray-300">
- Anime ID:
- </label>
- <input
- type="number"
- className="w-full px-3 py-2 border rounded-md text-black"
- value={id}
- onChange={(e) => setId(e.target.value)}
- />
- </div>
- <div className="mb-4">
- <button
- className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
- onClick={handleStore}
- >
- Store Data {season && `Season ${season}`}
- </button>
- </div>
-
- {hasilQuery?.some((i) => i?.season) && (
- <div className="border rounded-md p-4 mt-4">
- <h2 className="text-lg font-semibold mb-2">
- Which season do you want to format?
- </h2>
- <div className="w-full flex gap-2">
- {hasilQuery?.map((season, index) => (
- <button
- type="button"
- className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
- key={index}
- onClick={() => {
- setLoading(true);
- const data = hasilQuery[index].episodes;
- const convertedData = convertData(data);
- setSeason(index + 1);
- setResultData(convertedData);
- console.log(convertedData);
- setLoading(false);
- }}
- >
- <p>
- {season.season}{" "}
- {loading && <span className="animate-spin ml-2">🔄</span>}
- </p>
- </button>
- ))}
- </div>
- </div>
- )}
-
- {resultData && (
- <div className="border rounded-md p-4 mt-4">
- <h2 className="text-lg font-semibold mb-2">Season {season}</h2>
- <pre>{JSON.stringify(resultData, null, 2)}</pre>
- </div>
- )}
- {hasilQuery && (
- <div className="border rounded-md p-4 mt-4">
- <h2 className="text-lg font-semibold mb-2">
- Result Data,{" "}
- {hasilQuery.length > 0 && `${hasilQuery.length} Seasons`}:
- </h2>
- <pre>{JSON.stringify(hasilQuery, null, 2)}</pre>
- </div>
- )}
- </div>
- <div>
- {/* {resultData && (
- <div className="border rounded-md p-4 mt-4">
- <h2 className="text-lg font-semibold mb-2">Result Data:</h2>
- <pre>{JSON.stringify(resultData, null, 2)}</pre>
- </div>
- )} */}
- </div>
+ <AdminLayout page={page} setPage={setPage}>
+ <div className="h-full">
+ {page == 1 && (
+ <AdminDashboard
+ animeCount={animeCount}
+ infoCount={infoCount}
+ metaCount={metaCount}
+ report={report}
+ />
+ )}
+ {page == 2 && <AppendMeta api={api} />}
+ {page == 3 && <p className="flex-center h-full">Coming Soon!</p>}
+ {page == 4 && <p className="flex-center h-full">Coming Soon!</p>}
</div>
- </>
+ </AdminLayout>
);
}
diff --git a/pages/api/v2/admin/meta/index.js b/pages/api/v2/admin/meta/index.js
index 5f51b7f..600a3ef 100644
--- a/pages/api/v2/admin/meta/index.js
+++ b/pages/api/v2/admin/meta/index.js
@@ -27,12 +27,6 @@ export default async function handler(req, res) {
});
}
- const getId = await redis.get(`meta:${id}`);
- if (getId) {
- return res
- .status(200)
- .json({ message: `Data already exist for id: ${id}` });
- }
await redis.set(`meta:${id}`, JSON.stringify(data));
return res
.status(200)
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
index 4809ce5..910bbc6 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -152,7 +152,7 @@ export default function Info({ info, color }) {
<MobileNav sessions={session} hideProfile={true} />
<main className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5">
<div className="w-screen absolute">
- <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0 backdrop-blur-[2px]" />
+ <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" />
{info?.bannerImage && (
<Image
src={info?.bannerImage}
@@ -160,7 +160,7 @@ export default function Info({ info, color }) {
height={1000}
width={1000}
blurDataURL={info?.bannerImage}
- className="object-cover bg-image w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0"
+ className="object-cover bg-image blur-[2px] w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0"
/>
)}
</div>
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index f5b4fce..b74a3f2 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -4,7 +4,7 @@ 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/hooks/watchPageProvider";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
import { authOptions } from "../../../api/auth/[...nextauth]";
import { createList, createUser, getEpisode } from "@/prisma/user";
import Link from "next/link";
@@ -289,6 +289,29 @@ export default function Watch({
};
}, [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) {
@@ -338,7 +361,6 @@ export default function Watch({
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
- <meta property="og:url" content="https://moopa.live/" />
<meta
property="og:title"
content={`Watch - ${
@@ -347,12 +369,19 @@ export default function Watch({
/>
<meta
property="og:description"
- content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
+ content={episodeNavigation?.playing?.description || info?.description}
+ />
+ <meta
+ property="og:image"
+ content={episodeNavigation?.playing?.img || info?.bannerImage}
/>
- <meta property="og:image" content="/preview.png" />
<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
@@ -499,6 +528,7 @@ export default function Watch({
>
<EpisodeLists
info={info}
+ session={sessions}
map={mapEpisode}
providerId={provider}
watchId={watchId}
diff --git a/release.md b/release.md
index fc941c3..bf390ca 100644
--- a/release.md
+++ b/release.md
@@ -2,10 +2,9 @@
This document contains a summary of all significant changes made to this release.
-## 🎉 Update v4.1.1
+## 🎉 Update v4.1.2
### Fixed
-- Another patch API episode route
-- Manga list at homepage send user to anime page
-- Video progress doesn't showed up on episode thumbnail
+- Improvement on episode thumbnail when showing progress
+- Resolved mediaSession not showing on mobile devices
diff --git a/utils/getRedisWithPrefix.js b/utils/getRedisWithPrefix.js
new file mode 100644
index 0000000..31a466d
--- /dev/null
+++ b/utils/getRedisWithPrefix.js
@@ -0,0 +1,71 @@
+import { redis } from "@/lib/redis";
+
+export async function getValuesWithPrefix(prefix) {
+ let cursor = "0"; // Start at the beginning of the keyspace
+ let values = [];
+
+ do {
+ const [newCursor, matchingKeys] = await redis.scan(
+ cursor,
+ "MATCH",
+ prefix + "*",
+ "COUNT",
+ 100
+ );
+
+ // Retrieve values for matching keys and add them to the array
+ for (const key of matchingKeys) {
+ const value = await redis.get(key);
+ values.push(JSON.parse(value));
+ }
+
+ // Update the cursor for the next iteration
+ cursor = newCursor;
+ } while (cursor !== "0"); // Continue until the cursor is '0'
+
+ return values;
+}
+
+export async function countKeysWithPrefix(prefix) {
+ let cursor = "0"; // Start at the beginning of the keyspace
+ let count = 0;
+
+ do {
+ const [newCursor, matchingKeys] = await redis.scan(
+ cursor,
+ "MATCH",
+ prefix + "*",
+ "COUNT",
+ 100
+ );
+
+ // Increment the count by the number of matching keys in this iteration
+ count += matchingKeys.length;
+
+ // Update the cursor for the next iteration
+ cursor = newCursor;
+ } while (cursor !== "0"); // Continue until the cursor is '0'
+
+ return count;
+}
+
+export async function getValuesWithNumericKeys() {
+ const allKeys = await redis.keys("*"); // Fetch all keys in Redis
+ const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
+
+ const values = [];
+
+ for (const key of numericKeys) {
+ const value = await redis.get(key); // Retrieve the value for each numeric key
+ values.push(value);
+ }
+
+ return values;
+}
+
+export async function countNumericKeys() {
+ const allKeys = await redis.keys("*"); // Fetch all keys in Redis
+ const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
+
+ return numericKeys.length; // Return the count of numeric keys
+}