From 3815521b0d549020491dbc2d862347f6ff7334c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=CA=9C=C9=AA=C9=B4=E1=B4=87=C9=AA=20N=E1=B4=8F=E1=B4=9C?=
=?UTF-8?q?=E1=B4=A2=E1=B4=87=C9=B4?=
<157171073+Shineii86@users.noreply.github.com>
Date: Mon, 8 Jun 2026 15:08:22 +0530
Subject: [PATCH] feat: initial project setup for AniKatoAPI
- Express.js server with Vercel deployment support
- Complete API endpoints for anikototv.to anime data
- Home, search, info, episodes, stream, schedule endpoints
- Category filtering and genre support
- Caching system with 5-minute TTL
- CORS configuration
- Public documentation page
- Docker and Render deployment configs
- MIT License
---
.env.example | 5 +
.gitignore | 4 +
CHANGELOG.md | 35 ++++
Dockerfile | 13 ++
LICENSE | 21 +++
package.json | 29 +++
public/404.html | 65 +++++++
public/index.html | 218 ++++++++++++++++++++++
render.yaml | 9 +
server.js | 63 +++++++
src/configs/dataUrl.js | 34 ++++
src/configs/header.config.js | 10 +
src/controllers/animeInfo.controller.js | 25 +++
src/controllers/category.controller.js | 27 +++
src/controllers/episodeList.controller.js | 26 +++
src/controllers/filter.controller.js | 21 +++
src/controllers/homeInfo.controller.js | 20 ++
src/controllers/popular.controller.js | 21 +++
src/controllers/random.controller.js | 20 ++
src/controllers/schedule.controller.js | 26 +++
src/controllers/search.controller.js | 46 +++++
src/controllers/spotlight.controller.js | 20 ++
src/controllers/streamInfo.controller.js | 26 +++
src/controllers/suggestion.controller.js | 26 +++
src/controllers/topten.controller.js | 20 ++
src/controllers/trending.controller.js | 20 ++
src/extractors/animeInfo.extractor.js | 103 ++++++++++
src/extractors/category.extractor.js | 44 +++++
src/extractors/episodeList.extractor.js | 40 ++++
src/extractors/filter.extractor.js | 60 ++++++
src/extractors/homeInfo.extractor.js | 124 ++++++++++++
src/extractors/popular.extractor.js | 41 ++++
src/extractors/random.extractor.js | 40 ++++
src/extractors/schedule.extractor.js | 39 ++++
src/extractors/search.extractor.js | 67 +++++++
src/extractors/spotlight.extractor.js | 40 ++++
src/extractors/streamInfo.extractor.js | 58 ++++++
src/extractors/suggestion.extractor.js | 32 ++++
src/extractors/topten.extractor.js | 57 ++++++
src/extractors/trending.extractor.js | 30 +++
src/helper/cache.helper.js | 21 +++
src/helper/countPages.helper.js | 12 ++
src/helper/extractPages.helper.js | 16 ++
src/helper/formatTitle.helper.js | 9 +
src/routes/apiRoutes.js | 132 +++++++++++++
src/routes/category.route.js | 13 ++
vercel.json | 18 ++
47 files changed, 1846 insertions(+)
create mode 100644 .env.example
create mode 100644 .gitignore
create mode 100644 CHANGELOG.md
create mode 100644 Dockerfile
create mode 100644 LICENSE
create mode 100644 package.json
create mode 100644 public/404.html
create mode 100644 public/index.html
create mode 100644 render.yaml
create mode 100644 server.js
create mode 100644 src/configs/dataUrl.js
create mode 100644 src/configs/header.config.js
create mode 100644 src/controllers/animeInfo.controller.js
create mode 100644 src/controllers/category.controller.js
create mode 100644 src/controllers/episodeList.controller.js
create mode 100644 src/controllers/filter.controller.js
create mode 100644 src/controllers/homeInfo.controller.js
create mode 100644 src/controllers/popular.controller.js
create mode 100644 src/controllers/random.controller.js
create mode 100644 src/controllers/schedule.controller.js
create mode 100644 src/controllers/search.controller.js
create mode 100644 src/controllers/spotlight.controller.js
create mode 100644 src/controllers/streamInfo.controller.js
create mode 100644 src/controllers/suggestion.controller.js
create mode 100644 src/controllers/topten.controller.js
create mode 100644 src/controllers/trending.controller.js
create mode 100644 src/extractors/animeInfo.extractor.js
create mode 100644 src/extractors/category.extractor.js
create mode 100644 src/extractors/episodeList.extractor.js
create mode 100644 src/extractors/filter.extractor.js
create mode 100644 src/extractors/homeInfo.extractor.js
create mode 100644 src/extractors/popular.extractor.js
create mode 100644 src/extractors/random.extractor.js
create mode 100644 src/extractors/schedule.extractor.js
create mode 100644 src/extractors/search.extractor.js
create mode 100644 src/extractors/spotlight.extractor.js
create mode 100644 src/extractors/streamInfo.extractor.js
create mode 100644 src/extractors/suggestion.extractor.js
create mode 100644 src/extractors/topten.extractor.js
create mode 100644 src/extractors/trending.extractor.js
create mode 100644 src/helper/cache.helper.js
create mode 100644 src/helper/countPages.helper.js
create mode 100644 src/helper/extractPages.helper.js
create mode 100644 src/helper/formatTitle.helper.js
create mode 100644 src/routes/apiRoutes.js
create mode 100644 src/routes/category.route.js
create mode 100644 vercel.json
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..7a01c72
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+# Origins you want to allow
+ALLOWED_ORIGINS=https://site1.com,https://site2.com
+
+# Port (optional, defaults to 4444)
+PORT=4444
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f48416b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.env
+.DS_Store
+*.log
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5e3cfc7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,35 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.0.0] - 2024-12-19
+
+### Added
+- Initial project setup
+- Express.js server with Vercel deployment support
+- Home page API endpoint (`/api/`)
+- Anime info endpoint (`/api/info?id={anime-id}`)
+- Search endpoint (`/api/search?keyword={keyword}`)
+- Search suggestions endpoint (`/api/search/suggest?keyword={keyword}`)
+- Episode list endpoint (`/api/episodes/{anime-id}`)
+- Stream info endpoint (`/api/stream?id={episode-id}&server={server}&type={type}`)
+- Schedule endpoint (`/api/schedule?date={YYYY-MM-DD}`)
+- Spotlight endpoint (`/api/spotlight`)
+- Trending endpoint (`/api/trending`)
+- Top 10 endpoint (`/api/top-ten`)
+- Suggestions endpoint (`/api/suggestions?keyword={keyword}`)
+- Random anime endpoint (`/api/random`)
+- Most popular endpoint (`/api/most-popular?page={page}`)
+- Filter endpoint (`/api/filter?{params}`)
+- Category endpoints (`/api/{category}?page={page}`)
+- Caching system with 5-minute TTL
+- CORS configuration with allowed origins
+- Public HTML documentation page
+- 404 error page
+- Dockerfile for containerization
+- Render.yaml for Render deployment
+- MIT License
+- README with API documentation
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d9f967c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:18-alpine
+
+WORKDIR /app
+
+COPY package*.json ./
+
+RUN npm install
+
+COPY . .
+
+EXPOSE 4444
+
+CMD ["npm", "start"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ef05b5d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..24cd242
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "AniKatoAPI",
+ "version": "1.0.0",
+ "description": "A restful API to provide all information about anime from anikototv.to",
+ "main": "./server.js",
+ "scripts": {
+ "start": "node ./server.js",
+ "dev": "nodemon server.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.11.0",
+ "cheerio": "^1.0.0-rc.12",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.5",
+ "dotenv": "^17.2.0",
+ "express": "^5.2.1"
+ },
+ "type": "module",
+ "devDependencies": {
+ "nodemon": "^3.1.10"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Shineii86/AniKatoAPI.git"
+ }
+}
\ No newline at end of file
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..d6c04fe
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+ 404 - Not Found | AniKatoAPI
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..a020e24
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,218 @@
+
+
+
+
+
+ AniKatoAPI - Anime API
+
+
+
+
+
+
+
+
Base URL
+
+ https://anikatoapi.vercel.app/api
+
+
+
+
+
API Endpoints
+
+
+
GET
+
/api/
+
Get home page data including spotlight, trending, top airing, etc.
+
+
+
+
GET
+
/api/info?id={anime-id}
+
Get detailed information about a specific anime
+
+
+
+
GET
+
/api/search?keyword={keyword}&page={page}
+
Search for anime by keyword
+
+
+
+
GET
+
/api/search/suggest?keyword={keyword}
+
Get search suggestions
+
+
+
+
GET
+
/api/episodes/{anime-id}
+
Get episode list for an anime
+
+
+
+
GET
+
/api/stream?id={episode-id}&server={server}&type={sub/dub}
+
Get streaming information for an episode
+
+
+
+
GET
+
/api/schedule?date={YYYY-MM-DD}
+
Get anime schedule for a specific date
+
+
+
+
GET
+
/api/spotlight
+
Get spotlight/featured anime
+
+
+
+
GET
+
/api/trending
+
Get trending anime
+
+
+
+
GET
+
/api/top-ten
+
Get top 10 anime (today, week, month)
+
+
+
+
GET
+
/api/suggestions?keyword={keyword}
+
Get anime suggestions
+
+
+
+
GET
+
/api/random
+
Get random anime information
+
+
+
+
GET
+
/api/most-popular?page={page}
+
Get most popular anime
+
+
+
+
GET
+
/api/filter?{params}
+
Filter anime by type, status, genre, etc.
+
+
+
+
GET
+
/api/{category}?page={page}
+
Get anime by category (movie, tv, ova, ona, etc.)
+
+
+
+
+
Categories
+
+ top-airing, most-popular, most-favorite, completed, recently-updated, recently-added, subbed-anime, dubbed-anime, movie, tv, ova, ona, special, music
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/render.yaml b/render.yaml
new file mode 100644
index 0000000..f298e02
--- /dev/null
+++ b/render.yaml
@@ -0,0 +1,9 @@
+services:
+ - type: web
+ name: AniKatoAPI
+ env: node
+ buildCommand: npm install
+ startCommand: npm start
+ envVars:
+ - key: ALLOWED_ORIGINS
+ value: https://anikototv.to,https://anikoto.cz,https://anikoto.me,https://anikoto.net,https://anikoto.se
\ No newline at end of file
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..ed9e92a
--- /dev/null
+++ b/server.js
@@ -0,0 +1,63 @@
+import dotenv from "dotenv";
+import express from "express";
+import cors from "cors";
+import path from "path";
+import fs from "fs";
+import { fileURLToPath } from "url";
+import { dirname } from "path";
+import { createApiRoutes } from "./src/routes/apiRoutes.js";
+
+dotenv.config();
+
+const app = express();
+const PORT = process.env.PORT || 4444;
+const __filename = fileURLToPath(import.meta.url);
+const publicDir = path.join(dirname(__filename), "public");
+const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
+
+app.use(
+ cors({
+ origin: allowedOrigins?.includes("*") ? "*" : allowedOrigins || [],
+ methods: ["GET"],
+ })
+);
+
+app.use((req, res, next) => {
+ const origin = req.headers.origin;
+ if (
+ !allowedOrigins ||
+ allowedOrigins.includes("*") ||
+ (origin && allowedOrigins.includes(origin))
+ ) {
+ res.setHeader("Access-Control-Allow-Origin", origin || "*");
+ res.setHeader("Access-Control-Allow-Methods", "GET");
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
+ return next();
+ }
+ res
+ .status(403)
+ .json({ success: false, message: "Forbidden: Origin not allowed" });
+});
+
+app.use(express.static(publicDir, { redirect: false }));
+
+const jsonResponse = (res, data, status = 200) =>
+ res.status(status).json({ success: true, results: data });
+
+const jsonError = (res, message = "Internal server error", status = 500) =>
+ res.status(status).json({ success: false, message });
+
+createApiRoutes(app, jsonResponse, jsonError);
+
+app.use((req, res) => {
+ const filePath = path.join(publicDir, "404.html");
+ if (fs.existsSync(filePath)) {
+ res.status(404).sendFile(filePath);
+ } else {
+ res.status(404).json({ success: false, message: "Endpoint not found" });
+ }
+});
+
+app.listen(PORT, () => {
+ console.info(`AniKatoAPI listening at ${PORT}`);
+});
\ No newline at end of file
diff --git a/src/configs/dataUrl.js b/src/configs/dataUrl.js
new file mode 100644
index 0000000..6458939
--- /dev/null
+++ b/src/configs/dataUrl.js
@@ -0,0 +1,34 @@
+const BASE_URL = "https://anikototv.to";
+const ALT_DOMAINS = [
+ "https://anikoto.cz",
+ "https://anikoto.me",
+ "https://anikototv.to",
+ "https://anikoto.net",
+ "https://anikoto.se"
+];
+
+const URLS = {
+ home: `${BASE_URL}/home`,
+ search: `${BASE_URL}/search`,
+ genre: (genre) => `${BASE_URL}/genre/${genre}`,
+ azList: (letter) => `${BASE_URL}/az-list/${letter}`,
+ category: (cat) => `${BASE_URL}/${cat}`,
+ anime: (id) => `${BASE_URL}/anime/${id}`,
+ episode: (id) => `${BASE_URL}/episode/${id}`,
+ schedule: (date) => `${BASE_URL}/schedule/${date}`,
+ topTen: `${BASE_URL}/top-ten`,
+ mostPopular: `${BASE_URL}/most-popular`,
+ mostFavorite: `${BASE_URL}/most-favorite`,
+ recentlyUpdated: `${BASE_URL}/recently-updated`,
+ recentlyAdded: `${BASE_URL}/recently-added`,
+ subbedAnime: `${BASE_URL}/subbed-anime`,
+ dubbedAnime: `${BASE_URL}/dubbed-anime`,
+ movie: `${BASE_URL}/movie`,
+ ova: `${BASE_URL}/ova`,
+ ona: `${BASE_URL}/ona`,
+ special: `${BASE_URL}/special`,
+ tv: `${BASE_URL}/tv`,
+ music: `${BASE_URL}/music`
+};
+
+export { BASE_URL, ALT_DOMAINS, URLS };
\ No newline at end of file
diff --git a/src/configs/header.config.js b/src/configs/header.config.js
new file mode 100644
index 0000000..f19bb46
--- /dev/null
+++ b/src/configs/header.config.js
@@ -0,0 +1,10 @@
+const headers = {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Language": "en-US,en;q=0.5",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Connection": "keep-alive",
+ "Upgrade-Insecure-Requests": "1"
+};
+
+export { headers };
\ No newline at end of file
diff --git a/src/controllers/animeInfo.controller.js b/src/controllers/animeInfo.controller.js
new file mode 100644
index 0000000..b6c265e
--- /dev/null
+++ b/src/controllers/animeInfo.controller.js
@@ -0,0 +1,25 @@
+import { extractAnimeInfo } from "../extractors/animeInfo.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getAnimeInfo = async (req, res, next) => {
+ try {
+ const { id } = req.query;
+ if (!id) {
+ return res.status(400).json({ success: false, message: "Anime ID is required" });
+ }
+
+ const cacheKey = `anime_${id}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractAnimeInfo(id);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getAnimeInfo };
\ No newline at end of file
diff --git a/src/controllers/category.controller.js b/src/controllers/category.controller.js
new file mode 100644
index 0000000..47bb13a
--- /dev/null
+++ b/src/controllers/category.controller.js
@@ -0,0 +1,27 @@
+import { extractCategory } from "../extractors/category.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getCategory = async (req, res, next) => {
+ try {
+ const { category } = req.params;
+ const { page = 1 } = req.query;
+
+ if (!category) {
+ return res.status(400).json({ success: false, message: "Category is required" });
+ }
+
+ const cacheKey = `category_${category}_${page}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractCategory(category, page);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getCategory };
\ No newline at end of file
diff --git a/src/controllers/episodeList.controller.js b/src/controllers/episodeList.controller.js
new file mode 100644
index 0000000..3feb520
--- /dev/null
+++ b/src/controllers/episodeList.controller.js
@@ -0,0 +1,26 @@
+import { extractEpisodeList } from "../extractors/episodeList.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getEpisodeList = async (req, res, next) => {
+ try {
+ const { id } = req.params;
+
+ if (!id) {
+ return res.status(400).json({ success: false, message: "Anime ID is required" });
+ }
+
+ const cacheKey = `episodes_${id}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractEpisodeList(id);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getEpisodeList };
\ No newline at end of file
diff --git a/src/controllers/filter.controller.js b/src/controllers/filter.controller.js
new file mode 100644
index 0000000..d1b65d9
--- /dev/null
+++ b/src/controllers/filter.controller.js
@@ -0,0 +1,21 @@
+import { extractFilter } from "../extractors/filter.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getFilter = async (req, res, next) => {
+ try {
+ const params = req.query;
+ const cacheKey = `filter_${JSON.stringify(params)}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractFilter(params);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getFilter };
\ No newline at end of file
diff --git a/src/controllers/homeInfo.controller.js b/src/controllers/homeInfo.controller.js
new file mode 100644
index 0000000..43d8b9f
--- /dev/null
+++ b/src/controllers/homeInfo.controller.js
@@ -0,0 +1,20 @@
+import { extractHomeInfo } from "../extractors/homeInfo.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getHomeInfo = async (req, res, next) => {
+ try {
+ const cacheKey = "home";
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractHomeInfo();
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getHomeInfo };
\ No newline at end of file
diff --git a/src/controllers/popular.controller.js b/src/controllers/popular.controller.js
new file mode 100644
index 0000000..2cf6a76
--- /dev/null
+++ b/src/controllers/popular.controller.js
@@ -0,0 +1,21 @@
+import { extractPopular } from "../extractors/popular.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getPopular = async (req, res, next) => {
+ try {
+ const { page = 1 } = req.query;
+ const cacheKey = `popular_${page}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractPopular(page);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getPopular };
\ No newline at end of file
diff --git a/src/controllers/random.controller.js b/src/controllers/random.controller.js
new file mode 100644
index 0000000..e20c54f
--- /dev/null
+++ b/src/controllers/random.controller.js
@@ -0,0 +1,20 @@
+import { extractRandom } from "../extractors/random.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getRandom = async (req, res, next) => {
+ try {
+ const cacheKey = "random";
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractRandom();
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getRandom };
\ No newline at end of file
diff --git a/src/controllers/schedule.controller.js b/src/controllers/schedule.controller.js
new file mode 100644
index 0000000..978fe23
--- /dev/null
+++ b/src/controllers/schedule.controller.js
@@ -0,0 +1,26 @@
+import { extractSchedule } from "../extractors/schedule.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getSchedule = async (req, res, next) => {
+ try {
+ const { date } = req.query;
+
+ if (!date) {
+ return res.status(400).json({ success: false, message: "Date is required (YYYY-MM-DD)" });
+ }
+
+ const cacheKey = `schedule_${date}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractSchedule(date);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getSchedule };
\ No newline at end of file
diff --git a/src/controllers/search.controller.js b/src/controllers/search.controller.js
new file mode 100644
index 0000000..d9e74f5
--- /dev/null
+++ b/src/controllers/search.controller.js
@@ -0,0 +1,46 @@
+import { extractSearchResults, extractSearchSuggestions } from "../extractors/search.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getSearchResults = async (req, res, next) => {
+ try {
+ const { keyword, page = 1 } = req.query;
+ if (!keyword) {
+ return res.status(400).json({ success: false, message: "Keyword is required" });
+ }
+
+ const cacheKey = `search_${keyword}_${page}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractSearchResults(keyword, page);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+const getSearchSuggestions = async (req, res, next) => {
+ try {
+ const { keyword } = req.query;
+ if (!keyword) {
+ return res.status(400).json({ success: false, message: "Keyword is required" });
+ }
+
+ const cacheKey = `suggest_${keyword}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractSearchSuggestions(keyword);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getSearchResults, getSearchSuggestions };
\ No newline at end of file
diff --git a/src/controllers/spotlight.controller.js b/src/controllers/spotlight.controller.js
new file mode 100644
index 0000000..4d26180
--- /dev/null
+++ b/src/controllers/spotlight.controller.js
@@ -0,0 +1,20 @@
+import { extractSpotlight } from "../extractors/spotlight.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getSpotlight = async (req, res, next) => {
+ try {
+ const cacheKey = "spotlight";
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractSpotlight();
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getSpotlight };
\ No newline at end of file
diff --git a/src/controllers/streamInfo.controller.js b/src/controllers/streamInfo.controller.js
new file mode 100644
index 0000000..c13c812
--- /dev/null
+++ b/src/controllers/streamInfo.controller.js
@@ -0,0 +1,26 @@
+import { extractStreamInfo } from "../extractors/streamInfo.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getStreamInfo = async (req, res, next) => {
+ try {
+ const { id, server = "hd-1", type = "sub" } = req.query;
+
+ if (!id) {
+ return res.status(400).json({ success: false, message: "Episode ID is required" });
+ }
+
+ const cacheKey = `stream_${id}_${server}_${type}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractStreamInfo(id, server, type);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getStreamInfo };
\ No newline at end of file
diff --git a/src/controllers/suggestion.controller.js b/src/controllers/suggestion.controller.js
new file mode 100644
index 0000000..97dd4cf
--- /dev/null
+++ b/src/controllers/suggestion.controller.js
@@ -0,0 +1,26 @@
+import { extractSuggestions } from "../extractors/suggestion.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getSuggestions = async (req, res, next) => {
+ try {
+ const { keyword } = req.query;
+
+ if (!keyword) {
+ return res.status(400).json({ success: false, message: "Keyword is required" });
+ }
+
+ const cacheKey = `suggest_${keyword}`;
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractSuggestions(keyword);
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getSuggestions };
\ No newline at end of file
diff --git a/src/controllers/topten.controller.js b/src/controllers/topten.controller.js
new file mode 100644
index 0000000..07ef1a6
--- /dev/null
+++ b/src/controllers/topten.controller.js
@@ -0,0 +1,20 @@
+import { extractTopTen } from "../extractors/topten.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getTopTen = async (req, res, next) => {
+ try {
+ const cacheKey = "topTen";
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractTopTen();
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getTopTen };
\ No newline at end of file
diff --git a/src/controllers/trending.controller.js b/src/controllers/trending.controller.js
new file mode 100644
index 0000000..96d9fcb
--- /dev/null
+++ b/src/controllers/trending.controller.js
@@ -0,0 +1,20 @@
+import { extractTrending } from "../extractors/trending.extractor.js";
+import { getCache, setCache } from "../helper/cache.helper.js";
+
+const getTrending = async (req, res, next) => {
+ try {
+ const cacheKey = "trending";
+ const cached = getCache(cacheKey);
+ if (cached) {
+ return res.json({ success: true, results: cached });
+ }
+
+ const data = await extractTrending();
+ setCache(cacheKey, data);
+ res.json({ success: true, results: data });
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { getTrending };
\ No newline at end of file
diff --git a/src/extractors/animeInfo.extractor.js b/src/extractors/animeInfo.extractor.js
new file mode 100644
index 0000000..59a21a7
--- /dev/null
+++ b/src/extractors/animeInfo.extractor.js
@@ -0,0 +1,103 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { BASE_URL } from "../configs/dataUrl.js";
+
+const extractAnimeInfo = async (id) => {
+ try {
+ const url = `${BASE_URL}/anime/${id}`;
+ const { data } = await axios.get(url, { headers });
+ const $ = cheerio.load(data);
+
+ const title = $(".film-name h1, .anisc-detail .film-name").text().trim() || "";
+ const japaneseTitle = $(".film-name .alternate-title, .anisc-detail .film-name small").text().trim() || "";
+ const poster = $(".film-poster img").attr("src") || "";
+ const showType = $(".film-info .row .col1 .item:first-child .name").text().trim() || "";
+ const quality = $(".film-info .row .col1 .item:nth-child(2) .name").text().trim() || "";
+
+ const animeInfo = {};
+ $(".anisc-info .item").each((i, el) => {
+ const label = $(el).find(".name").text().trim().replace(":", "");
+ const value = $(el).find(".text").text().trim();
+ if (label && value) {
+ animeInfo[label] = value;
+ }
+ });
+
+ const overview = $(".anisc-detail .description, .film-description .text").text().trim() || "";
+
+ const genres = [];
+ $(".anisc-info .item:last-child a, .genres a").each((i, el) => {
+ const genre = $(el).text().trim();
+ if (genre) genres.push(genre);
+ });
+
+ const seasons = [];
+ $(".seasons .season-item, .ss-list a").each((i, el) => {
+ const seasonId = $(el).attr("href")?.split("/").pop() || "";
+ const seasonTitle = $(el).text().trim() || "";
+ const seasonPoster = $(el).find("img").attr("src") || "";
+
+ if (seasonId) {
+ seasons.push({
+ id: seasonId,
+ title: seasonTitle,
+ poster: seasonPoster
+ });
+ }
+ });
+
+ const relatedData = [];
+ $(".related-content .film-detail, .film-list-block .film-detail").each((i, el) => {
+ const relId = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const relPoster = $(el).find("img").attr("src") || "";
+ const relTitle = $(el).find(".film-name a").text().trim() || "";
+ const relType = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+
+ if (relId) {
+ relatedData.push({
+ id: relId,
+ poster: relPoster,
+ title: relTitle,
+ type: relType
+ });
+ }
+ });
+
+ const recommendedData = [];
+ $(".recommend-content .film-detail, .film-list-block .film-detail").each((i, el) => {
+ const recId = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const recPoster = $(el).find("img").attr("src") || "";
+ const recTitle = $(el).find(".film-name a").text().trim() || "";
+ const recType = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+
+ if (recId) {
+ recommendedData.push({
+ id: recId,
+ poster: recPoster,
+ title: recTitle,
+ type: recType
+ });
+ }
+ });
+
+ return {
+ id,
+ title,
+ japaneseTitle,
+ poster,
+ showType,
+ quality,
+ overview,
+ animeInfo,
+ genres,
+ seasons,
+ relatedData,
+ recommendedData
+ };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractAnimeInfo };
\ No newline at end of file
diff --git a/src/extractors/category.extractor.js b/src/extractors/category.extractor.js
new file mode 100644
index 0000000..c261b8f
--- /dev/null
+++ b/src/extractors/category.extractor.js
@@ -0,0 +1,44 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+import { countPages } from "../helper/countPages.helper.js";
+import { extractPages } from "../helper/extractPages.helper.js";
+
+const extractCategory = async (category, page = 1) => {
+ try {
+ const url = URLS.category(category);
+ const $ = await extractPages(url, page);
+ const totalPages = countPages($);
+
+ const results = [];
+ $(".film_list-wrap .flw-item, .film-detail").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+ const showType = $(el).find(".fd-infor .fdi-item:nth-child(2)").text().trim() || "";
+ const sub = parseInt($(el).find(".tick-sub").text().trim()) || 0;
+ const dub = parseInt($(el).find(".tick-dub").text().trim()) || 0;
+ const eps = parseInt($(el).find(".tick-eps").text().trim()) || 0;
+ const adultContent = $(el).find(".tick-adult, .adult-badge").length > 0;
+
+ if (id) {
+ results.push({
+ id,
+ poster,
+ title,
+ japaneseTitle,
+ tvInfo: { showType, sub, dub, eps },
+ adultContent
+ });
+ }
+ });
+
+ return { totalPages, data: results };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractCategory };
\ No newline at end of file
diff --git a/src/extractors/episodeList.extractor.js b/src/extractors/episodeList.extractor.js
new file mode 100644
index 0000000..d3343c2
--- /dev/null
+++ b/src/extractors/episodeList.extractor.js
@@ -0,0 +1,40 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { BASE_URL } from "../configs/dataUrl.js";
+
+const extractEpisodeList = async (id) => {
+ try {
+ const url = `${BASE_URL}/anime/${id}`;
+ const { data } = await axios.get(url, { headers });
+ const $ = cheerio.load(data);
+
+ const episodes = [];
+ $(".episodes-list .ep-item, .ss-list a").each((i, el) => {
+ const epId = $(el).attr("data-id") || $(el).attr("href")?.split("/").pop() || "";
+ const epNumber = parseInt($(el).find(".ep-number, .ep-num").text().trim()) || i + 1;
+ const title = $(el).find(".ep-title, .ep-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".ep-jp").text().trim() || "";
+ const filler = $(el).hasClass("filler") || false;
+
+ if (epId) {
+ episodes.push({
+ id: epId,
+ episode_no: epNumber,
+ title,
+ japaneseTitle,
+ filler
+ });
+ }
+ });
+
+ return {
+ totalEpisodes: episodes.length,
+ episodes
+ };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractEpisodeList };
\ No newline at end of file
diff --git a/src/extractors/filter.extractor.js b/src/extractors/filter.extractor.js
new file mode 100644
index 0000000..8244fbf
--- /dev/null
+++ b/src/extractors/filter.extractor.js
@@ -0,0 +1,60 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { BASE_URL } from "../configs/dataUrl.js";
+import { countPages } from "../helper/countPages.helper.js";
+import { extractPages } from "../helper/extractPages.helper.js";
+
+const extractFilter = async (params) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ if (params.type) queryParams.set("type", params.type);
+ if (params.status) queryParams.set("status", params.status);
+ if (params.rated) queryParams.set("rated", params.rated);
+ if (params.score) queryParams.set("score", params.score);
+ if (params.season) queryParams.set("season", params.season);
+ if (params.language) queryParams.set("language", params.language);
+ if (params.genres) queryParams.set("genres", params.genres);
+ if (params.sort) queryParams.set("sort", params.sort);
+ if (params.sy) queryParams.set("sy", params.sy);
+ if (params.sm) queryParams.set("sm", params.sm);
+ if (params.sd) queryParams.set("sd", params.sd);
+ if (params.ey) queryParams.set("ey", params.ey);
+ if (params.em) queryParams.set("em", params.em);
+ if (params.ed) queryParams.set("ed", params.ed);
+ if (params.keyword) queryParams.set("keyword", params.keyword);
+
+ const url = `${BASE_URL}/filter?${queryParams.toString()}`;
+ const $ = await extractPages(url, params.page || 1);
+ const totalPages = countPages($);
+
+ const results = [];
+ $(".film_list-wrap .flw-item, .film-detail").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+ const showType = $(el).find(".fd-infor .fdi-item:nth-child(2)").text().trim() || "";
+ const sub = parseInt($(el).find(".tick-sub").text().trim()) || 0;
+ const dub = parseInt($(el).find(".tick-dub").text().trim()) || 0;
+ const eps = parseInt($(el).find(".tick-eps").text().trim()) || 0;
+
+ if (id) {
+ results.push({
+ id,
+ poster,
+ title,
+ japaneseTitle,
+ tvInfo: { showType, sub, dub, eps }
+ });
+ }
+ });
+
+ return { totalPages, data: results };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractFilter };
\ No newline at end of file
diff --git a/src/extractors/homeInfo.extractor.js b/src/extractors/homeInfo.extractor.js
new file mode 100644
index 0000000..d06b75f
--- /dev/null
+++ b/src/extractors/homeInfo.extractor.js
@@ -0,0 +1,124 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+
+const extractHomeInfo = async () => {
+ try {
+ const { data } = await axios.get(URLS.home, { headers });
+ const $ = cheerio.load(data);
+
+ const spotlights = [];
+ $(".swiper-slide, .spotlight-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fdi-item:nth-child(1)").text().trim() || "";
+ const description = $(el).find(".fd-infor .desc, .film-description").text().trim() || "";
+ const showType = $(el).find(".fdi-item:nth-child(2)").text().trim() || "";
+ const duration = $(el).find(".fdi-item:nth-child(3)").text().trim() || "";
+ const releaseDate = $(el).find(".fdi-item:nth-child(4)").text().trim() || "";
+
+ if (id) {
+ spotlights.push({
+ id,
+ poster,
+ title,
+ japaneseTitle,
+ description,
+ tvInfo: { showType, duration, releaseDate }
+ });
+ }
+ });
+
+ const trending = [];
+ $(".trending-list .film-detail, .trending-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const number = parseInt($(el).find(".number").text().trim()) || i + 1;
+
+ if (id) {
+ trending.push({ id, number, poster, title });
+ }
+ });
+
+ const topAiring = [];
+ $(".section-id-02 .film-detail, .top-airing .film-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+
+ if (id) {
+ topAiring.push({ id, poster, title, japaneseTitle });
+ }
+ });
+
+ const mostPopular = [];
+ $(".section-id-03 .film-detail, .popular .film-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+
+ if (id) {
+ mostPopular.push({ id, poster, title });
+ }
+ });
+
+ const mostFavorite = [];
+ $(".section-id-04 .film-detail, .favorite .film-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+
+ if (id) {
+ mostFavorite.push({ id, poster, title });
+ }
+ });
+
+ const latestCompleted = [];
+ $(".section-id-05 .film-detail, .completed .film-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+
+ if (id) {
+ latestCompleted.push({ id, poster, title });
+ }
+ });
+
+ const latestEpisode = [];
+ $(".section-id-06 .film-detail, .latest .film-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const episode = $(el).find(".tick-eps, .tick-sub").text().trim() || "";
+
+ if (id) {
+ latestEpisode.push({ id, poster, title, episode });
+ }
+ });
+
+ const genres = [];
+ $(".genre-item, .genres a").each((i, el) => {
+ const genre = $(el).text().trim();
+ if (genre) genres.push(genre);
+ });
+
+ return {
+ spotlights,
+ trending,
+ topAiring,
+ mostPopular,
+ mostFavorite,
+ latestCompleted,
+ latestEpisode,
+ genres
+ };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractHomeInfo };
\ No newline at end of file
diff --git a/src/extractors/popular.extractor.js b/src/extractors/popular.extractor.js
new file mode 100644
index 0000000..2e073fb
--- /dev/null
+++ b/src/extractors/popular.extractor.js
@@ -0,0 +1,41 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+import { countPages } from "../helper/countPages.helper.js";
+import { extractPages } from "../helper/extractPages.helper.js";
+
+const extractPopular = async (page = 1) => {
+ try {
+ const $ = await extractPages(URLS.mostPopular, page);
+ const totalPages = countPages($);
+
+ const results = [];
+ $(".film_list-wrap .flw-item, .film-detail").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+ const showType = $(el).find(".fd-infor .fdi-item:nth-child(2)").text().trim() || "";
+ const sub = parseInt($(el).find(".tick-sub").text().trim()) || 0;
+ const dub = parseInt($(el).find(".tick-dub").text().trim()) || 0;
+ const eps = parseInt($(el).find(".tick-eps").text().trim()) || 0;
+
+ if (id) {
+ results.push({
+ id,
+ poster,
+ title,
+ japaneseTitle,
+ tvInfo: { showType, sub, dub, eps }
+ });
+ }
+ });
+
+ return { totalPages, data: results };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractPopular };
\ No newline at end of file
diff --git a/src/extractors/random.extractor.js b/src/extractors/random.extractor.js
new file mode 100644
index 0000000..89293fb
--- /dev/null
+++ b/src/extractors/random.extractor.js
@@ -0,0 +1,40 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { BASE_URL } from "../configs/dataUrl.js";
+
+const extractRandom = async () => {
+ try {
+ const { data } = await axios.get(`${BASE_URL}/random`, {
+ headers,
+ maxRedirects: 5
+ });
+ const $ = cheerio.load(data);
+
+ const title = $(".film-name h1, .anisc-detail .film-name").text().trim() || "";
+ const japaneseTitle = $(".film-name .alternate-title, .anisc-detail .film-name small").text().trim() || "";
+ const poster = $(".film-poster img").attr("src") || "";
+ const showType = $(".film-info .row .col1 .item:first-child .name").text().trim() || "";
+
+ const animeInfo = {};
+ $(".anisc-info .item").each((i, el) => {
+ const label = $(el).find(".name").text().trim().replace(":", "");
+ const value = $(el).find(".text").text().trim();
+ if (label && value) {
+ animeInfo[label] = value;
+ }
+ });
+
+ return {
+ title,
+ japaneseTitle,
+ poster,
+ showType,
+ animeInfo
+ };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractRandom };
\ No newline at end of file
diff --git a/src/extractors/schedule.extractor.js b/src/extractors/schedule.extractor.js
new file mode 100644
index 0000000..2703090
--- /dev/null
+++ b/src/extractors/schedule.extractor.js
@@ -0,0 +1,39 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+
+const extractSchedule = async (date) => {
+ try {
+ const url = URLS.schedule(date);
+ const { data } = await axios.get(url, { headers });
+ const $ = cheerio.load(data);
+
+ const schedule = [];
+ $(".schedule-item, .anime-schedule .item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+ const time = $(el).find(".time, .schedule-time").text().trim() || "";
+ const episodeNo = parseInt($(el).find(".episode-no, .ep-num").text().trim()) || 0;
+ const releaseDate = $(el).find(".date, .release-date").text().trim() || "";
+
+ if (id) {
+ schedule.push({
+ id,
+ title,
+ japaneseTitle,
+ time,
+ episode_no: episodeNo,
+ releaseDate
+ });
+ }
+ });
+
+ return schedule;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractSchedule };
\ No newline at end of file
diff --git a/src/extractors/search.extractor.js b/src/extractors/search.extractor.js
new file mode 100644
index 0000000..bf0251c
--- /dev/null
+++ b/src/extractors/search.extractor.js
@@ -0,0 +1,67 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+import { countPages } from "../helper/countPages.helper.js";
+
+import { extractPages } from "../helper/extractPages.helper.js";
+
+const extractSearchResults = async (keyword, page = 1) => {
+ try {
+ const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
+ const $ = await extractPages(url, page);
+ const totalPages = countPages($);
+
+ const results = [];
+ $(".film_list-wrap .flw-item, .search-list .film-detail").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+ const showType = $(el).find(".fd-infor .fdi-item:nth-child(2)").text().trim() || "";
+ const sub = parseInt($(el).find(".tick-sub").text().trim()) || 0;
+ const dub = parseInt($(el).find(".tick-dub").text().trim()) || 0;
+ const eps = parseInt($(el).find(".tick-eps").text().trim()) || 0;
+
+ if (id) {
+ results.push({
+ id,
+ poster,
+ title,
+ japaneseTitle,
+ tvInfo: { showType, sub, dub, eps }
+ });
+ }
+ });
+
+ return { totalPages, data: results };
+ } catch (error) {
+ throw error;
+ }
+};
+
+const extractSearchSuggestions = async (keyword) => {
+ try {
+ const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
+ const { data } = await axios.get(url, { headers });
+ const $ = cheerio.load(data);
+
+ const suggestions = [];
+ $(".search-suggest .film-detail, .nav-item a").each((i, el) => {
+ const id = $(el).attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name, .dynamic-name").text().trim() || "";
+ const showType = $(el).find(".fdi-item:first-child").text().trim() || "";
+
+ if (id) {
+ suggestions.push({ id, poster, title, showType });
+ }
+ });
+
+ return suggestions;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractSearchResults, extractSearchSuggestions };
\ No newline at end of file
diff --git a/src/extractors/spotlight.extractor.js b/src/extractors/spotlight.extractor.js
new file mode 100644
index 0000000..d4dd6a8
--- /dev/null
+++ b/src/extractors/spotlight.extractor.js
@@ -0,0 +1,40 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+
+const extractSpotlight = async () => {
+ try {
+ const { data } = await axios.get(URLS.home, { headers });
+ const $ = cheerio.load(data);
+
+ const spotlights = [];
+ $(".swiper-slide, .spotlight-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fdi-item:nth-child(1)").text().trim() || "";
+ const description = $(el).find(".fd-infor .desc, .film-description").text().trim() || "";
+ const showType = $(el).find(".fdi-item:nth-child(2)").text().trim() || "";
+ const duration = $(el).find(".fdi-item:nth-child(3)").text().trim() || "";
+ const releaseDate = $(el).find(".fdi-item:nth-child(4)").text().trim() || "";
+
+ if (id) {
+ spotlights.push({
+ id,
+ poster,
+ title,
+ japaneseTitle,
+ description,
+ tvInfo: { showType, duration, releaseDate }
+ });
+ }
+ });
+
+ return spotlights;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractSpotlight };
\ No newline at end of file
diff --git a/src/extractors/streamInfo.extractor.js b/src/extractors/streamInfo.extractor.js
new file mode 100644
index 0000000..6d70eab
--- /dev/null
+++ b/src/extractors/streamInfo.extractor.js
@@ -0,0 +1,58 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { BASE_URL } from "../configs/dataUrl.js";
+
+const extractStreamInfo = async (episodeId, server = "hd-1", type = "sub") => {
+ try {
+ const url = `${BASE_URL}/watch/${episodeId}`;
+ const { data } = await axios.get(url, { headers });
+ const $ = cheerio.load(data);
+
+ const servers = [];
+ $(".servers .server-item, .type-list .server-item").each((i, el) => {
+ const serverId = $(el).attr("data-id") || $(el).attr("data-server-id") || "";
+ const serverName = $(el).text().trim() || "";
+ const serverType = $(el).hasClass("sub") ? "sub" : "dub";
+
+ if (serverId) {
+ servers.push({ id: serverId, name: serverName, type: serverType });
+ }
+ });
+
+ const sources = [];
+ $(".player-source .source-item, .embed-source").each((i, el) => {
+ const sourceId = $(el).attr("data-id") || "";
+ const sourceUrl = $(el).attr("data-src") || $(el).find("iframe").attr("src") || "";
+ const sourceType = $(el).attr("data-type") || "hls";
+
+ if (sourceId || sourceUrl) {
+ sources.push({ id: sourceId, url: sourceUrl, type: sourceType });
+ }
+ });
+
+ const subtitles = [];
+ $(".player-sub .sub-item, .subtitle-item").each((i, el) => {
+ const subUrl = $(el).attr("data-url") || $(el).attr("src") || "";
+ const subLabel = $(el).text().trim() || "";
+ const subLang = $(el).attr("data-lang") || "en";
+
+ if (subUrl) {
+ subtitles.push({ url: subUrl, label: subLabel, language: subLang });
+ }
+ });
+
+ return {
+ episodeId,
+ server,
+ type,
+ servers,
+ sources,
+ subtitles
+ };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractStreamInfo };
\ No newline at end of file
diff --git a/src/extractors/suggestion.extractor.js b/src/extractors/suggestion.extractor.js
new file mode 100644
index 0000000..d6ab880
--- /dev/null
+++ b/src/extractors/suggestion.extractor.js
@@ -0,0 +1,32 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+
+const extractSuggestions = async (keyword) => {
+ try {
+ const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
+ const { data } = await axios.get(url, { headers });
+ const $ = cheerio.load(data);
+
+ const suggestions = [];
+ $(".search-suggest .film-detail, .nav-item a").each((i, el) => {
+ const id = $(el).attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+ const showType = $(el).find(".fdi-item:first-child").text().trim() || "";
+ const releaseDate = $(el).find(".fdi-item:nth-child(2)").text().trim() || "";
+
+ if (id) {
+ suggestions.push({ id, poster, title, japaneseTitle, showType, releaseDate });
+ }
+ });
+
+ return suggestions;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractSuggestions };
\ No newline at end of file
diff --git a/src/extractors/topten.extractor.js b/src/extractors/topten.extractor.js
new file mode 100644
index 0000000..1a28f76
--- /dev/null
+++ b/src/extractors/topten.extractor.js
@@ -0,0 +1,57 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+
+const extractTopTen = async () => {
+ try {
+ const { data } = await axios.get(URLS.topTen, { headers });
+ const $ = cheerio.load(data);
+
+ const today = [];
+ const week = [];
+ const month = [];
+
+ $(".top-ten-list.today .top-ten-item, #top-viewed-day .top-list-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const name = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const number = parseInt($(el).find(".number").text().trim()) || i + 1;
+ const showType = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+
+ if (id) {
+ today.push({ id, number, name, poster, tvInfo: { showType } });
+ }
+ });
+
+ $(".top-ten-list.week .top-ten-item, #top-viewed-week .top-list-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const name = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const number = parseInt($(el).find(".number").text().trim()) || i + 1;
+ const showType = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+
+ if (id) {
+ week.push({ id, number, name, poster, tvInfo: { showType } });
+ }
+ });
+
+ $(".top-ten-list.month .top-ten-item, #top-viewed-month .top-list-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const name = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const number = parseInt($(el).find(".number").text().trim()) || i + 1;
+ const showType = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+
+ if (id) {
+ month.push({ id, number, name, poster, tvInfo: { showType } });
+ }
+ });
+
+ return { today, week, month };
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractTopTen };
\ No newline at end of file
diff --git a/src/extractors/trending.extractor.js b/src/extractors/trending.extractor.js
new file mode 100644
index 0000000..7ecdc4c
--- /dev/null
+++ b/src/extractors/trending.extractor.js
@@ -0,0 +1,30 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+import { URLS } from "../configs/dataUrl.js";
+
+const extractTrending = async () => {
+ try {
+ const { data } = await axios.get(URLS.home, { headers });
+ const $ = cheerio.load(data);
+
+ const trending = [];
+ $(".trending-list .film-detail, .trending-item").each((i, el) => {
+ const id = $(el).find("a").attr("href")?.split("/").pop() || "";
+ const poster = $(el).find("img").attr("src") || "";
+ const title = $(el).find(".film-name a, .dynamic-name").text().trim() || "";
+ const japaneseTitle = $(el).find(".fd-infor .fdi-item:first-child").text().trim() || "";
+ const number = parseInt($(el).find(".number").text().trim()) || i + 1;
+
+ if (id) {
+ trending.push({ id, number, poster, title, japaneseTitle });
+ }
+ });
+
+ return trending;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractTrending };
\ No newline at end of file
diff --git a/src/helper/cache.helper.js b/src/helper/cache.helper.js
new file mode 100644
index 0000000..86e5812
--- /dev/null
+++ b/src/helper/cache.helper.js
@@ -0,0 +1,21 @@
+const cache = new Map();
+const CACHE_TTL = 5 * 60 * 1000;
+
+const getCache = (key) => {
+ const item = cache.get(key);
+ if (item && Date.now() - item.timestamp < CACHE_TTL) {
+ return item.data;
+ }
+ cache.delete(key);
+ return null;
+};
+
+const setCache = (key, data) => {
+ cache.set(key, { data, timestamp: Date.now() });
+};
+
+const clearCache = () => {
+ cache.clear();
+};
+
+export { getCache, setCache, clearCache };
\ No newline at end of file
diff --git a/src/helper/countPages.helper.js b/src/helper/countPages.helper.js
new file mode 100644
index 0000000..b7b1b42
--- /dev/null
+++ b/src/helper/countPages.helper.js
@@ -0,0 +1,12 @@
+const countPages = ($) => {
+ const pages = [];
+ $(".pagination li a").each((i, el) => {
+ const text = $(el).text().trim();
+ if (!isNaN(text) && text !== "") {
+ pages.push(parseInt(text));
+ }
+ });
+ return pages.length > 0 ? Math.max(...pages) : 1;
+};
+
+export { countPages };
\ No newline at end of file
diff --git a/src/helper/extractPages.helper.js b/src/helper/extractPages.helper.js
new file mode 100644
index 0000000..677dced
--- /dev/null
+++ b/src/helper/extractPages.helper.js
@@ -0,0 +1,16 @@
+import * as cheerio from "cheerio";
+import axios from "axios";
+import { headers } from "../configs/header.config.js";
+
+const extractPages = async (url, page = 1) => {
+ try {
+ const finalUrl = page > 1 ? `${url}?page=${page}` : url;
+ const { data } = await axios.get(finalUrl, { headers });
+ const $ = cheerio.load(data);
+ return $;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export { extractPages };
\ No newline at end of file
diff --git a/src/helper/formatTitle.helper.js b/src/helper/formatTitle.helper.js
new file mode 100644
index 0000000..eb438d2
--- /dev/null
+++ b/src/helper/formatTitle.helper.js
@@ -0,0 +1,9 @@
+const formatTitle = (title) => {
+ if (!title) return "";
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+};
+
+export { formatTitle };
\ No newline at end of file
diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js
new file mode 100644
index 0000000..36867f1
--- /dev/null
+++ b/src/routes/apiRoutes.js
@@ -0,0 +1,132 @@
+import { getHomeInfo } from "../controllers/homeInfo.controller.js";
+import { getAnimeInfo } from "../controllers/animeInfo.controller.js";
+import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
+import { getEpisodeList } from "../controllers/episodeList.controller.js";
+import { getStreamInfo } from "../controllers/streamInfo.controller.js";
+import { getSchedule } from "../controllers/schedule.controller.js";
+import { getSpotlight } from "../controllers/spotlight.controller.js";
+import { getTrending } from "../controllers/trending.controller.js";
+import { getTopTen } from "../controllers/topten.controller.js";
+import { getSuggestions } from "../controllers/suggestion.controller.js";
+import { getRandom } from "../controllers/random.controller.js";
+import { getPopular } from "../controllers/popular.controller.js";
+import { getFilter } from "../controllers/filter.controller.js";
+import { categoryRoutes } from "./category.route.js";
+
+const createApiRoutes = (app, jsonResponse, jsonError) => {
+ app.get("/api/", async (req, res, next) => {
+ try {
+ await getHomeInfo(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/info", async (req, res, next) => {
+ try {
+ await getAnimeInfo(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/search", async (req, res, next) => {
+ try {
+ await getSearchResults(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/search/suggest", async (req, res, next) => {
+ try {
+ await getSearchSuggestions(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/episodes/:id", async (req, res, next) => {
+ try {
+ await getEpisodeList(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/stream", async (req, res, next) => {
+ try {
+ await getStreamInfo(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/schedule", async (req, res, next) => {
+ try {
+ await getSchedule(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/spotlight", async (req, res, next) => {
+ try {
+ await getSpotlight(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/trending", async (req, res, next) => {
+ try {
+ await getTrending(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/top-ten", async (req, res, next) => {
+ try {
+ await getTopTen(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/suggestions", async (req, res, next) => {
+ try {
+ await getSuggestions(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/random", async (req, res, next) => {
+ try {
+ await getRandom(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/most-popular", async (req, res, next) => {
+ try {
+ await getPopular(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ app.get("/api/filter", async (req, res, next) => {
+ try {
+ await getFilter(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+
+ categoryRoutes(app, jsonResponse, jsonError);
+};
+
+export { createApiRoutes };
\ No newline at end of file
diff --git a/src/routes/category.route.js b/src/routes/category.route.js
new file mode 100644
index 0000000..f7c7921
--- /dev/null
+++ b/src/routes/category.route.js
@@ -0,0 +1,13 @@
+import { getCategory } from "../controllers/category.controller.js";
+
+const categoryRoutes = (app, jsonResponse, jsonError) => {
+ app.get("/api/:category", async (req, res, next) => {
+ try {
+ await getCategory(req, res, next);
+ } catch (error) {
+ jsonError(res, error.message);
+ }
+ });
+};
+
+export { categoryRoutes };
\ No newline at end of file
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..549ebf7
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,18 @@
+{
+ "version": 2,
+ "builds": [
+ {
+ "src": "server.js",
+ "use": "@vercel/node"
+ }
+ ],
+ "routes": [
+ {
+ "src": "/(.*)",
+ "dest": "server.js",
+ "headers": {
+ "Access-Control-Allow-Origin": "*"
+ }
+ }
+ ]
+}
\ No newline at end of file