aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--components/anime/watch/primarySide.js17
-rw-r--r--components/dataAni.json182
-rw-r--r--components/home/content.js269
-rw-r--r--components/home/schedule.js2
-rw-r--r--components/listEditor.js19
-rw-r--r--components/videoPlayer.js143
-rw-r--r--lib/Artplayer.js19
-rw-r--r--lib/anify/info.js33
-rw-r--r--lib/anify/page.js39
-rw-r--r--lib/prisma.js11
-rw-r--r--package-lock.json141
-rw-r--r--package.json5
-rw-r--r--pages/_document.js6
-rw-r--r--pages/api/anify/info/[id].js37
-rw-r--r--pages/api/anify/page/[...params].js41
-rw-r--r--pages/api/consumet/episode/[id].js12
-rw-r--r--pages/api/user/profile.js52
-rw-r--r--pages/api/user/update/episode.js68
-rw-r--r--pages/en/anime/[...id].js1
-rw-r--r--pages/en/anime/popular.js15
-rw-r--r--pages/en/anime/recently-watched.js159
-rw-r--r--pages/en/anime/trending.js15
-rw-r--r--pages/en/anime/watch/[...info].js29
-rw-r--r--pages/en/index.js159
-rw-r--r--pages/en/manga/[id].js24
-rw-r--r--pages/en/manga/read/[...params].js15
-rw-r--r--pages/en/search/[param].js7
-rw-r--r--prisma/schema.prisma31
-rw-r--r--prisma/user.js203
-rw-r--r--styles/globals.css40
31 files changed, 1595 insertions, 202 deletions
diff --git a/README.md b/README.md
index 02170e6..0802ba1 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,9 @@ PROXY_URI="I recommend you to use this cors-anywhere as a proxy https://github.c
API_URI="host your own API from this repo https://github.com/consumet/api.consumet.org. Don't put / at the end of the url."
API_KEY="this API key is used for schedules and manga page. get the key from https://anify.tv/discord"
DISQUS_SHORTNAME='put your disqus shortname here. (optional)'
+
+## Prisma
+DATABASE_URL="Your postgresql connection url"
```
4. Add this endpoint as Redirect Url on AniList Developer :
diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js
index fa12711..2e28563 100644
--- a/components/anime/watch/primarySide.js
+++ b/components/anime/watch/primarySide.js
@@ -26,6 +26,7 @@ export default function PrimarySide({
disqus,
setOnList,
episodeList,
+ timeWatched,
}) {
const [episodeData, setEpisodeData] = useState();
const [open, setOpen] = useState(false);
@@ -35,8 +36,6 @@ export default function PrimarySide({
useEffect(() => {
setLoading(true);
- setEpisodeData();
- setSkip();
async function fetchData() {
if (info) {
const { data } = await axios.get(
@@ -47,15 +46,15 @@ export default function PrimarySide({
`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((r) => {
- if (!r.ok) {
- switch (r.status) {
+ ).then((res) => {
+ if (!res.ok) {
+ switch (res.status) {
case 404: {
return null;
}
}
}
- return r.json();
+ return res.json();
});
const op =
@@ -72,6 +71,10 @@ export default function PrimarySide({
}
fetchData();
+ return () => {
+ setEpisodeData();
+ setSkip();
+ };
}, [providerId, watchId, info]);
useEffect(() => {
@@ -141,7 +144,9 @@ export default function PrimarySide({
skip={skip}
proxy={proxy}
aniId={info.id}
+ aniTitle={info.title?.romaji || info.title?.english}
track={navigation}
+ timeWatched={timeWatched}
/>
)
) : (
diff --git a/components/dataAni.json b/components/dataAni.json
new file mode 100644
index 0000000..7ffd8ee
--- /dev/null
+++ b/components/dataAni.json
@@ -0,0 +1,182 @@
+{
+ "currentPage": 1,
+ "hasNextPage": false,
+ "results": [
+ {
+ "id": "125367",
+ "malId": 43608,
+ "title": {
+ "romaji": "Kaguya-sama wa Kokurasetai: Ultra Romantic",
+ "english": "Kaguya-sama: Love is War -Ultra Romantic-",
+ "native": "かぐや様は告らせたい-ウルトラロマンティック-",
+ "userPreferred": "Kaguya-sama wa Kokurasetai: Ultra Romantic"
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx125367-bl5vGalMH2cC.png",
+ "cover": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/125367-hGPJLSNfprO3.jpg",
+ "popularity": 174430,
+ "description": "The elite members of Shuchiin Academy's student council continue their competitive day-to-day antics. Council president Miyuki Shirogane clashes daily against vice-president Kaguya Shinomiya, each fighting tooth and nail to trick the other into confessing their romantic love. Kaguya struggles within the strict confines of her wealthy, uptight family, rebelling against her cold default demeanor as she warms to Shirogane and the rest of her friends.<br>\n<br>\nMeanwhile, council treasurer Yuu Ishigami suffers under the weight of his hopeless crush on Tsubame Koyasu, a popular upperclassman who helps to instill a new confidence in him. Miko Iino, the newest student council member, grows closer to the rule-breaking Ishigami while striving to overcome her own authoritarian moral code.<br>\n<br>\nAs love further blooms at Shuchiin Academy, the student council officers drag their outsider friends into increasingly comedic conflicts.<br>\n<br>\n(Source: MAL Rewrite)<br>\n<br>\n<i>Note: The first episode had an advanced screening on April 2, in both New York & Los Angeles.<br>",
+ "rating": 90,
+ "genres": ["Comedy", "Psychological", "Romance", "Slice of Life"],
+ "color": "#d6f1a1",
+ "totalEpisodes": 13,
+ "currentEpisodeCount": 13,
+ "type": "TV",
+ "releaseDate": 2022
+ },
+ {
+ "id": "112641",
+ "malId": 40591,
+ "title": {
+ "romaji": "Kaguya-sama wa Kokurasetai?: Tensaitachi no Renai Zunousen",
+ "english": "Kaguya-sama: Love is War?",
+ "native": "かぐや様は告らせたい?~天才たちの恋愛頭脳戦~",
+ "userPreferred": "Kaguya-sama wa Kokurasetai?: Tensaitachi no Renai Zunousen"
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx112641-zoGC8d6FaPXU.jpg",
+ "cover": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/112641-mKZe0zng0ndV.jpg",
+ "popularity": 303429,
+ "description": "After a slow but eventful summer vacation, Shuchiin Academy's second term is now starting in full force. As August transitions into September, Miyuki Shirogane's birthday looms ever closer, leaving Kaguya Shinomiya in a serious predicament as to how to celebrate it. Furthermore, the tenure of the school's 67th student council is coming to an end. Due to the council members being in different classes, the only time Kaguya and Miyuki have to be together will soon disappear, putting all of their cunning plans at risk.<br></br>\n\nA long and difficult election that will decide the fate of the new student council awaits, as multiple challengers fight for the coveted title of president.<br></br>\n",
+ "rating": 86,
+ "genres": ["Comedy", "Psychological", "Romance", "Slice of Life"],
+ "color": "#DAD797",
+ "totalEpisodes": 12,
+ "currentEpisodeCount": 12,
+ "type": "TV",
+ "releaseDate": 2020
+ },
+ {
+ "id": "101921",
+ "malId": 37999,
+ "title": {
+ "romaji": "Kaguya-sama wa Kokurasetai: Tensaitachi no Renai Zunousen",
+ "english": "Kaguya-sama: Love is War",
+ "native": "かぐや様は告らせたい~天才たちの恋愛頭脳戦~",
+ "userPreferred": "Kaguya-sama wa Kokurasetai: Tensaitachi no Renai Zunousen"
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx101921-VvdGQy1ZySYf.jpg",
+ "cover": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/101921-GgvvFhlNhzlF.jpg",
+ "popularity": 378107,
+ "description": "Known for being both brilliant and powerful, Miyuki Shirogane and Kaguya Shinomiya lead the illustrious Shuchiin Academy as near equals. And everyone thinks they’d make a great couple. Pride and arrogance are in ample supply, so the only logical move is to trick the other into instigating a date! Who will come out on top in this psychological war where the first move is the only one that matters?\n<br><br>\n(Source: Aniplex)",
+ "rating": 83,
+ "genres": ["Comedy", "Psychological", "Romance", "Slice of Life"],
+ "color": "#e45086",
+ "totalEpisodes": 12,
+ "currentEpisodeCount": 12,
+ "type": "TV",
+ "releaseDate": 2019
+ },
+ {
+ "id": "125368",
+ "malId": 43609,
+ "title": {
+ "romaji": "Kaguya-sama wa Kokurasetai: Tensaitachi no Renai Zunousen OVA",
+ "english": null,
+ "native": "かぐや様は告らせたい~天才たちの恋愛頭脳戦~OVA",
+ "userPreferred": "Kaguya-sama wa Kokurasetai: Tensaitachi no Renai Zunousen OVA"
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx125368-QhcBkbNP0ZfU.png",
+ "cover": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/125368-zCY0WnSQl7RG.jpg",
+ "popularity": 63453,
+ "description": "OVA adapting both chapters of <i>Kaguya-sama wa Kokurasetai Darkness</i> and Chapter 96 of the main series.",
+ "rating": 75,
+ "genres": ["Comedy", "Ecchi", "Romance", "Slice of Life"],
+ "color": "#c97843",
+ "totalEpisodes": 1,
+ "currentEpisodeCount": 1,
+ "type": "OVA",
+ "releaseDate": 2021
+ },
+ {
+ "id": "151384",
+ "malId": 52198,
+ "title": {
+ "romaji": "Kaguya-sama wa Kokurasetai: First Kiss wa Owaranai",
+ "english": "Kaguya-sama: Love is War -The First Kiss That Never Ends-",
+ "native": "かぐや様は告らせたい -ファーストキッスは終わらない-",
+ "userPreferred": "Kaguya-sama wa Kokurasetai: First Kiss wa Owaranai"
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151384-gv0q8wOE6D58.jpg",
+ "cover": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/151384-JybfIpHr2gx6.jpg",
+ "popularity": 68747,
+ "description": "Shuchiin Academy’s student council room: the place where Student Council Vice President Kaguya Shinomiya and President Miyuki Shirogane met. After a long battle in love, these two geniuses communicated their feelings and, at the Hoshin Festival, had their very first kiss. However, there was no clear confession of love. The relationship between these two, who assumed they would be a couple, remains ambiguous. Now, overly conscious of their feelings, they must face the biggest challenge yet: Christmas. It’s Shirogane who wants it to be perfect versus Kaguya who pursues the imperfect situation. This is the very “normal” love story of two geniuses and the first kiss that never ends.\n<br><br>\n(Source: Aniplex of America)\n<br><br>\n<em>Kaguya-sama: Love is War -The First Kiss That Never Ends- first premiered in theaters across Japan on December 17, 2022. It was later released as 4 episodes on TV, streaming, and Blu-Ray/DVD.<em>",
+ "rating": 88,
+ "genres": ["Comedy", "Psychological", "Romance", "Slice of Life"],
+ "color": "#e45d78",
+ "totalEpisodes": 4,
+ "currentEpisodeCount": 4,
+ "type": "TV",
+ "releaseDate": 2023
+ },
+ {
+ "id": "3322",
+ "malId": 3322,
+ "title": {
+ "romaji": "Wagaya no Oinari-sama.",
+ "english": "Our Home's Fox Deity",
+ "native": "我が家のお稲荷さま。",
+ "userPreferred": "Wagaya no Oinari-sama."
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3322.jpg",
+ "cover": null,
+ "popularity": 4111,
+ "description": "The Mizuchi bloodline has long been hunted by Yokai, or monsters. Toru and Noboru Takagami are descendents of this bloodline, and under their grandmother's discretion, are given a secret weapon to combat these monsters. It is Tenko Kugen, a fox deity who can take the shape of a man or woman at will. The mischievous deity is accompanied by a shrine maiden, Ko, who will both live with the Takagami brothers at their house. Life just got complicated. <br><br>\n(Source: NIS America) ",
+ "rating": 66,
+ "genres": ["Adventure", "Fantasy", "Supernatural"],
+ "color": "#f1a150",
+ "totalEpisodes": 24,
+ "currentEpisodeCount": 24,
+ "type": "TV",
+ "releaseDate": 2008
+ },
+ {
+ "id": "5484",
+ "malId": 5484,
+ "title": {
+ "romaji": "Wagaya no Oinari-sama. Specials",
+ "english": null,
+ "native": "我が家のお稲荷さま。",
+ "userPreferred": "Wagaya no Oinari-sama. Specials"
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5484.jpg",
+ "cover": null,
+ "popularity": 458,
+ "description": "Short comedy sketches involving Tenko &amp; Kou.",
+ "rating": 55,
+ "genres": ["Adventure", "Comedy", "Fantasy", "Supernatural"],
+ "color": "#aed678",
+ "totalEpisodes": 12,
+ "currentEpisodeCount": 12,
+ "type": "SPECIAL",
+ "releaseDate": 2008
+ },
+ {
+ "id": "165557",
+ "malId": 50325,
+ "title": {
+ "romaji": "Kaguya-sama wa Kokurasetai: Ultra Romantic 3rd Season Teaser PV - Ishigami Yuu wa Kataritai",
+ "english": "Kaguya-sama: Love is War -Ultra Romantic- \"Yu Ishigami Wants to Chat\"",
+ "native": "第3期『かぐや様は告らせたい-ウルトラロマンティック-』 / ティザーPV 「石上優は語りたい」",
+ "userPreferred": "Kaguya-sama wa Kokurasetai: Ultra Romantic 3rd Season Teaser PV - Ishigami Yuu wa Kataritai"
+ },
+ "status": "Completed",
+ "image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b165557-q7A6HcZD0CHs.png",
+ "cover": null,
+ "popularity": 2412,
+ "description": "Every Thursday, Shuchiin Academy student council secretary Yuu Ishigami can be found in a state of anxiety. He counts down the hours until he can escape to the student council chamber to read the latest chapter of the popular seinen manga series, \"Momo Doesn't Think.\" Joining Ishigami in his weekly reading, student council president Miyuki Shirogane swiftly grows excited at the news of a third season of the manga's anime adaptation.<br><br>\n\nHowever, it is only a matter of time before vice president Kaguya Shinomiya discovers what has been engrossing the two boys. With the stigma of being labeled an otaku looming over his head, Shirogane must do everything he can to keep his embarrassing secret from coming to light.<br><br>\n\n(Source: MAL Rewrite)<br><br>\n\n<i>Note: This short PV adapts the 110th chapter of the manga and serves as a teaser for the April 2022 release of Kaguya-sama wa Kokurasetai: Ultra Romantic.</i>",
+ "rating": 77,
+ "genres": ["Comedy", "Romance"],
+ "color": "#f1d6c9",
+ "totalEpisodes": 1,
+ "currentEpisodeCount": 1,
+ "type": "ONA",
+ "releaseDate": 2021
+ }
+ ]
+}
diff --git a/components/home/content.js b/components/home/content.js
index 9d41fe9..f13c7a8 100644
--- a/components/home/content.js
+++ b/components/home/content.js
@@ -10,10 +10,17 @@ import {
import { parseCookies } from "nookies";
import { ChevronLeftIcon } from "@heroicons/react/20/solid";
-import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
+import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/router";
-export default function Content({ ids, section, data, og, userName }) {
+export default function Content({
+ ids,
+ section,
+ data,
+ userData,
+ og,
+ userName,
+}) {
const router = useRouter();
const [startX, setStartX] = useState(null);
@@ -115,6 +122,9 @@ export default function Content({ ids, section, data, og, userName }) {
filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData;
const goToPage = () => {
+ if (section === "Recently Watched") {
+ router.push(`/${lang}/anime/recently-watched`);
+ }
if (section === "Trending Now") {
router.push(`/${lang}/anime/trending`);
}
@@ -159,95 +169,184 @@ export default function Content({ ids, section, data, og, userName }) {
onClick={handleClick}
ref={containerRef}
>
- {slicedData?.map((anime) => {
- const progress = og?.find((i) => i.mediaId === anime.id);
-
- return (
- <div
- key={anime.id}
- className="flex flex-col gap-3 shrink-0 cursor-pointer"
- >
- <Link
- href={`/${lang}/anime/${anime.id}`}
- className="hover:scale-105 hover:shadow-lg group relative duration-300 ease-out"
- >
- {ids === "onGoing" && (
- <div className="h-[190px] lg:h-[265px] w-[135px] lg:w-[185px] bg-gradient-to-b from-transparent to-black absolute z-40 rounded-md whitespace-normal font-karla group">
- <div className="flex flex-col items-center h-full justify-end text-center pb-5">
- <h1 className="line-clamp-1 w-[70%] text-[10px]">
- {anime.title.romaji || anime.title.english}
- </h1>
- {checkProgress(progress) &&
- !clicked?.hasOwnProperty(anime.id) && (
- <ExclamationCircleIcon className="w-7 h-7 absolute z-40 -top-3 -right-3" />
- )}
- {checkProgress(progress) && (
- <div
- onClick={() => handleAlert(anime.id)}
- className="group-hover:visible invisible absolute top-0 bg-black bg-opacity-20 w-full h-full z-20 text-center"
- >
- <h1 className="text-[12px] lg:text-sm pt-28 lg:pt-44 font-bold opacity-100">
- {checkProgress(progress)}
- </h1>
- </div>
- )}
- {anime.nextAiringEpisode && (
- <div className="flex gap-1 text-[13px] lg:text-base">
- <h1>
- Episode {anime.nextAiringEpisode.episode} in
+
+ {ids !== "recentlyWatched"
+ ? slicedData?.map((anime) => {
+ const progress = og?.find((i) => i.mediaId === anime.id);
+
+ return (
+ <div
+ key={anime.id}
+ className="flex flex-col gap-3 shrink-0 cursor-pointer"
+ >
+ <Link
+ href={`/${lang}/anime/${anime.id}`}
+ className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative"
+ title={anime.title.romaji}
+ >
+ {ids === "onGoing" && (
+ <div className="h-[190px] lg:h-[265px] w-[135px] lg:w-[185px] bg-gradient-to-b from-transparent to-black absolute z-40 rounded-md whitespace-normal font-karla group">
+ <div className="flex flex-col items-center h-full justify-end text-center pb-5">
+ <h1 className="line-clamp-1 w-[70%] text-[10px]">
+ {anime.title.romaji || anime.title.english}
</h1>
- <h1 className="font-bold">
- {convertSecondsToTime(
- anime?.nextAiringEpisode?.timeUntilAiring
+ {checkProgress(progress) &&
+ !clicked?.hasOwnProperty(anime.id) && (
+ <ExclamationCircleIcon className="w-7 h-7 absolute z-40 -top-3 -right-3" />
)}
- </h1>
+ {checkProgress(progress) && (
+ <div
+ onClick={() => handleAlert(anime.id)}
+ className="group-hover:visible invisible absolute top-0 bg-black bg-opacity-20 w-full h-full z-20 text-center"
+ >
+ <h1 className="text-[12px] lg:text-sm pt-28 lg:pt-44 font-bold opacity-100">
+ {checkProgress(progress)}
+ </h1>
+ </div>
+ )}
+ {anime.nextAiringEpisode && (
+ <div className="flex gap-1 text-[13px] lg:text-base">
+ <h1>
+ Episode {anime.nextAiringEpisode.episode} in
+ </h1>
+ <h1 className="font-bold">
+ {convertSecondsToTime(
+ anime?.nextAiringEpisode?.timeUntilAiring
+ )}
+ </h1>
+ </div>
+ )}
</div>
+ </div>
+ )}
+ <Image
+ draggable={false}
+ src={
+ anime.image ||
+ anime.coverImage?.extraLarge ||
+ anime.coverImage?.large ||
+ "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
+ }
+ alt={
+ anime.title.romaji ||
+ anime.title.english ||
+ "coverImage"
+ }
+ width={500}
+ height={300}
+ placeholder="blur"
+ blurDataURL={
+ anime.image ||
+ anime.coverImage?.extraLarge ||
+ anime.coverImage?.large ||
+ "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
+ }
+ className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90"
+ />
+ </Link>
+ {ids !== "onGoing" && (
+ <Link
+ href={`/en/anime/${anime.id}`}
+ className="w-[135px] lg:w-[185px] line-clamp-2"
+ title={anime.title.romaji}
+ >
+ <h1 className="font-karla font-semibold xl:text-base text-[15px]">
+ {anime.status === "RELEASING" ? (
+ <span className="dots bg-green-500" />
+ ) : anime.status === "NOT_YET_RELEASED" ? (
+ <span className="dots bg-red-500" />
+ ) : null}
+ {anime.title.romaji}
+ </h1>
+ </Link>
+ )}
+ </div>
+ );
+ })
+ : userData
+ ?.filter((i) => i.title && i.title !== null)
+ ?.slice(0, 10)
+ .map((i) => {
+ const time = i.timeWatched;
+ const duration = i.duration;
+ let prog = (time / duration) * 100;
+ if (prog > 90) prog = 100;
+
+ return (
+ <Link
+ key={i.watchId}
+ className="flex flex-col gap-2 shrink-0 cursor-pointer"
+ href={`/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ >
+ <div className="relative w-[320px] aspect-video rounded-md overflow-hidden group">
+ <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" />
+ <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30">
+ <PlayIcon className="w-5 h-5 shrink-0" />
+ <h1
+ className="font-semibold font-karla line-clamp-1"
+ title={i?.title || i.anititle}
+ >
+ {i?.title === i.aniTitle
+ ? `Episode ${i.episode}`
+ : i?.title || i.anititle}
+ </h1>
+ </div>
+ <span
+ className={`absolute bottom-0 left-0 h-[2px] bg-red-600 z-30`}
+ style={{
+ width: `${prog}%`,
+ }}
+ />
+ {i?.image && (
+ <Image
+ src={i?.image}
+ width={200}
+ height={200}
+ alt="Episode Thumbnail"
+ className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10"
+ />
)}
</div>
- </div>
- )}
- <Image
- draggable={false}
- src={
- anime.image ||
- anime.coverImage?.extraLarge ||
- anime.coverImage?.large ||
- "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
- }
- alt={
- anime.title.romaji || anime.title.english || "coverImage"
- }
- width={500}
- height={300}
- placeholder="blur"
- blurDataURL={
- anime.image ||
- anime.coverImage?.extraLarge ||
- anime.coverImage?.large ||
- "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
- }
- className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90"
- />
- </Link>
- {ids !== "onGoing" && (
- <Link
- href={`/en/anime/${anime.id}`}
- className="w-[135px] lg:w-[185px] line-clamp-2"
- >
- <h1 className="font-karla font-semibold xl:text-base text-[15px]">
- {anime.status === "RELEASING" ? (
- <span className="dots bg-green-500" />
- ) : anime.status === "NOT_YET_RELEASED" ? (
- <span className="dots bg-red-500" />
- ) : null}
- {anime.title.romaji}
- </h1>
- </Link>
- )}
+
+ <div className="flex flex-col font-karla w-full">
+ {/* <h1 className="font-semibold">{i.title}</h1> */}
+ <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]">
+ <span
+ className="text-white"
+ style={{
+ display: "inline-block",
+ maxWidth: "220px",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ }}
+ title={i.aniTitle}
+ >
+ {i.aniTitle}
+ </span>{" "}
+ | Episode {i.episode}
+ </p>
+ </div>
+ </Link>
+ );
+ })}
+ {userData?.length >= 10 && section !== "Recommendations" && (
+ <div
+ key={section}
+ className="flex cursor-pointer"
+ onClick={goToPage}
+ >
+ <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200">
+ <h1 className="whitespace-pre-wrap text-sm">
+ More on {section}
+ </h1>
+ <ArrowRightCircleIcon className="w-5 h-5" />
</div>
- );
- })}
- {filteredData.length >= 10 && section !== "Recommendations" && (
+ </div>
+ )}
+ {filteredData?.length >= 10 && section !== "Recommendations" && (
<div
key={section}
className="flex cursor-pointer"
diff --git a/components/home/schedule.js b/components/home/schedule.js
index 187fa17..73c63f0 100644
--- a/components/home/schedule.js
+++ b/components/home/schedule.js
@@ -37,6 +37,8 @@ export default function Schedule({ data, scheduleData, time }) {
setCurrentPage(todayIndex >= 0 ? todayIndex : 0);
}, [currentDay, days]);
+ // console.log({ scheduleData });
+
return (
<div className="flex flex-col gap-5 px-4 lg:px-0">
<h1 className="font-bold font-karla text-[20px] lg:px-5">
diff --git a/components/listEditor.js b/components/listEditor.js
index 49aa38e..fa249e3 100644
--- a/components/listEditor.js
+++ b/components/listEditor.js
@@ -1,11 +1,22 @@
import { useState } from "react";
import Image from "next/image";
import { toast } from "react-toastify";
+import { useRouter } from "next/router";
-const ListEditor = ({ animeId, session, stats, prg, max, image = null }) => {
+const ListEditor = ({
+ animeId,
+ session,
+ stats,
+ prg,
+ max,
+ image = null,
+ close,
+}) => {
const [status, setStatus] = useState(stats ?? "CURRENT");
const [progress, setProgress] = useState(prg ?? 0);
+ const router = useRouter();
+
const handleSubmit = async (e) => {
e.preventDefault();
console.log("Submitting", status?.name, progress);
@@ -57,9 +68,11 @@ const ListEditor = ({ animeId, session, stats, prg, max, image = null }) => {
draggable: true,
theme: "dark",
});
+ close();
setTimeout(() => {
- window.location.reload();
- }, 3000);
+ // window.location.reload();
+ router.reload();
+ }, 1000);
// showAlert("Media list entry saved", "success");
} catch (error) {
toast.error("Something went wrong", {
diff --git a/components/videoPlayer.js b/components/videoPlayer.js
index 3709fd0..8f08fd3 100644
--- a/components/videoPlayer.js
+++ b/components/videoPlayer.js
@@ -2,6 +2,7 @@ import Player from "../lib/Artplayer";
import { useEffect, useState } from "react";
import { useAniList } from "../lib/anilist/useAnilist";
import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
+import { useRouter } from "next/router";
const fontSize = [
{
@@ -31,17 +32,23 @@ export default function VideoPlayer({
proxy,
provider,
track,
+ aniTitle,
+ timeWatched,
}) {
const [url, setUrl] = useState("");
const [source, setSource] = useState([]);
const { markProgress } = useAniList(session);
+ const router = useRouter();
+
const [resolution, setResolution] = useState("auto");
const [subSize, setSubSize] = useState({ size: "16px", html: "Small" });
const [defSize, setDefSize] = useState();
const [subtitle, setSubtitle] = useState();
const [defSub, setDefSub] = useState();
+ const [autoPlay, setAutoPlay] = useState(false);
+
useEffect(() => {
const resol = localStorage.getItem("quality");
const sub = JSON.parse(localStorage.getItem("subSize"));
@@ -127,7 +134,7 @@ export default function VideoPlayer({
option={{
url: `${url}`,
title: `${title}`,
- autoplay: true,
+ autoplay: false,
screenshot: true,
moreVideoAttr: {
crossOrigin: "anonymous",
@@ -136,9 +143,6 @@ export default function VideoPlayer({
...(provider !== "gogoanime" && {
plugins: [
artplayerPluginHlsQuality({
- // Show quality in control
- // control: true,
-
// Show quality in setting
setting: true,
@@ -179,6 +183,8 @@ export default function VideoPlayer({
subtitles={subtitle}
provider={provider}
track={track}
+ autoplay={autoPlay}
+ setautoplay={setAutoPlay}
style={{
width: "100%",
height: "100%",
@@ -186,19 +192,21 @@ export default function VideoPlayer({
}}
getInstance={(art) => {
art.on("ready", () => {
- // console.log(art.storage.settings);
const seek = art.storage.get(id);
- const seekTime = seek?.time || 0;
+ const seekTime = seek?.timeWatched || 0;
const duration = art.duration;
const percentage = seekTime / duration;
+ const percentagedb = timeWatched / duration;
if (subSize) {
art.subtitle.style.fontSize = subSize?.size;
}
- if (percentage >= 0.9) {
+ if (percentage >= 0.9 || percentagedb >= 0.9) {
art.currentTime = 0;
console.log("Video started from the beginning");
+ } else if (timeWatched) {
+ art.currentTime = timeWatched;
} else {
art.currentTime = seekTime;
}
@@ -206,36 +214,129 @@ export default function VideoPlayer({
let marked = 0;
- art.on("video:timeupdate", () => {
+ art.on("video:playing", () => {
if (!session) return;
- const mediaSession = navigator.mediaSession;
- const currentTime = art.currentTime;
- const duration = art.duration;
- const percentage = currentTime / duration;
+ const intervalId = setInterval(async () => {
+ const resp = await fetch("/api/user/update/episode", {
+ method: "PUT",
+ body: JSON.stringify({
+ name: session?.user?.name,
+ id: String(aniId),
+ watchId: id,
+ title: track?.playing?.title || aniTitle,
+ aniTitle: aniTitle,
+ image: track?.playing?.image,
+ number: track?.playing?.number,
+ duration: art.duration,
+ timeWatched: art.currentTime,
+ provider: provider,
+ }),
+ });
+ // console.log("updating db");
+ }, 5000);
+
+ art.on("video:pause", () => {
+ clearInterval(intervalId);
+ });
+
+ art.on("video:ended", () => {
+ clearInterval(intervalId);
+ });
+
+ art.on("destroy", () => {
+ clearInterval(intervalId);
+ // console.log("clearing interval");
+ });
+ });
+
+ art.on("video:playing", () => {
+ const interval = setInterval(async () => {
+ art.storage.set(id, {
+ aniId: String(aniId),
+ watchId: id,
+ title: track?.playing?.title || aniTitle,
+ aniTitle: aniTitle,
+ image: track?.playing?.image,
+ episode: track?.playing?.number,
+ duration: art.duration,
+ timeWatched: art.currentTime,
+ provider: provider,
+ createdAt: new Date().toISOString(),
+ });
+ }, 5000);
+
+ art.on("video:pause", () => {
+ clearInterval(interval);
+ });
+
+ art.on("video:ended", () => {
+ clearInterval(interval);
+ });
- mediaSession.setPositionState({
- duration: art.duration,
- playbackRate: art.playbackRate,
- position: art.currentTime,
+ art.on("destroy", () => {
+ clearInterval(interval);
});
+ });
+
+ art.on("video:timeupdate", async () => {
+ if (!session) return;
+
+ var currentTime = art.currentTime;
+ const duration = art.duration;
+ const percentage = currentTime / duration;
if (percentage >= 0.9) {
// use >= instead of >
if (marked < 1) {
marked = 1;
markProgress(aniId, progress, stats);
- // console.log("Video progress marked");
}
}
});
+ art.on("video:ended", () => {
+ if (!track?.next) return;
+ if (localStorage.getItem("autoplay") === "true") {
+ art.controls.add({
+ name: "next-button",
+ position: "top",
+ html: '<div class="vid-con"><button class="next-button progress">Play Next</button></div>',
+ click: function (...args) {
+ if (track?.next) {
+ router.push(
+ `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent(
+ track?.next?.id
+ )}&num=${track?.next?.number}`
+ );
+ }
+ },
+ });
+
+ const button = document.querySelector(".next-button");
+
+ function stopTimeout() {
+ clearTimeout(timeoutId);
+ button.classList.remove("progress");
+ }
+
+ let timeoutId = setTimeout(() => {
+ art.controls.remove("next-button");
+ if (track?.next) {
+ router.push(
+ `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent(
+ track?.next?.id
+ )}&num=${track?.next?.number}`
+ );
+ }
+ }, 7000);
+
+ button.addEventListener("mouseover", stopTimeout);
+ }
+ });
+
art.on("video:timeupdate", () => {
var currentTime = art.currentTime;
// console.log(art.currentTime);
- art.storage.set(id, {
- time: art.currentTime,
- duration: art.duration,
- });
if (
skip?.op &&
diff --git a/lib/Artplayer.js b/lib/Artplayer.js
index 94ceff1..96afe2b 100644
--- a/lib/Artplayer.js
+++ b/lib/Artplayer.js
@@ -13,9 +13,17 @@ export default function Player({
getInstance,
id,
track,
+ // socket
+ socket,
+ isPlay,
+ watchdata,
+ room,
+ autoplay,
+ setautoplay,
...rest
}) {
const artRef = useRef();
+
const router = useRouter();
function playM3u8(video, url, art) {
@@ -70,6 +78,17 @@ export default function Player({
},
],
settings: [
+ {
+ html: "Autoplay",
+ // icon: '<img width="22" heigth="22" src="/assets/img/state.svg">',
+ tooltip: "ON/OFF",
+ switch: localStorage.getItem("autoplay") === "true" ? true : false,
+ onSwitch: function (item) {
+ setautoplay(!item.switch);
+ localStorage.setItem("autoplay", !item.switch);
+ return !item.switch;
+ },
+ },
provider === "zoro" && {
html: "Subtitles",
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="28" viewBox="0 -960 960 960"><path d="M240-350h360v-60H240v60zm420 0h60v-60h-60v60zM240-470h60v-60h-60v60zm120 0h360v-60H360v60zM140-160q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42v520q0 24-18 42t-42 18H140zm0-60h680v-520H140v520zm0 0v-520 520z"></path></svg>',
diff --git a/lib/anify/info.js b/lib/anify/info.js
new file mode 100644
index 0000000..8978664
--- /dev/null
+++ b/lib/anify/info.js
@@ -0,0 +1,33 @@
+import axios from "axios";
+import cacheData from "memory-cache";
+
+export async function fetchInfo(id, key) {
+ try {
+ const { data } = await axios.get(
+ `https://api.anify.tv/info/${id}?apikey=${key}`
+ );
+ return data;
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function getAnifyInfo(id, key) {
+ try {
+ const cached = cacheData.get(id);
+ if (cached) {
+ return cached;
+ } else {
+ const data = await fetchInfo(id, key);
+ if (data) {
+ cacheData.put(id, data, 1000 * 60 * 10);
+ return data;
+ } else {
+ return { message: "Schedule not found" };
+ }
+ }
+ } catch (error) {
+ return { error };
+ }
+}
diff --git a/lib/anify/page.js b/lib/anify/page.js
new file mode 100644
index 0000000..6361230
--- /dev/null
+++ b/lib/anify/page.js
@@ -0,0 +1,39 @@
+import cacheData from "memory-cache";
+
+// Function to fetch new data
+async function fetchData(id, providerId, chapterId, key) {
+ try {
+ const res = await fetch(
+ `https://api.anify.tv/pages?id=${id}&providerId=${providerId}&readId=${chapterId}&apikey=${key}`
+ );
+ const data = await res.json();
+ return data;
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function getAnifyPage(
+ mediaId,
+ providerId,
+ chapterId,
+ key
+) {
+ try {
+ const cached = cacheData.get(chapterId);
+ if (cached) {
+ return cached;
+ } else {
+ const data = await fetchData(mediaId, providerId, chapterId, key);
+ if (!data.error) {
+ cacheData.put(chapterId, data, 1000 * 60 * 10);
+ return data;
+ } else {
+ return { message: "Manga/Novel not found :(" };
+ }
+ }
+ } catch (error) {
+ return { error };
+ }
+}
diff --git a/lib/prisma.js b/lib/prisma.js
new file mode 100644
index 0000000..7a6e5d7
--- /dev/null
+++ b/lib/prisma.js
@@ -0,0 +1,11 @@
+import { PrismaClient } from "@prisma/client";
+
+const globalForPrisma = globalThis;
+
+const prisma = globalForPrisma.prisma || new PrismaClient();
+
+if (process.env.NODE_ENV !== "production") {
+ globalForPrisma.prisma = prisma;
+}
+
+module.exports = { prisma };
diff --git a/package-lock.json b/package-lock.json
index 0311d19..3684da5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,19 @@
{
"name": "moopa",
- "version": "3.8.7",
+ "version": "3.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "moopa",
- "version": "3.8.7",
+ "version": "3.9.0",
"dependencies": {
"@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.17",
+ "@prisma/client": "^5.1.1",
"@vercel/og": "^0.5.4",
+ "adblock-checker": "^0.1.4",
"artplayer": "^5.0.9",
"artplayer-plugin-hls-quality": "^2.0.0",
"axios": "^1.4.0",
@@ -41,6 +43,7 @@
"depcheck": "^1.4.3",
"eslint": "^8.38.0",
"eslint-config-next": "12.1.6",
+ "prisma": "^5.1.1",
"tailwind-scrollbar": "^2.1.0",
"tailwindcss": "^3.3.1"
}
@@ -2288,6 +2291,38 @@
"url": "https://github.com/sponsors/panva"
}
},
+ "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==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/engines-version": "5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e"
+ },
+ "engines": {
+ "node": ">=16.13"
+ },
+ "peerDependencies": {
+ "prisma": "*"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ }
+ }
+ },
+ "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==",
+ "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=="
+ },
"node_modules/@resvg/resvg-wasm": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.1.tgz",
@@ -2950,6 +2985,14 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/adblock-checker": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/adblock-checker/-/adblock-checker-0.1.4.tgz",
+ "integrity": "sha512-a4X3r3TIhEaBmPb2a8m8BRetDRT1SQ7XBDWh3mXLvCeLpp8fhYlfdssUXqsT0VBzhskBAJwmXWTUlUyvpKXw/w==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3743,6 +3786,12 @@
"node": ">= 8"
}
},
+ "node_modules/crypto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
+ "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
+ "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
+ },
"node_modules/crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -5817,6 +5866,70 @@
"node": ">=8"
}
},
+ "node_modules/jake/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jake/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jake/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jake/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/jake/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jake/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jest-worker": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@@ -5838,6 +5951,14 @@
"node": ">=8"
}
},
+ "node_modules/jest-worker/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jest-worker/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -7090,6 +7211,22 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"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==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/engines": "5.1.1"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=16.13"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
diff --git a/package.json b/package.json
index 51244e1..3d2c0eb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "moopa",
- "version": "3.8.7",
+ "version": "3.9.0",
"private": true,
"founder": "Factiven",
"scripts": {
@@ -14,7 +14,9 @@
"@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.17",
+ "@prisma/client": "^5.1.1",
"@vercel/og": "^0.5.4",
+ "adblock-checker": "^0.1.4",
"artplayer": "^5.0.9",
"artplayer-plugin-hls-quality": "^2.0.0",
"axios": "^1.4.0",
@@ -44,6 +46,7 @@
"depcheck": "^1.4.3",
"eslint": "^8.38.0",
"eslint-config-next": "12.1.6",
+ "prisma": "^5.1.1",
"tailwind-scrollbar": "^2.1.0",
"tailwindcss": "^3.3.1"
}
diff --git a/pages/_document.js b/pages/_document.js
index effc121..d79f31f 100644
--- a/pages/_document.js
+++ b/pages/_document.js
@@ -1,11 +1,15 @@
import { Html, Head, Main, NextScript } from "next/document";
+// import { randomBytes } from "crypto";
export default function Document() {
+ // const nonce = randomBytes(128).toString("base64");
+ // const csp = `object-src 'none'; base-uri 'none'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http: 'nonce-${nonce}' 'strict-dynamic'`;
+
return (
<Html lang="en">
<Head>
<link rel="manifest" href="/manifest.json" />
- <link rel="apple-touch-icon" href="/icon.png"></link>
+ <link rel="apple-touch-icon" href="/icon.png" />
<meta name="theme-color" content="#141519" />
<link
rel="stylesheet"
diff --git a/pages/api/anify/info/[id].js b/pages/api/anify/info/[id].js
new file mode 100644
index 0000000..c33d158
--- /dev/null
+++ b/pages/api/anify/info/[id].js
@@ -0,0 +1,37 @@
+import axios from "axios";
+import cacheData from "memory-cache";
+
+const API_KEY = process.env.API_KEY;
+
+// Function to fetch new data
+export async function fetchInfo(id) {
+ try {
+ const { data } = await axios.get(
+ `https://api.anify.tv/info/${id}?apikey=${API_KEY}`
+ );
+ return data;
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function handler(req, res) {
+ try {
+ const id = req.query.id;
+ const cached = cacheData.get(id);
+ if (cached) {
+ return res.status(200).json(cached);
+ } else {
+ const data = await fetchInfo(id);
+ if (data) {
+ res.status(200).json(data);
+ cacheData.put(id, data, 1000 * 60 * 10);
+ } else {
+ res.status(404).json({ message: "Schedule not found" });
+ }
+ }
+ } catch (error) {
+ res.status(500).json({ error });
+ }
+}
diff --git a/pages/api/anify/page/[...params].js b/pages/api/anify/page/[...params].js
new file mode 100644
index 0000000..80dda6c
--- /dev/null
+++ b/pages/api/anify/page/[...params].js
@@ -0,0 +1,41 @@
+import axios from "axios";
+import cacheData from "memory-cache";
+
+const API_KEY = process.env.API_KEY;
+
+// Function to fetch new data
+async function fetchData(id, providerId, chapterId) {
+ try {
+ const res = await fetch(
+ `https://api.anify.tv/pages?id=${id}&providerId=${providerId}&readId=${chapterId}&apikey=${API_KEY}`
+ );
+ const data = await res.json();
+ return data;
+ // return { id, providerId, chapterId };
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function handler(req, res) {
+ try {
+ const id = req.query.params;
+ const chapter = req.query.chapter;
+ // res.status(200).json({ id, chapter });
+ const cached = cacheData.get(chapter);
+ if (cached) {
+ return res.status(200).json(cached);
+ } else {
+ const data = await fetchData(id[0], id[1], chapter);
+ if (data) {
+ res.status(200).json(data);
+ cacheData.put(id[2], data, 1000 * 60 * 10);
+ } else {
+ res.status(404).json({ message: "Manga/Novel not found :(" });
+ }
+ }
+ } catch (error) {
+ res.status(500).json({ error });
+ }
+}
diff --git a/pages/api/consumet/episode/[id].js b/pages/api/consumet/episode/[id].js
index 737292f..e6f40ce 100644
--- a/pages/api/consumet/episode/[id].js
+++ b/pages/api/consumet/episode/[id].js
@@ -7,12 +7,18 @@ export default async function handler(req, res) {
try {
const id = req.query.id;
const dub = req.query.dub || false;
+ const refresh = req.query.refresh || false;
const providers = ["enime", "gogoanime"];
const datas = [];
const cached = cacheData.get(id + dub);
- if (cached) {
+
+ if (refresh) {
+ cacheData.del(id + dub);
+ }
+
+ if (!refresh && cached) {
return res.status(200).json(cached);
} else {
async function fetchData(provider) {
@@ -31,7 +37,7 @@ export default async function handler(req, res) {
}
return res.json();
});
- if (data.episodes?.length > 0) {
+ if (data.episodes.length > 0) {
datas.push({
providerId: provider,
episodes: dub ? data.episodes : data.episodes.reverse(),
@@ -53,7 +59,7 @@ export default async function handler(req, res) {
if (datas.length === 0) {
return res.status(404).json({ message: "Anime not found" });
} else {
- cacheData.put(id + dub, { data: datas }, 1000 * 60 * 60 * 15); // 15 minutes
+ cacheData.put(id + dub, { data: datas }, 1000 * 60 * 60 * 10);
res.status(200).json({ data: datas });
}
}
diff --git a/pages/api/user/profile.js b/pages/api/user/profile.js
new file mode 100644
index 0000000..2b44ae2
--- /dev/null
+++ b/pages/api/user/profile.js
@@ -0,0 +1,52 @@
+import {
+ createUser,
+ deleteUser,
+ getUser,
+ updateUser,
+} from "../../../prisma/user";
+
+export default async function handler(req, res) {
+ try {
+ switch (req.method) {
+ case "POST": {
+ const { name, setting } = req.body;
+ const new_user = await createUser(name, setting);
+ if (!new_user) {
+ return res.status(200).json({ message: "User is already created" });
+ } else {
+ return res.status(201).json(new_user);
+ }
+ }
+ case "PUT": {
+ const { name, anime } = req.body;
+ const user = await updateUser(name, anime);
+ if (!user) {
+ return res.status(200).json({ message: "Title is already there" });
+ } else {
+ return res.status(200).json(user);
+ }
+ }
+ case "GET": {
+ const { name } = req.query;
+ const user = await getUser(name);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ } else {
+ return res.status(200).json(user);
+ }
+ }
+ case "DELETE": {
+ const { name } = req.body;
+ const user = await deleteUser(name);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ } else {
+ return res.status(200).json(user);
+ }
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ return res.status(500).json({ message: "Internal server error" });
+ }
+}
diff --git a/pages/api/user/update/episode.js b/pages/api/user/update/episode.js
new file mode 100644
index 0000000..f69bb78
--- /dev/null
+++ b/pages/api/user/update/episode.js
@@ -0,0 +1,68 @@
+import {
+ createList,
+ getEpisode,
+ updateUserEpisode,
+} from "../../../../prisma/user";
+
+export default async function handler(req, res) {
+ try {
+ switch (req.method) {
+ case "POST": {
+ const { name, id } = JSON.parse(req.body);
+
+ const episode = await createList(name, id);
+ if (!episode) {
+ return res
+ .status(200)
+ .json({ message: "Episode is already created" });
+ } else {
+ return res.status(201).json(episode);
+ }
+ }
+ case "PUT": {
+ const {
+ name,
+ id,
+ watchId,
+ title,
+ image,
+ number,
+ duration,
+ timeWatched,
+ aniTitle,
+ provider,
+ } = JSON.parse(req.body);
+ const episode = await updateUserEpisode({
+ name,
+ id,
+ watchId,
+ title,
+ image,
+ number,
+ duration,
+ timeWatched,
+ aniTitle,
+ provider,
+ });
+ if (!episode) {
+ return res.status(200).json({ message: "Episode is already there" });
+ } else {
+ return res.status(200).json(episode);
+ }
+ }
+ case "GET": {
+ const { name, id } = req.query;
+ // console.log(req.query);
+ const episode = await getEpisode(name, id);
+ if (!episode) {
+ return res.status(404).json({ message: "Episode not found" });
+ } else {
+ return res.status(200).json(episode);
+ }
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ return res.status(500).json({ message: "Internal server error" });
+ }
+}
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
index 6c78955..5e4aed8 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -154,6 +154,7 @@ export default function Info({ info, color }) {
prg={progress}
max={info?.episodes}
image={info}
+ close={handleClose}
/>
)}
</div>
diff --git a/pages/en/anime/popular.js b/pages/en/anime/popular.js
index b8b19ba..8cbbeab 100644
--- a/pages/en/anime/popular.js
+++ b/pages/en/anime/popular.js
@@ -3,7 +3,6 @@ import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
-import Navbar from "../../../components/navbar";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
@@ -98,7 +97,7 @@ export default function PopularAnime({ sessions }) {
<>
<MobileNav sessions={sessions} />
<div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
- <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed left-3">
+ <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
<ChevronLeftIcon className="w-5 h-5" />
<h1 className="text-xl">Popular Anime</h1>
@@ -110,7 +109,11 @@ export default function PopularAnime({ sessions }) {
key={index}
className="flex flex-col items-center w-[150px] lg:w-[180px]"
>
- <Link href={`/en/anime/${i.id}`} className="p-2">
+ <Link
+ href={`/en/anime/${i.id}`}
+ className="p-2"
+ title={i.title.romaji}
+ >
<Image
src={i.coverImage.large}
alt={i.title.romaji}
@@ -119,7 +122,11 @@ export default function PopularAnime({ sessions }) {
className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded hover:scale-105 scale-100 transition-all duration-200 ease-out"
/>
</Link>
- <Link href={`/en/anime/${i.id}`} className="w-full px-2">
+ <Link
+ href={`/en/anime/${i.id}`}
+ className="w-full px-2"
+ title={i.title.romaji}
+ >
<h1 className="font-karla font-bold xl:text-base text-[15px] line-clamp-2">
{i.status === "RELEASING" ? (
<span className="dots bg-green-500" />
diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js
new file mode 100644
index 0000000..0a7fbae
--- /dev/null
+++ b/pages/en/anime/recently-watched.js
@@ -0,0 +1,159 @@
+import { ChevronLeftIcon, PlayIcon } from "@heroicons/react/24/solid";
+import Image from "next/image";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+import Skeleton from "react-loading-skeleton";
+import Footer from "../../../components/footer";
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../api/auth/[...nextauth]";
+import MobileNav from "../../../components/home/mobileNav";
+
+export default function PopularAnime({ sessions }) {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setLoading(true);
+ const fetchData = async () => {
+ let data;
+ if (sessions?.user?.name) {
+ data = await fetch(
+ `/api/user/profile?name=${sessions?.user?.name}`
+ ).then((res) => {
+ if (!res.ok) {
+ switch (res.status) {
+ case 404: {
+ return console.log("user not found");
+ }
+ case 500: {
+ return console.log("server error");
+ }
+ }
+ }
+ return res.json();
+ });
+ }
+ if (!data) {
+ const dat = JSON.parse(localStorage.getItem("artplayer_settings"));
+ if (dat) {
+ const arr = Object.keys(dat).map((key) => dat[key]);
+ setData(arr);
+ setLoading(false);
+ }
+ } else {
+ setData(data?.WatchListEpisode);
+ setLoading(false);
+ }
+ };
+ fetchData();
+ }, []);
+
+ return (
+ <>
+ <MobileNav sessions={sessions} />
+ <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
+ <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed left-0 px-3">
+ <Link href="/en" className="flex gap-2 items-center font-karla">
+ <ChevronLeftIcon className="w-5 h-5" />
+ <h1 className="text-xl">Recently Watched</h1>
+ </Link>
+ </div>
+ <div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 gap-3 md:gap-7 pt-16">
+ {data
+ ?.filter((i) => i.title !== null)
+ .map((i) => {
+ const time = i.timeWatched;
+ const duration = i.duration;
+ let prog = (time / duration) * 100;
+ if (prog > 90) prog = 100;
+
+ return (
+ <Link
+ key={i.watchId}
+ className="flex flex-col gap-2 shrink-0 cursor-pointer"
+ href={`/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ >
+ <div className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group">
+ <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" />
+ <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30">
+ <PlayIcon className="w-5 h-5 shrink-0" />
+ <h1
+ className="font-semibold text-sm md:text-base font-karla line-clamp-1"
+ title={i?.title || i.anititle}
+ >
+ {i?.title || i.anititle}
+ </h1>
+ </div>
+ <span
+ className={`absolute bottom-0 left-0 h-[2px] bg-red-600 z-30`}
+ style={{
+ width: `${prog}%`,
+ }}
+ />
+ {i?.image && (
+ <Image
+ src={i?.image}
+ width={200}
+ height={200}
+ alt="Episode Thumbnail"
+ className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10"
+ />
+ )}
+ </div>
+ <div className="flex flex-col font-karla w-full">
+ {/* <h1 className="font-semibold">{i.title}</h1> */}
+ <p className="flex items-center gap-1 text-sm text-gray-400 md:w-[320px]">
+ <span
+ className="text-white max-w-[150px] md:max-w-[220px]"
+ style={{
+ display: "inline-block",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ }}
+ title={i.aniTitle}
+ >
+ {i.aniTitle}
+ </span>{" "}
+ | Episode {i.episode}
+ </p>
+ </div>
+ </Link>
+ );
+ })}
+
+ {loading && (
+ <>
+ {[1, 2, 4, 5, 6, 7, 8].map((item) => (
+ <div
+ key={item}
+ className="flex flex-col gap-2 items-center md:w-[320px] rounded-md overflow-hidden"
+ >
+ <div className="w-full">
+ <Skeleton className="w-fit aspect-video rounded" />
+ </div>
+ <div className="w-full">
+ <Skeleton width={80} height={20} />
+ </div>
+ </div>
+ ))}
+ </>
+ )}
+ </div>
+ </div>
+ <Footer />
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ return {
+ props: {
+ sessions: session,
+ },
+ };
+}
diff --git a/pages/en/anime/trending.js b/pages/en/anime/trending.js
index cbc30ab..9f8a187 100644
--- a/pages/en/anime/trending.js
+++ b/pages/en/anime/trending.js
@@ -3,7 +3,6 @@ import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
-import Navbar from "../../../components/navbar";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
@@ -98,7 +97,7 @@ export default function TrendingAnime({ sessions }) {
<>
<MobileNav sessions={sessions} />
<div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
- <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed left-3">
+ <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
<ChevronLeftIcon className="w-5 h-5" />
<h1 className="text-xl">Trending Now</h1>
@@ -110,7 +109,11 @@ export default function TrendingAnime({ sessions }) {
key={index}
className="flex flex-col items-center w-[150px] lg:w-[180px]"
>
- <Link href={`/en/anime/${i.id}`} className="p-2">
+ <Link
+ href={`/en/anime/${i.id}`}
+ className="p-2"
+ title={i.title.romaji}
+ >
<Image
src={i.coverImage.large}
alt={i.title.romaji}
@@ -119,7 +122,11 @@ export default function TrendingAnime({ sessions }) {
className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded hover:scale-105 scale-100 transition-all duration-200 ease-out"
/>
</Link>
- <Link href={`/en/anime/${i.id}`} className="w-full px-2">
+ <Link
+ href={`/en/anime/${i.id}`}
+ className="w-full px-2"
+ title={i.title.romaji}
+ >
<h1 className="font-karla font-bold xl:text-base text-[15px] line-clamp-2">
{i.status === "RELEASING" ? (
<span className="dots bg-green-500" />
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index 67e38c2..bc8851b 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -9,6 +9,8 @@ import Navigasi from "../../../../components/home/staticNav";
import PrimarySide from "../../../../components/anime/watch/primarySide";
import SecondarySide from "../../../../components/anime/watch/secondarySide";
import { GET_MEDIA_USER } from "../../../../queries";
+import { createList, createUser, getEpisode } from "../../../../prisma/user";
+// import { updateUser } from "../../../../prisma/user";
export default function Info({
sessions,
@@ -17,6 +19,7 @@ export default function Info({
provider,
epiNumber,
dub,
+ userData,
proxy,
disqus,
}) {
@@ -124,7 +127,7 @@ export default function Info({
}
}
}
-
+
setInfo(data.data.Media);
const response = await fetch(
@@ -156,12 +159,16 @@ export default function Info({
setLoading(false);
}
}
-
+
setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
// setEpiData(episodes);
setLoading(false);
}
getInfo();
+
+ return () => {
+ setCurrentEpisode(null);
+ };
}, [sessions?.user?.name, epiNumber, dub]);
// console.log(proxy);
@@ -190,6 +197,7 @@ export default function Info({
setOnList={setOnList}
setLoading={setLoading}
loading={loading}
+ timeWatched={userData?.timeWatched}
/>
<SecondarySide
info={info}
@@ -227,6 +235,22 @@ export async function getServerSideProps(context) {
const epiNumber = query.num;
const dub = query.dub;
+ let userData = null;
+
+ 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;
+ })
+ );
+ }
+
return {
props: {
sessions: session,
@@ -235,6 +259,7 @@ export async function getServerSideProps(context) {
watchId: watchId || null,
epiNumber: epiNumber || null,
dub: dub || null,
+ userData: userData?.[0] || null,
proxy,
disqus,
},
diff --git a/pages/en/index.js b/pages/en/index.js
index 35de96d..c3a98fc 100644
--- a/pages/en/index.js
+++ b/pages/en/index.js
@@ -21,17 +21,40 @@ import { useCountdown } from "../../utils/useCountdownSeconds";
import Navigasi from "../../components/home/staticNav";
import MobileNav from "../../components/home/mobileNav";
import axios from "axios";
+import { createUser } from "../../prisma/user";
-// Filter schedules for each day
-// const filterByCountryOfOrigin = (schedule, country) => {
-// const filteredSchedule = {};
-// for (const day in schedule) {
-// filteredSchedule[day] = schedule[day].filter(
-// (anime) => anime.countryOfOrigin === country
-// );
-// }
-// return filteredSchedule;
-// };
+import { checkAdBlock } from "adblock-checker";
+import { ToastContainer, toast } from "react-toastify";
+
+export async function getServerSideProps(context) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ if (session) {
+ await createUser(session.user.name);
+ }
+
+ const trendingDetail = await aniListData({
+ sort: "TRENDING_DESC",
+ page: 1,
+ });
+ const popularDetail = await aniListData({
+ sort: "POPULARITY_DESC",
+ page: 1,
+ });
+ const genreDetail = await aniListData({ sort: "TYPE", page: 1 });
+
+ const upComing = await getUpcomingAnime();
+
+ return {
+ props: {
+ genre: genreDetail.props,
+ detail: trendingDetail.props,
+ populars: popularDetail.props,
+ sessions: session,
+ upComing,
+ },
+ };
+}
export default function Home({ detail, populars, sessions, upComing }) {
const { media: current } = useAniList(sessions, { stats: "CURRENT" });
@@ -42,6 +65,27 @@ export default function Home({ detail, populars, sessions, upComing }) {
const [anime, setAnime] = useState([]);
+ useEffect(() => {
+ async function adBlock() {
+ const ad = await checkAdBlock();
+ if (ad) {
+ toast.dark(
+ "Please disable your adblock for better experience, we don't have any ads on our site.",
+ {
+ position: "top-center",
+ autoClose: false,
+ hideProgressBar: true,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ theme: "dark",
+ }
+ );
+ }
+ }
+ adBlock();
+ }, []);
+
const update = () => {
setAnime((prevAnime) => prevAnime.slice(1));
};
@@ -89,6 +133,9 @@ export default function Home({ detail, populars, sessions, upComing }) {
const [list, setList] = useState(null);
const [planned, setPlanned] = useState(null);
const [greeting, setGreeting] = useState("");
+ const [user, setUser] = useState(null);
+
+ // console.log({ user });
const [prog, setProg] = useState(null);
@@ -96,6 +143,43 @@ export default function Home({ detail, populars, sessions, upComing }) {
const data = detail.data[0];
useEffect(() => {
+ async function userData() {
+ let data;
+ if (sessions?.user?.name) {
+ data = await fetch(
+ `/api/user/profile?name=${sessions?.user?.name}`
+ ).then((res) => {
+ if (!res.ok) {
+ switch (res.status) {
+ case 404: {
+ return console.log("user not found");
+ }
+ case 500: {
+ return console.log("server error");
+ }
+ }
+ }
+ return res.json();
+ });
+ }
+ if (!data) {
+ const dat = JSON.parse(localStorage.getItem("artplayer_settings"));
+ if (dat) {
+ const arr = Object.keys(dat).map((key) => dat[key]);
+ const newFirst = arr?.sort((a, b) => {
+ return new Date(b?.createdAt) - new Date(a?.createdAt);
+ });
+ setUser(newFirst);
+ }
+ } else {
+ setUser(data?.WatchListEpisode);
+ }
+ // const data = await res.json();
+ }
+ userData();
+ }, [sessions?.user?.name]);
+
+ useEffect(() => {
const time = new Date().getHours();
let greeting = "";
@@ -112,7 +196,8 @@ export default function Home({ detail, populars, sessions, upComing }) {
setGreeting(greeting);
async function userData() {
- if (!sessions) return;
+ if (!sessions?.user?.name) return;
+
const getMedia =
current.filter((item) => item.status === "CURRENT")[0] || null;
const list = getMedia?.entries
@@ -131,7 +216,8 @@ export default function Home({ detail, populars, sessions, upComing }) {
}
}
userData();
- }, [sessions, current, plan]);
+ }, [sessions?.user?.name, current, plan]);
+
return (
<>
<Head>
@@ -158,6 +244,13 @@ export default function Home({ detail, populars, sessions, upComing }) {
<div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] ">
<Navigasi />
<SearchBar />
+ <ToastContainer
+ pauseOnFocusLoss={false}
+ style={{
+ width: "400px",
+ }}
+ />
+
{/* PC / TABLET */}
<div className=" hidden justify-center lg:flex my-16">
<div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between">
@@ -228,6 +321,22 @@ export default function Home({ detail, populars, sessions, upComing }) {
animate={{ opacity: 1 }}
transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop
>
+ {user?.length > 0 && (
+ <motion.div // Add motion.div to each child component
+ key="recentlyWatched"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="recentlyWatched"
+ section="Recently Watched"
+ userData={user}
+ />
+ </motion.div>
+ )}
+
{sessions && releaseData?.length > 0 && (
<motion.div // Add motion.div to each child component
key="onGoing"
@@ -354,29 +463,3 @@ export default function Home({ detail, populars, sessions, upComing }) {
</>
);
}
-
-export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
-
- const trendingDetail = await aniListData({
- sort: "TRENDING_DESC",
- page: 1,
- });
- const popularDetail = await aniListData({
- sort: "POPULARITY_DESC",
- page: 1,
- });
- const genreDetail = await aniListData({ sort: "TYPE", page: 1 });
-
- const upComing = await getUpcomingAnime();
-
- return {
- props: {
- genre: genreDetail.props,
- detail: trendingDetail.props,
- populars: popularDetail.props,
- sessions: session,
- upComing,
- },
- };
-}
diff --git a/pages/en/manga/[id].js b/pages/en/manga/[id].js
index eb53a93..bb3cbc2 100644
--- a/pages/en/manga/[id].js
+++ b/pages/en/manga/[id].js
@@ -9,12 +9,12 @@ import { useEffect, useState } from "react";
import { setCookie } from "nookies";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
+import getAnifyInfo from "../../../lib/anify/info";
export default function Manga({ info, userManga, chapters }) {
const [domainUrl, setDomainUrl] = useState("");
const [firstEp, setFirstEp] = useState();
- const chaptersData =
- info.chapters.data.length === 0 ? chapters : info.chapters.data;
+ const chaptersData = info.chapters.data;
useEffect(() => {
setDomainUrl(window.location.origin);
@@ -84,8 +84,7 @@ export async function getServerSideProps(context) {
const { id } = context.query;
const key = process.env.API_KEY;
- const res = await fetch(`https://api.anify.tv/info/${id}?apikey=${key}`);
- const data = await res.json();
+ const data = await getAnifyInfo(id, key);
let userManga = null;
@@ -152,27 +151,10 @@ export async function getServerSideProps(context) {
};
}
- let chapter = null;
-
- if (data?.chapters?.data.length === 0) {
- const res2 = await fetch(
- `https://api.anify.tv/chapters/${id}?apikey=${key}`
- );
- const data2 = await res2.json();
- if (data2.error) {
- return {
- notFound: true
- }
- } else {
- chapter = data2;
- }
- }
-
return {
props: {
info: data,
userManga,
- chapters: chapter || null,
},
};
}
diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js
index dbabb3d..e608d16 100644
--- a/pages/en/manga/read/[...params].js
+++ b/pages/en/manga/read/[...params].js
@@ -15,6 +15,7 @@ import Head from "next/head";
import nookies from "nookies";
import ShortCutModal from "../../../../components/manga/modals/shortcutModal";
import ChapterModal from "../../../../components/manga/modals/chapterModal";
+import getAnifyPage from "../../../../lib/anify/page";
export default function Read({ data, currentId, sessions }) {
const [info, setInfo] = useState();
@@ -228,6 +229,8 @@ export async function getServerSideProps(context) {
const cookies = nookies.get(context);
+ const key = process.env.API_KEY;
+
const query = context.query;
const providerId = query.params[0];
const chapterId = query.chapterId;
@@ -243,18 +246,12 @@ export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
- const key = process.env.API_KEY;
- const res = await fetch(
- `https://api.anify.tv/pages?providerId=${providerId}&readId=${encodeURIComponent(
- chapterId
- )}&apikey=${key}`
- );
+ const data = await getAnifyPage(mediaId, providerId, chapterId, key);
- const data = await res.json();
if (data.error) {
return {
- notFound: true
- }
+ notFound: true,
+ };
}
return {
diff --git a/pages/en/search/[param].js b/pages/en/search/[param].js
index cacc2b8..abd4f04 100644
--- a/pages/en/search/[param].js
+++ b/pages/en/search/[param].js
@@ -429,7 +429,7 @@ export default function Card() {
? `/en/manga/${anime.id}`
: `/en/anime/${anime.id}`
}
- className=""
+ title={anime.title.userPreferred}
>
<Image
className="object-cover bg-[#3B3C41] w-[146px] h-[208px] xxs:w-[115px] xxs:h-[163px] xs:w-[135px] xs:h-[192px] xl:w-[185px] xl:h-[265px] hover:scale-105 scale-100 transition-all cursor-pointer duration-200 ease-out rounded-[10px]"
@@ -439,7 +439,10 @@ export default function Card() {
height={500}
/>
</Link>
- <Link href={`/en/anime/${anime.id}`}>
+ <Link
+ href={`/en/anime/${anime.id}`}
+ title={anime.title.userPreferred}
+ >
<h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2">
{anime.status === "RELEASING" ? (
<span className="dots bg-green-500" />
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..f336e54
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,31 @@
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+model UserProfile {
+ id String @id @default(cuid())
+ name String @unique
+ setting Json?
+ WatchListEpisode WatchListEpisode[]
+}
+
+model WatchListEpisode {
+ id String @id @default(cuid())
+ aniId String?
+ title String?
+ aniTitle String?
+ image String?
+ episode Int?
+ timeWatched Int?
+ duration Int?
+ provider String?
+ createdDate DateTime? @default(now())
+ userProfile UserProfile @relation(fields: [userProfileId], references: [name])
+ userProfileId String
+ watchId String
+}
diff --git a/prisma/user.js b/prisma/user.js
new file mode 100644
index 0000000..04222d5
--- /dev/null
+++ b/prisma/user.js
@@ -0,0 +1,203 @@
+// import { prisma } from "../lib/prisma";
+import { PrismaClient } from "@prisma/client";
+const prisma = new PrismaClient();
+
+export const createUser = async (name, setting) => {
+ const checkUser = await prisma.userProfile.findUnique({
+ where: {
+ name: name,
+ },
+ });
+ if (!checkUser) {
+ const user = await prisma.userProfile.create({
+ data: {
+ name: name,
+ setting,
+ },
+ });
+
+ return user;
+ } else {
+ return null;
+ }
+};
+
+export const updateUser = async (name, anime) => {
+ const checkAnime = await prisma.watchListItem.findUnique({
+ where: {
+ title: anime.title,
+ userProfileId: name,
+ },
+ });
+ if (checkAnime) {
+ const checkEpisode = await prisma.watchListEpisode.findUnique({
+ where: {
+ url: anime.id,
+ },
+ });
+ if (checkEpisode) {
+ return null;
+ } else {
+ const user = await prisma.watchListItem.update({
+ where: {
+ title: anime.title,
+ userProfileId: name,
+ },
+ });
+ }
+ } else {
+ const user = await prisma.userProfile.update({
+ where: { name: name },
+ data: {
+ watchList: {
+ create: {
+ title: anime.title,
+ episodes: {
+ create: {
+ url: anime.id,
+ },
+ },
+ },
+ },
+ },
+ include: {
+ watchList: true,
+ },
+ });
+
+ return user;
+ }
+};
+
+export const getUser = async (name) => {
+ if (!name) {
+ const user = await prisma.userProfile.findMany({
+ include: {
+ WatchListEpisode: true,
+ },
+ });
+ return user;
+ } else {
+ const user = await prisma.userProfile.findFirst({
+ where: {
+ name: name,
+ },
+ include: {
+ WatchListEpisode: {
+ orderBy: {
+ createdDate: "desc",
+ },
+ },
+ },
+ });
+ return user;
+ }
+};
+
+export const deleteUser = async (name) => {
+ const user = await prisma.userProfile.delete({
+ where: {
+ name: name,
+ },
+ });
+ return user;
+};
+
+export const createList = async (name, id, title) => {
+ const checkEpisode = await prisma.watchListEpisode.findFirst({
+ where: {
+ userProfileId: name,
+ watchId: id,
+ },
+ });
+ if (checkEpisode) {
+ return null;
+ } else {
+ const episode = await prisma.userProfile.update({
+ where: { name: name },
+ data: {
+ WatchListEpisode: {
+ create: [
+ {
+ watchId: id,
+ },
+ ],
+ },
+ },
+ include: {
+ WatchListEpisode: true,
+ },
+ });
+ return episode;
+ }
+};
+
+export const getEpisode = async (name, id) => {
+ console.log({ name, id });
+ const episode = await prisma.watchListEpisode.findMany({
+ // where: {
+ // AND: [{ userProfileId: name }, { watchId: id }],
+ // },
+ where: {
+ AND: [
+ {
+ userProfileId: name,
+ },
+ {
+ watchId: {
+ equals: id,
+ },
+ },
+ ],
+ },
+ });
+ return episode;
+};
+
+export const updateUserEpisode = async ({
+ name,
+ id,
+ watchId,
+ title,
+ image,
+ number,
+ duration,
+ timeWatched,
+ aniTitle,
+ provider,
+}) => {
+ const user = await prisma.watchListEpisode.updateMany({
+ where: {
+ userProfileId: name,
+ watchId: watchId,
+ },
+ data: {
+ title: title,
+ aniTitle: aniTitle,
+ image: image,
+ aniId: id,
+ provider: provider,
+ duration: duration,
+ episode: number,
+ timeWatched: timeWatched,
+ createdDate: new Date(),
+ },
+ });
+
+ // const user = name;
+
+ return user;
+};
+
+export const updateTimeWatched = async (id, timeWatched) => {
+ const user = await prisma.watchListEpisode.update({
+ where: {
+ id: id,
+ // userProfileId: name,
+ },
+ data: {
+ timeWatched: timeWatched,
+ },
+ });
+ return user;
+};
diff --git a/styles/globals.css b/styles/globals.css
index 8063105..7e26486 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -358,3 +358,43 @@ pre code {
opacity: 0;
}
}
+
+/* .skip-button {
+ @apply bg-white xs:w-28 xs:h-9 w-24 h-7 -top-12 right-[2%] rounded-md font-karla shadow-xl hover:bg-[#f1f1f1] text-black absolute xs:text-[15px] text-xs md:text-sm;
+} */
+
+.vid-con {
+ @apply h-full absolute -top-12 right-[2%];
+}
+
+.next-button {
+ position: relative;
+ @apply xs:w-28 xs:h-9 w-24 h-7 rounded-md font-karla shadow-xl text-black xs:text-[15px] text-xs md:text-sm flex-center hover:bg-[#b9b9b9];
+ background: #ffffff;
+ border-radius: 6px;
+ cursor: pointer;
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ overflow: hidden;
+}
+
+.next-button::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ height: 100%;
+ width: 100%;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+}
+.next-button.progress::before {
+ animation: progress 7s ease forwards;
+}
+@keyframes progress {
+ 0% {
+ left: -100%;
+ }
+ 100% {
+ left: 0%;
+ }
+}