aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-09-13 00:45:53 +0700
committerGitHub <[email protected]>2023-09-13 00:45:53 +0700
commit7327a69b55a20b99b14ee0803d6cf5f8b88c45ef (patch)
treecbcca777593a8cc4b0282e7d85a6fc51ba517e25 /lib
parentUpdate issue templates (diff)
downloadmoopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.tar.xz
moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.zip
Update v4 - Merge pre-push to main (#71)
* Create build-test.yml * initial v4 commit * update: github workflow * update: push on branch * Update .github/ISSUE_TEMPLATE/bug_report.md * configuring next.config.js file
Diffstat (limited to 'lib')
-rw-r--r--lib/Artplayer.js19
-rw-r--r--lib/anify/info.js13
-rw-r--r--lib/anify/page.js13
-rw-r--r--lib/anilist/AniList.js2
-rw-r--r--lib/anilist/aniAdvanceSearch.js92
-rw-r--r--lib/anilist/getMedia.js90
-rw-r--r--lib/anilist/getUpcomingAnime.js52
-rw-r--r--lib/anilist/useAnilist.js250
-rw-r--r--lib/graphql/query.js304
-rw-r--r--lib/hooks/isOpenState.js17
-rw-r--r--lib/hooks/useDebounce.js17
-rw-r--r--lib/redis.js13
12 files changed, 710 insertions, 172 deletions
diff --git a/lib/Artplayer.js b/lib/Artplayer.js
index 96afe2b..48da24d 100644
--- a/lib/Artplayer.js
+++ b/lib/Artplayer.js
@@ -14,10 +14,9 @@ export default function Player({
id,
track,
// socket
- socket,
- isPlay,
- watchdata,
- room,
+ // isPlay,
+ // watchdata,
+ // room,
autoplay,
setautoplay,
...rest
@@ -59,18 +58,20 @@ export default function Player({
theme: "#f97316",
controls: [
{
+ index: 10,
name: "fast-rewind",
- position: "right",
- html: '<svg class="hi-solid hi-rewind inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>',
+ position: "left",
+ html: '<svg class="hi-solid hi-rewind inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>',
tooltip: "Backward 5s",
click: function () {
art.backward = 5;
},
},
{
+ index: 11,
name: "fast-forward",
- position: "right",
- html: '<svg class="hi-solid hi-fast-forward inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>',
+ position: "left",
+ html: '<svg class="hi-solid hi-fast-forward inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>',
tooltip: "Forward 5s",
click: function () {
art.forward = 5;
@@ -79,7 +80,7 @@ export default function Player({
],
settings: [
{
- html: "Autoplay",
+ html: "Autoplay Next",
// icon: '<img width="22" heigth="22" src="/assets/img/state.svg">',
tooltip: "ON/OFF",
switch: localStorage.getItem("autoplay") === "true" ? true : false,
diff --git a/lib/anify/info.js b/lib/anify/info.js
index 8978664..e7d4025 100644
--- a/lib/anify/info.js
+++ b/lib/anify/info.js
@@ -1,5 +1,5 @@
import axios from "axios";
-import cacheData from "memory-cache";
+import redis from "../redis";
export async function fetchInfo(id, key) {
try {
@@ -15,13 +15,18 @@ export async function fetchInfo(id, key) {
export default async function getAnifyInfo(id, key) {
try {
- const cached = cacheData.get(id);
+ let cached;
+ if (redis) {
+ cached = await redis.get(id);
+ }
if (cached) {
- return cached;
+ return JSON.parse(cached);
} else {
const data = await fetchInfo(id, key);
if (data) {
- cacheData.put(id, data, 1000 * 60 * 10);
+ if (redis) {
+ await redis.set(id, JSON.stringify(data), "EX", 60 * 10);
+ }
return data;
} else {
return { message: "Schedule not found" };
diff --git a/lib/anify/page.js b/lib/anify/page.js
index 6361230..b2b1207 100644
--- a/lib/anify/page.js
+++ b/lib/anify/page.js
@@ -1,4 +1,4 @@
-import cacheData from "memory-cache";
+import redis from "../redis";
// Function to fetch new data
async function fetchData(id, providerId, chapterId, key) {
@@ -21,13 +21,18 @@ export default async function getAnifyPage(
key
) {
try {
- const cached = cacheData.get(chapterId);
+ let cached;
+ if (redis) {
+ cached = await redis.get(chapterId);
+ }
if (cached) {
- return cached;
+ return JSON.parse(cached);
} else {
const data = await fetchData(mediaId, providerId, chapterId, key);
if (!data.error) {
- cacheData.put(chapterId, data, 1000 * 60 * 10);
+ if (redis) {
+ await redis.set(chapterId, JSON.stringify(data), "EX", 60 * 10);
+ }
return data;
} else {
return { message: "Manga/Novel not found :(" };
diff --git a/lib/anilist/AniList.js b/lib/anilist/AniList.js
index f5fe19c..b8d6ed3 100644
--- a/lib/anilist/AniList.js
+++ b/lib/anilist/AniList.js
@@ -29,8 +29,10 @@ export async function aniListData({ sort, page = 1 }) {
romaji
english
}
+ bannerImage
coverImage {
extraLarge
+ color
}
description
}
diff --git a/lib/anilist/aniAdvanceSearch.js b/lib/anilist/aniAdvanceSearch.js
index 263ca9d..02a5c53 100644
--- a/lib/anilist/aniAdvanceSearch.js
+++ b/lib/anilist/aniAdvanceSearch.js
@@ -1,63 +1,53 @@
-const advance = `
- query ($search: String, $type: MediaType, $status: MediaStatus, $season: MediaSeason, $seasonYear: Int, $genres: [String], $tags: [String], $sort: [MediaSort], $page: Int, $perPage: Int) {
- Page (page: $page, perPage: $perPage) {
- pageInfo {
- total
- currentPage
- lastPage
- hasNextPage
- }
- media (search: $search, type: $type, status: $status, season: $season, seasonYear: $seasonYear, genre_in: $genres, tag_in: $tags, sort: $sort, isAdult: false) {
- id
- title {
- userPreferred
- }
- type
- episodes
- chapters
- status
- format
- season
- seasonYear
- coverImage {
- extraLarge
- color
- }
- averageScore
- isAdult
- }
+import { advanceSearchQuery } from "../graphql/query";
+
+export async function aniAdvanceSearch({
+ search,
+ type,
+ genres,
+ page,
+ sort,
+ format,
+ season,
+ seasonYear,
+ perPage,
+}) {
+ const categorizedGenres = genres?.reduce((result, item) => {
+ const existingEntry = result[item.type];
+
+ if (existingEntry) {
+ existingEntry.push(item.value);
+ } else {
+ result[item.type] = [item.value];
}
- }
-`;
-export async function aniAdvanceSearch(options = {}) {
- const {
- search = null,
- type = "ANIME",
- seasonYear = NaN,
- season = undefined,
- genres = null,
- page = 1,
- perPage = null,
- sort = "POPULARITY_DESC",
- } = options;
- // console.log(page);
+ return result;
+ }, {});
+
const response = await fetch("https://graphql.anilist.co/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
- query: advance,
+ query: advanceSearchQuery,
variables: {
- search: search,
- type: type,
- seasonYear: seasonYear,
- season: season,
- genres: genres,
- perPage: perPage,
- sort: sort,
- page: page,
+ ...(search && {
+ search: search,
+ ...(!sort && { sort: "SEARCH_MATCH" }),
+ }),
+ ...(type && { type: type }),
+ ...(seasonYear && { seasonYear: seasonYear }),
+ ...(season && {
+ season: season,
+ ...(!seasonYear && { seasonYear: new Date().getFullYear() }),
+ }),
+ ...(categorizedGenres && { ...categorizedGenres }),
+ ...(format && { format: format }),
+ // ...(genres && { genres: genres }),
+ // ...(tags && { tags: tags }),
+ ...(perPage && { perPage: perPage }),
+ ...(sort && { sort: sort }),
+ ...(page && { page: page }),
},
}),
});
diff --git a/lib/anilist/getMedia.js b/lib/anilist/getMedia.js
new file mode 100644
index 0000000..c4628ab
--- /dev/null
+++ b/lib/anilist/getMedia.js
@@ -0,0 +1,90 @@
+import { useEffect, useState } from "react";
+
+export default function GetMedia(session, stats) {
+ const [media, setMedia] = useState([]);
+ const [recommendations, setRecommendations] = 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 }),
+ });
+ return response.json();
+ };
+
+ useEffect(() => {
+ if (!username || !accessToken) return;
+ const queryMedia = `
+ query ($username: String, $status: MediaListStatus, $sort: [RecommendationSort]) {
+ Page(page: 1, perPage: 10) {
+ recommendations(sort: $sort, onList: true) {
+ mediaRecommendation {
+ id
+ title {
+ userPreferred
+ }
+ description
+ format
+ type
+ status(version: 2)
+ bannerImage
+ isAdult
+ coverImage {
+ extraLarge
+ }
+ }
+ }
+ }
+ MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) {
+ lists {
+ status
+ name
+ entries {
+ id
+ mediaId
+ status
+ progress
+ score
+ media {
+ id
+ status
+ nextAiringEpisode {
+ timeUntilAiring
+ episode
+ }
+ title {
+ english
+ romaji
+ }
+ episodes
+ coverImage {
+ large
+ }
+ }
+ }
+ }
+ }
+}
+
+ `;
+ fetchGraphQL(queryMedia, {
+ username,
+ status: status?.stats,
+ sort: "ID_DESC",
+ }).then((data) => {
+ setMedia(data.data.MediaListCollection.lists);
+ setRecommendations(
+ data.data.Page.recommendations.map((i) => i.mediaRecommendation)
+ );
+ });
+ }, [username, accessToken, status?.stats]);
+
+ return { media, recommendations };
+}
diff --git a/lib/anilist/getUpcomingAnime.js b/lib/anilist/getUpcomingAnime.js
index fc848fd..2ab9315 100644
--- a/lib/anilist/getUpcomingAnime.js
+++ b/lib/anilist/getUpcomingAnime.js
@@ -19,23 +19,39 @@ const getUpcomingAnime = async () => {
}
const query = `
- query ($season: MediaSeason, $seasonYear: Int) {
- Page(page: 1, perPage: 20) {
- media(season: $season, seasonYear: $seasonYear, sort: POPULARITY_DESC, type: ANIME) {
+ query ($season: MediaSeason, $year: Int, $format: MediaFormat, $excludeFormat: MediaFormat, $status: MediaStatus, $minEpisodes: Int, $page: Int) {
+ Page(page: $page) {
+ pageInfo {
+ hasNextPage
+ total
+ }
+ media(season: $season, seasonYear: $year, format: $format, format_not: $excludeFormat, status: $status, episodes_greater: $minEpisodes, isAdult: false, type: ANIME, sort: TITLE_ROMAJI) {
id
- coverImage{
- large
- }
- bannerImage
+ idMal
title {
- english
romaji
native
+ english
+ }
+ startDate {
+ year
+ month
+ day
+ }
+ status
+ season
+ format
+ description
+ bannerImage
+ coverImage {
+ extraLarge
+ color
}
- nextAiringEpisode {
- episode
- airingAt
- timeUntilAiring
+ airingSchedule(notYetAired: true, perPage: 1) {
+ nodes {
+ episode
+ airingAt
+ }
}
}
}
@@ -43,8 +59,9 @@ const getUpcomingAnime = async () => {
`;
const variables = {
- season: currentSeason,
- seasonYear: currentYear,
+ season: "FALL",
+ year: currentYear,
+ format: "TV",
};
let response = await fetch("https://graphql.anilist.co", {
@@ -63,13 +80,14 @@ const getUpcomingAnime = async () => {
let currentSeasonAnime = json.data.Page.media;
let nextAiringAnime = currentSeasonAnime.filter(
- (anime) =>
- anime.nextAiringEpisode !== null && anime.nextAiringEpisode.episode === 1
+ (anime) => anime.airingSchedule.nodes?.[0]?.episode === 1
);
if (nextAiringAnime.length >= 1) {
nextAiringAnime.sort(
- (a, b) => a.nextAiringEpisode.airingAt - b.nextAiringEpisode.airingAt
+ (a, b) =>
+ a.airingSchedule.nodes?.[0].airingAt -
+ b.airingSchedule.nodes?.[0].airingAt
);
return nextAiringAnime; // return all upcoming anime, not just the first two
}
diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js
index 72e11ca..17ab11b 100644
--- a/lib/anilist/useAnilist.js
+++ b/lib/anilist/useAnilist.js
@@ -1,63 +1,107 @@
-import { useState, useEffect } from "react";
import { toast } from "react-toastify";
-export const useAniList = (session, stats) => {
- const [media, setMedia] = useState([]);
+export const useAniList = (session) => {
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,
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
body: JSON.stringify({ query, variables }),
});
return response.json();
};
- 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
- status
- progress
- score
- media {
- id
- status
- nextAiringEpisode {
- timeUntilAiring
- episode
- }
- title {
- english
- romaji
- }
- episodes
- coverImage {
- large
- }
- }
- }
- }
- }
+ const quickSearch = async ({ search, type, isAdult = false }) => {
+ if (!search || search === " ") return;
+ const searchQuery = `
+ query ($type: MediaType, $search: String, $isAdult: Boolean) {
+ Page(perPage: 8) {
+ pageInfo {
+ total
+ hasNextPage
+ }
+ results: media(type: $type, isAdult: $isAdult, search: $search) {
+ id
+ title {
+ userPreferred
+ }
+ coverImage {
+ medium
}
+ type
+ format
+ bannerImage
+ isLicensed
+ genres
+ startDate {
+ year
+ }
+ }
+ }
+}
`;
- fetchGraphQL(queryMedia, { username, status: status?.stats }).then((data) =>
- setMedia(data.data.MediaListCollection.lists)
- );
- }, [username, accessToken, status?.stats]);
+ const data = await fetchGraphQL(searchQuery, { search, type, isAdult });
+ return data;
+ };
+
+ const multiSearch = async (search) => {
+ if (!search || search === " ") return;
+ const searchQuery = `
+ query ($search: String, $isAdult: Boolean) {
+ anime: Page(perPage: 8) {
+ pageInfo {
+ total
+ hasNextPage
+ }
+ results: media(type: ANIME, isAdult: $isAdult, search: $search) {
+ id
+ title {
+ userPreferred
+ }
+ coverImage {
+ medium
+ }
+ type
+ format
+ bannerImage
+ isLicensed
+ genres
+ startDate {
+ year
+ }
+ }
+ }
+ manga: Page(perPage: 8) {
+ pageInfo {
+ total
+ hasNextPage
+ }
+ results: media(type: MANGA, isAdult: $isAdult, search: $search) {
+ id
+ title {
+ userPreferred
+ }
+ coverImage {
+ medium
+ }
+ type
+ format
+ bannerImage
+ isLicensed
+ startDate {
+ year
+ }
+ }
+ }
+}
+`;
+ const data = await fetchGraphQL(searchQuery, { search });
+ return data;
+ };
const markComplete = async (mediaId) => {
if (!accessToken) return;
@@ -94,7 +138,10 @@ export const useAniList = (session, stats) => {
query ($id: Int) {
Media(id: $id) {
mediaListEntry {
+ progress
+ status
customLists
+ repeat
}
id
type
@@ -103,6 +150,11 @@ export const useAniList = (session, stats) => {
english
native
}
+ format
+ episodes
+ nextAiringEpisode {
+ episode
+ }
}
}
`;
@@ -110,23 +162,11 @@ export const useAniList = (session, stats) => {
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) {
+ mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String], $repeat: Int) {
+ SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists, repeat: $repeat) {
id
mediaId
progress
@@ -137,46 +177,82 @@ export const useAniList = (session, stats) => {
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);
+ if (media && media.type !== "MANGA") {
+ let customList;
+
+ if (session.user.name) {
+ const res = await fetch(
+ `/api/user/profile?name=${session.user.name}`
+ ).then((res) => res.json());
+ customList = res?.setting === null ? true : res?.setting?.CustomLists;
}
- let lists = media?.mediaListEntry?.customLists
- ? Object.entries(media?.mediaListEntry?.customLists)
+ 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")) {
+
+ if (customList === true && !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",
- });
+
+ const singleEpisode =
+ (!media.episodes ||
+ (media.format === "MOVIE" && media.episodes === 1)) &&
+ 1;
+ const videoEpisode = Number(progress) || singleEpisode;
+ const mediaEpisode =
+ media.nextAiringEpisode?.episode || media.episodes || singleEpisode;
+ const status =
+ media.mediaListEntry?.status === "REPEATING" ? "REPEATING" : "CURRENT";
+
+ let variables = {
+ mediaId,
+ progress,
+ status,
+ progressVolumes: volumeProgress,
+ lists,
+ };
+
+ if (videoEpisode === mediaEpisode) {
+ variables.status = "COMPLETED";
+ if (media.mediaListEntry?.status === "REPEATING")
+ variables.repeat = media.mediaListEntry.repeat + 1;
}
+
+ // if (lists.length > 0) {
+ await fetchGraphQL(progressWatched, variables);
+ console.log(`Progress Updated: ${progress}`, status);
+ // }
+ } else if (media && media.type === "MANGA") {
+ let variables = {
+ mediaId,
+ progress,
+ status: stats,
+ progressVolumes: volumeProgress,
+ };
+
+ await fetchGraphQL(progressWatched, variables);
+ console.log(`Progress Updated: ${progress}`, status);
+ toast.success(`Progress Updated: ${progress}`, {
+ position: "bottom-right",
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ draggable: true,
+ theme: "dark",
+ });
}
};
- return { media, markComplete, markProgress, markPlanning, getUserLists };
+ return {
+ markComplete,
+ markProgress,
+ markPlanning,
+ getUserLists,
+ multiSearch,
+ quickSearch,
+ };
};
diff --git a/lib/graphql/query.js b/lib/graphql/query.js
new file mode 100644
index 0000000..297edb2
--- /dev/null
+++ b/lib/graphql/query.js
@@ -0,0 +1,304 @@
+const scheduleQuery = `
+query ($weekStart: Int, $weekEnd: Int, $page: Int) {
+ Page(page: $page) {
+ pageInfo {
+ hasNextPage
+ total
+ }
+ airingSchedules(airingAt_greater: $weekStart, airingAt_lesser: $weekEnd) {
+ id
+ episode
+ airingAt
+ media {
+ id
+ idMal
+ title {
+ romaji
+ native
+ english
+ }
+ startDate {
+ year
+ month
+ day
+ }
+ endDate {
+ year
+ month
+ day
+ }
+ type
+ status
+ season
+ format
+ genres
+ synonyms
+ duration
+ popularity
+ episodes
+ source(version: 2)
+ countryOfOrigin
+ hashtag
+ averageScore
+ siteUrl
+ description
+ bannerImage
+ isAdult
+ coverImage {
+ extraLarge
+ color
+ }
+ trailer {
+ id
+ site
+ thumbnail
+ }
+ externalLinks {
+ site
+ url
+ }
+ rankings {
+ rank
+ type
+ season
+ allTime
+ }
+ studios(isMain: true) {
+ nodes {
+ id
+ name
+ siteUrl
+ }
+ }
+ relations {
+ edges {
+ relationType(version: 2)
+ node {
+ id
+ title {
+ romaji
+ native
+ english
+ }
+ siteUrl
+ }
+ }
+ }
+ }
+ }
+ }
+}
+`;
+
+const advanceSearchQuery = `
+query ($page: Int = 1, $id: Int, $type: MediaType, $isAdult: Boolean = false, $search: String, $format: [MediaFormat], $status: MediaStatus, $countryOfOrigin: CountryCode, $source: MediaSource, $season: MediaSeason, $seasonYear: Int, $year: String, $onList: Boolean, $yearLesser: FuzzyDateInt, $yearGreater: FuzzyDateInt, $episodeLesser: Int, $episodeGreater: Int, $durationLesser: Int, $durationGreater: Int, $chapterLesser: Int, $chapterGreater: Int, $volumeLesser: Int, $volumeGreater: Int, $licensedBy: [Int], $isLicensed: Boolean, $genres: [String], $excludedGenres: [String], $tags: [String], $excludedTags: [String], $minimumTagRank: Int, $sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
+ Page(page: $page, perPage: 20) {
+ pageInfo {
+ total
+ perPage
+ currentPage
+ lastPage
+ hasNextPage
+ }
+ media(id: $id, type: $type, season: $season, format_in: $format, status: $status, countryOfOrigin: $countryOfOrigin, source: $source, search: $search, onList: $onList, seasonYear: $seasonYear, startDate_like: $year, startDate_lesser: $yearLesser, startDate_greater: $yearGreater, episodes_lesser: $episodeLesser, episodes_greater: $episodeGreater, duration_lesser: $durationLesser, duration_greater: $durationGreater, chapters_lesser: $chapterLesser, chapters_greater: $chapterGreater, volumes_lesser: $volumeLesser, volumes_greater: $volumeGreater, licensedById_in: $licensedBy, isLicensed: $isLicensed, genre_in: $genres, genre_not_in: $excludedGenres, tag_in: $tags, tag_not_in: $excludedTags, minimumTagRank: $minimumTagRank, sort: $sort, isAdult: $isAdult) {
+ id
+ title {
+ userPreferred
+ }
+ coverImage {
+ extraLarge
+ large
+ color
+ }
+ startDate {
+ year
+ month
+ day
+ }
+ endDate {
+ year
+ month
+ day
+ }
+ bannerImage
+ season
+ seasonYear
+ description
+ type
+ format
+ status(version: 2)
+ episodes
+ duration
+ chapters
+ volumes
+ genres
+ isAdult
+ averageScore
+ popularity
+ nextAiringEpisode {
+ airingAt
+ timeUntilAiring
+ episode
+ }
+ mediaListEntry {
+ id
+ status
+ }
+ studios(isMain: true) {
+ edges {
+ isMain
+ node {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+}`;
+
+const currentUserQuery = `
+query {
+ Viewer {
+ id
+ name
+ avatar {
+ large
+ medium
+ }
+ bannerImage
+ mediaListOptions {
+ animeList {
+ customLists
+ }
+ }
+ }
+ }`;
+
+const mediaInfoQuery = `
+ query ($id: Int) {
+ Media(id: $id) {
+ id
+ type
+ format
+ title {
+ romaji
+ english
+ native
+ }
+ coverImage {
+ extraLarge
+ large
+ color
+ }
+ bannerImage
+ description
+ episodes
+ nextAiringEpisode {
+ episode
+ airingAt
+ timeUntilAiring
+ }
+ averageScore
+ popularity
+ status
+ genres
+ season
+ seasonYear
+ duration
+ relations {
+ edges {
+ id
+ relationType(version: 2)
+ node {
+ id
+ title {
+ userPreferred
+ }
+ format
+ type
+ status(version: 2)
+ bannerImage
+ coverImage {
+ extraLarge
+ color
+ }
+ }
+ }
+ }
+ recommendations {
+ nodes {
+ mediaRecommendation {
+ id
+ title {
+ romaji
+ }
+ coverImage {
+ extraLarge
+ large
+ }
+ }
+ }
+ }
+ }
+}`;
+
+const mediaUserQuery = `
+query ($username: String, $status: MediaListStatus) {
+ MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) {
+ user {
+ id
+ name
+ about (asHtml: true)
+ createdAt
+ avatar {
+ large
+ }
+ statistics {
+ anime {
+ count
+ episodesWatched
+ meanScore
+ minutesWatched
+ }
+ }
+ bannerImage
+ mediaListOptions {
+ animeList {
+ sectionOrder
+ }
+ }
+ }
+ lists {
+ status
+ name
+ entries {
+ id
+ mediaId
+ status
+ progress
+ score
+ media {
+ id
+ status
+ title {
+ english
+ romaji
+ }
+ episodes
+ coverImage {
+ large
+ }
+ }
+ }
+ }
+ }
+ }`;
+
+export {
+ scheduleQuery,
+ advanceSearchQuery,
+ currentUserQuery,
+ mediaInfoQuery,
+ mediaUserQuery,
+};
diff --git a/lib/hooks/isOpenState.js b/lib/hooks/isOpenState.js
new file mode 100644
index 0000000..6aade61
--- /dev/null
+++ b/lib/hooks/isOpenState.js
@@ -0,0 +1,17 @@
+import React, { createContext, useContext, useState } from "react";
+
+const SearchContext = createContext();
+
+export const SearchProvider = ({ children }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <SearchContext.Provider value={{ isOpen, setIsOpen }}>
+ {children}
+ </SearchContext.Provider>
+ );
+};
+
+export function useSearch() {
+ return useContext(SearchContext);
+}
diff --git a/lib/hooks/useDebounce.js b/lib/hooks/useDebounce.js
new file mode 100644
index 0000000..e3a1631
--- /dev/null
+++ b/lib/hooks/useDebounce.js
@@ -0,0 +1,17 @@
+import { useEffect, useState } from "react";
+
+export default function useDebounce(value, delay) {
+ const [debounceValue, setDebounceValue] = useState(value);
+
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ setDebounceValue(value);
+ }, delay);
+
+ return () => {
+ clearTimeout(timeoutId);
+ };
+ }, [value, delay]);
+
+ return debounceValue;
+}
diff --git a/lib/redis.js b/lib/redis.js
new file mode 100644
index 0000000..ed8b8c5
--- /dev/null
+++ b/lib/redis.js
@@ -0,0 +1,13 @@
+import { Redis } from "ioredis";
+
+const REDIS_URL = process.env.REDIS_URL;
+
+let redis;
+
+if (REDIS_URL) {
+ redis = new Redis(REDIS_URL);
+} else {
+ console.warn("REDIS_URL is not defined. Redis caching will be disabled.");
+}
+
+export default redis;