From 3e78826658c7d2a4e9b3c1d73e63dacc1d39c361 Mon Sep 17 00:00:00 2001 From: Factiven Date: Sat, 12 Aug 2023 22:54:26 +0700 Subject: [PATCH] Update v3.9.3 - Merged Beta to Main (#51) * commit * update db * Update v3.9.1-beta-v3.1 * Update v3.9.1 * Fix watched progress not showing * Secure headers * Fix recently watched image * Update v3.9.2 > Added custom lists for AniList > Fixed episode listMode progress * Update db route * Fixed AniList * Fix next button on dub anime > video is playing sub anime instead dub * small adjusment for premid * fix eslint * small updates > added ability to remove episode from recently watched * Update v3.9.3 --- .env.example | 5 +- .eslintrc.json | 5 +- README.md | 1 + components/anime/episode.js | 1 + components/anime/infoDetails.js | 5 +- components/anime/viewMode/listMode.js | 18 +- components/anime/viewMode/thumbnailDetail.js | 4 +- components/anime/viewMode/thumbnailOnly.js | 4 +- components/anime/watch/primary/details.js | 18 +- components/anime/watch/primarySide.js | 20 +- components/anime/watch/secondarySide.js | 4 +- components/home/content.js | 126 ++++++++--- components/home/schedule.js | 4 +- components/manga/info/topSection.js | 2 +- components/manga/rightBar.js | 1 + components/videoPlayer.js | 23 +- lib/anilist/useAnilist.js | 200 +++++++++++------- next.config.js | 2 +- package-lock.json | 4 +- package.json | 2 +- pages/api/consumet/episode/[id].js | 5 +- pages/api/user/profile.js | 16 +- pages/api/user/update/episode.js | 30 ++- pages/en/anime/[...id].js | 6 +- pages/en/anime/recently-watched.js | 98 ++++++++- pages/en/anime/watch/[...info].js | 6 +- pages/en/dmca.js | 5 +- pages/en/index.js | 36 +++- pages/en/manga/read/[...params].js | 1 + pages/index.js | 21 +- .../migration.sql | 5 + prisma/schema.prisma | 2 +- prisma/user.js | 48 ++++- public/preview.png | Bin 0 -> 871269 bytes 34 files changed, 528 insertions(+), 200 deletions(-) create mode 100644 prisma/migrations/20230810051657_ondelete_cascade/migration.sql create mode 100644 public/preview.png diff --git a/.env.example b/.env.example index a79878ac..2f85f934 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,7 @@ NEXTAUTH_URL="for development use http://localhost:3000/ and for production use PROXY_URI="I recommend you to use this cors-anywhere as a proxy https://github.com/Rob--W/cors-anywhere follow the instruction on how to use it there. Skip this if you only use gogoanime as a source" 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)' \ No newline at end of file +DISQUS_SHORTNAME='put your disqus shortname here. (optional)' + +## Prisma +DATABASE_URL="Your postgresql connection url" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index dfa8f73d..dbda85f6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,7 @@ { "extends": "next/core-web-vitals", - "rules": { "react/no-unescaped-entities": 0 } + "rules": { + "react/no-unescaped-entities": 0, + "react/no-unknown-property": ["error", { "ignore": ["css"] }] + } } diff --git a/README.md b/README.md index f21ccfc7..4f964a5f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ npm install 3. Generate Prisma : ```bash +npx prisma migrate dev npx prisma generate ``` diff --git a/components/anime/episode.js b/components/anime/episode.js index c889c252..5d3451b8 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -246,6 +246,7 @@ export default function AnimeEpisode({ info, progress }) { info={info} episode={episode} index={index} + artStorage={artStorage} providerId={providerId} progress={progress} dub={isDub} diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js index 0cf233c3..814e49be 100644 --- a/components/anime/infoDetails.js +++ b/components/anime/infoDetails.js @@ -45,7 +45,10 @@ export default function DesktopDetails({
-

+

{info ? ( info?.title?.romaji || info?.title?.english ) : ( diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js index 20162623..f3bcf058 100644 --- a/components/anime/viewMode/listMode.js +++ b/components/anime/viewMode/listMode.js @@ -4,10 +4,16 @@ export default function ListMode({ info, episode, index, + artStorage, providerId, progress, dub, }) { + const time = artStorage?.[episode?.id]?.timeWatched; + const duration = artStorage?.[episode?.id]?.duration; + let prog = (time / duration) * 100; + if (prog > 90) prog = 100; + return (
90) prog = 100; @@ -33,7 +33,7 @@ export default function ThumbnailDetail({ className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]" /> 90) prog = 100; @@ -25,7 +25,7 @@ export default function ThumbnailOnly({ Episode {episode?.number} )}
-
+

Studios @@ -93,11 +97,15 @@ export default function Details({
{info ? ( <> -
{info.title?.romaji || ""}
-
+
+ {info.title?.romaji || ""} +
+
{info.title?.english || ""}
-
{info.title?.native || ""}
+
+ {info.title?.native || ""} +
) : ( @@ -120,7 +128,7 @@ export default function Details({
{info && (

)} diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js index c6017957..b032fd64 100644 --- a/components/anime/watch/primarySide.js +++ b/components/anime/watch/primarySide.js @@ -27,6 +27,7 @@ export default function PrimarySide({ setOnList, episodeList, timeWatched, + dub, }) { const [episodeData, setEpisodeData] = useState(); const [open, setOpen] = useState(false); @@ -148,6 +149,7 @@ export default function PrimarySide({ aniTitle={info.title?.romaji || info.title?.english} track={navigation} timeWatched={timeWatched} + dub={dub} /> ) ) : ( @@ -162,13 +164,14 @@ export default function PrimarySide({ {navigation?.playing?.title || info.title?.romaji}

-

+

Episode {epiNumber} -

+

@@ -180,7 +183,11 @@ export default function PrimarySide({ (episode) => episode.number === parseInt(e.target.value) ); router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${selectedEpisode.id}&num=${selectedEpisode.number}` + `/en/anime/watch/${info.id}/${providerId}?id=${ + selectedEpisode.id + }&num=${selectedEpisode.number}${ + dub ? `&dub=${dub}` : "" + }` ); }} > @@ -199,7 +206,11 @@ export default function PrimarySide({ }relative group`} onClick={() => { router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${navigation?.next.id}&num=${navigation?.next.number}` + `/en/anime/watch/${info.id}/${providerId}?id=${ + navigation?.next.id + }&num=${navigation?.next.number}${ + dub ? `&dub=${dub}` : "" + }` ); }} > @@ -229,6 +240,7 @@ export default function PrimarySide({
0 ? ( episode.some((item) => item.title && item.description) > 0 ? ( episode.map((item) => { - const time = artStorage?.[item.id]?.time; + const time = artStorage?.[item.id]?.timeWatched; const duration = artStorage?.[item.id]?.duration; let prog = (time / duration) * 100; if (prog > 90) prog = 100; @@ -50,7 +50,7 @@ export default function SecondarySide({ }`} /> { + if (userName) { + // remove from database + const res = await fetch(`/api/user/update/episode`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: userName, + id: id, + }), + }); + const data = await res.json(); + + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + + // update client + setRemoved(id); + + if (data?.message === "Episode deleted") { + toast.success("Episode removed from history", { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + draggable: true, + theme: "dark", + }); + } + } else { + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + + setRemoved(id); + } + }; + return (
- {ids !== "recentlyWatched" ? slicedData?.map((anime) => { const progress = og?.find((i) => i.mediaId === anime.id); @@ -273,14 +329,27 @@ export default function Content({ if (prog > 90) prog = 100; return ( - -
+
+
removeItem(i.watchId)} + > + + + Remove from history + +
+
+
@@ -299,6 +368,7 @@ export default function Content({ width: `${prog}%`, }} /> + {i?.image && ( )} -
+ -
+ {/*

{i.title}

*/}

{" "} | Episode {i.episode}

-
- + +
); })} - {userData?.length >= 10 && section !== "Recommendations" && ( -
-
-

- More on {section} -

- + {userData?.filter((i) => i.aniId !== null)?.length >= 10 && + section !== "Recommendations" && ( +
+
+

+ More on {section} +

+ +
-
- )} + )} {filteredData?.length >= 10 && section !== "Recommendations" && (
{scheduleData[days[currentPage]] - .filter((show, index, self) => { + ?.filter((show, index, self) => { return index === self.findIndex((s) => s.id === show.id); }) - .map((i, index) => { + ?.map((i, index) => { const currentTime = Date.now(); const hasAired = i.airingAt < currentTime; diff --git a/components/manga/info/topSection.js b/components/manga/info/topSection.js index 14dc5e50..40b5a376 100644 --- a/components/manga/info/topSection.js +++ b/components/manga/info/topSection.js @@ -66,7 +66,7 @@ export default function TopSection({ info, firstEp, setCookie }) {
-

+

{info.title?.romaji || info.title?.english || info.title?.native}

diff --git a/components/manga/rightBar.js b/components/manga/rightBar.js index 6d37e4a8..18c5e552 100644 --- a/components/manga/rightBar.js +++ b/components/manga/rightBar.js @@ -151,6 +151,7 @@ export default function RightBar({ Chapter Progress { + art.subtitle.style({ + fontSize: art.height * 0.05 + "px", + }); + }); + art.on("video:timeupdate", async () => { if (!session) return; @@ -313,7 +314,9 @@ export default function VideoPlayer({ router.push( `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent( track?.next?.id - )}&num=${track?.next?.number}` + )}&num=${track?.next?.number}${ + dub ? `&dub=${dub}` : "" + }` ); } }, @@ -332,7 +335,7 @@ export default function VideoPlayer({ router.push( `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent( track?.next?.id - )}&num=${track?.next?.number}` + )}&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}` ); } }, 7000); diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js index bedb4a59..72e11ca8 100644 --- a/lib/anilist/useAnilist.js +++ b/lib/anilist/useAnilist.js @@ -1,14 +1,18 @@ import { useState, useEffect } from "react"; import { toast } from "react-toastify"; -function useMedia(username, accessToken, status) { +export const useAniList = (session, stats) => { const [media, setMedia] = useState([]); + const accessToken = session?.user?.token; + const username = session?.user?.name; + const status = stats || null; const fetchGraphQL = async (query, variables) => { const response = await fetch("https://graphql.anilist.co/", { method: "POST", headers: { "Content-Type": "application/json", + Authorization: accessToken ? `Bearer ${accessToken}` : undefined, }, body: JSON.stringify({ query, variables }), }); @@ -18,68 +22,47 @@ function useMedia(username, accessToken, status) { useEffect(() => { if (!username || !accessToken) return; const queryMedia = ` - query ($username: String, $status: MediaListStatus) { - MediaListCollection(userName: $username, type: ANIME, status: $status) { - lists { - status - name - entries { - id - mediaId + query ($username: String, $status: MediaListStatus) { + MediaListCollection(userName: $username, type: ANIME, status: $status) { + lists { status - progress - score - media { + name + entries { id + mediaId status - nextAiringEpisode { + progress + score + media { + id + status + nextAiringEpisode { timeUntilAiring episode - } - title { - english - romaji - } - episodes - coverImage { - large + } + title { + english + romaji + } + episodes + coverImage { + large + } } } } } } - } - `; + `; fetchGraphQL(queryMedia, { username, status: status?.stats }).then((data) => setMedia(data.data.MediaListCollection.lists) ); }, [username, accessToken, status?.stats]); - return media; -} - -export function useAniList(session, stats) { - const accessToken = session?.user?.token; - const username = session?.user?.name; - const status = stats || null; - const media = useMedia(username, accessToken, status); - - const fetchGraphQL = async (query, variables) => { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: accessToken ? `Bearer ${accessToken}` : undefined, - }, - body: JSON.stringify({ query, variables }), - }); - return response.json(); - }; - - const markComplete = (mediaId) => { + const markComplete = async (mediaId) => { if (!accessToken) return; const completeQuery = ` - mutation($mediaId: Int ) { + mutation($mediaId: Int) { SaveMediaListEntry(mediaId: $mediaId, status: COMPLETED) { id mediaId @@ -87,14 +70,13 @@ export function useAniList(session, stats) { } } `; - fetchGraphQL(completeQuery, { mediaId }).then((data) => - console.log({ Complete: data }) - ); + const data = await fetchGraphQL(completeQuery, { mediaId }); + console.log({ Complete: data }); }; - const markPlanning = (mediaId) => { + const markPlanning = async (mediaId) => { if (!accessToken) return; - const completeQuery = ` + const planningQuery = ` mutation($mediaId: Int ) { SaveMediaListEntry(mediaId: $mediaId, status: PLANNING) { id @@ -103,40 +85,98 @@ export function useAniList(session, stats) { } } `; - fetchGraphQL(completeQuery, { mediaId }).then((data) => - console.log({ added_to_list: data }) - ); + const data = await fetchGraphQL(planningQuery, { mediaId }); + console.log({ added_to_list: data }); }; - const markProgress = (mediaId, progress, stats, volumeProgress) => { - if (!accessToken) return; - const progressWatched = ` - mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int) { - SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes) { + const getUserLists = async (id) => { + const getLists = ` + query ($id: Int) { + Media(id: $id) { + mediaListEntry { + customLists + } id - mediaId - progress - status + type + title { + romaji + english + native + } } } + `; + const data = await fetchGraphQL(getLists, { id }); + return data; + }; + + const customLists = async (lists) => { + const setList = ` + mutation($lists: [String]){ + UpdateUser(animeListOptions: { customLists: $lists }){ + id + } + } + `; + const data = await fetchGraphQL(setList, { lists }); + return data; + }; + + const markProgress = async (mediaId, progress, stats, volumeProgress) => { + if (!accessToken) return; + const progressWatched = ` + mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String]) { + SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists) { + id + mediaId + progress + status + } + } `; - fetchGraphQL(progressWatched, { - mediaId, - progress, - status: stats, - progressVolumes: volumeProgress, - }).then(() => { - console.log(`Progress Updated: ${progress}`); - toast.success(`Progress Updated: ${progress}`, { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", - }); - }); + + const user = await getUserLists(mediaId); + const media = user?.data?.Media; + if (media) { + let checkList = media?.mediaListEntry?.customLists + ? Object.entries(media?.mediaListEntry?.customLists).map( + ([key, value]) => key + ) || [] + : []; + + if (!checkList?.includes("Watched using Moopa")) { + checkList.push("Watched using Moopa"); + await customLists(checkList); + } + + let lists = media?.mediaListEntry?.customLists + ? Object.entries(media?.mediaListEntry?.customLists) + .filter(([key, value]) => value === true) + .map(([key, value]) => key) || [] + : []; + if (!lists?.includes("Watched using Moopa")) { + lists.push("Watched using Moopa"); + } + if (lists.length > 0) { + await fetchGraphQL(progressWatched, { + mediaId, + progress, + status: stats, + progressVolumes: volumeProgress, + lists, + }); + console.log(`Progress Updated: ${progress}`); + toast.success(`Progress Updated: ${progress}`, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + draggable: true, + theme: "dark", + }); + } + } }; - return { media, markComplete, markProgress, markPlanning }; -} + return { media, markComplete, markProgress, markPlanning, getUserLists }; +}; diff --git a/next.config.js b/next.config.js index fcf654b5..f7da5180 100644 --- a/next.config.js +++ b/next.config.js @@ -18,7 +18,7 @@ module.exports = withPWA({ }, ], }, - distDir: process.env.BUILD_DIR || ".next", + // distDir: process.env.BUILD_DIR || ".next", trailingSlash: true, output: "standalone", // async headers() { diff --git a/package-lock.json b/package-lock.json index a1ff2797..cecee3ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "moopa", - "version": "3.9.1", + "version": "3.9.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "moopa", - "version": "3.9.1", + "version": "3.9.3", "dependencies": { "@apollo/client": "^3.7.3", "@headlessui/react": "^1.7.15", diff --git a/package.json b/package.json index b5ddad8a..76d9adf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moopa", - "version": "3.9.1", + "version": "3.9.3", "private": true, "founder": "Factiven", "scripts": { diff --git a/pages/api/consumet/episode/[id].js b/pages/api/consumet/episode/[id].js index e6f40ce6..6e7f3180 100644 --- a/pages/api/consumet/episode/[id].js +++ b/pages/api/consumet/episode/[id].js @@ -1,4 +1,3 @@ -import axios from "axios"; import cacheData from "memory-cache"; const API_URL = process.env.API_URI; @@ -9,7 +8,7 @@ export default async function handler(req, res) { const dub = req.query.dub || false; const refresh = req.query.refresh || false; - const providers = ["enime", "gogoanime"]; + const providers = ["enime", "gogoanime", "zoro"]; const datas = []; const cached = cacheData.get(id + dub); @@ -59,7 +58,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 * 10); + 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 index dd22bd88..e20aaca8 100644 --- a/pages/api/user/profile.js +++ b/pages/api/user/profile.js @@ -43,13 +43,21 @@ export default async function handler(req, res) { } case "DELETE": { const { name } = req.body; - const user = await deleteUser(name); - if (!user) { - return res.status(404).json({ message: "User not found" }); + // return res.status(200).json({ name }); + if (session.user.name !== name) { + return res.status(401).json({ message: "Unauthorized" }); } else { - return res.status(200).json(user); + const user = await deleteUser(name); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } else { + return res.status(200).json(user); + } } } + default: { + return res.status(405).json({ message: "Method not allowed" }); + } } } catch (error) { console.log(error); diff --git a/pages/api/user/update/episode.js b/pages/api/user/update/episode.js index 79744467..52c94947 100644 --- a/pages/api/user/update/episode.js +++ b/pages/api/user/update/episode.js @@ -3,6 +3,7 @@ import { authOptions } from "../../auth/[...nextauth]"; import { createList, + deleteEpisode, getEpisode, updateUserEpisode, } from "../../../../prisma/user"; @@ -16,13 +17,17 @@ export default async function handler(req, res) { 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" }); + if (session.user.name !== name) { + return res.status(401).json({ message: "Unauthorized" }); } else { - return res.status(201).json(episode); + 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": { @@ -68,6 +73,19 @@ export default async function handler(req, res) { return res.status(200).json(episode); } } + case "DELETE": { + const { name, id } = req.body; + if (session.user.name !== name) { + return res.status(401).json({ message: "Unauthorized" }); + } else { + const episode = await deleteEpisode(name, id); + if (!episode) { + return res.status(404).json({ message: "Episode not found" }); + } else { + return res.status(200).json({ message: "Episode deleted" }); + } + } + } } } catch (error) { console.log(error); diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js index 5e4aed86..534aa176 100644 --- a/pages/en/anime/[...id].js +++ b/pages/en/anime/[...id].js @@ -125,14 +125,14 @@ export default function Info({ info, color }) { }&image=${info.bannerImage || info.coverImage.extraLarge}`} /> - + handleClose()}>
{!session && (
-

+
Edit your list -

+