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
This commit is contained in:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-08 15:08:22 +05:30
commit 3815521b0d
47 changed files with 1846 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Origins you want to allow
ALLOWED_ORIGINS=https://site1.com,https://site2.com
# Port (optional, defaults to 4444)
PORT=4444
+4
View File
@@ -0,0 +1,4 @@
node_modules/
.env
.DS_Store
*.log
+35
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 4444
CMD ["npm", "start"]
+21
View File
@@ -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.
+29
View File
@@ -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"
}
}
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Not Found | AniKatoAPI</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
text-align: center;
padding: 40px;
}
h1 {
font-size: 8rem;
margin-bottom: 20px;
background: linear-gradient(90deg, #e94560, #0f3460);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
h2 {
font-size: 2rem;
margin-bottom: 20px;
color: #a0a0a0;
}
p {
color: #a0a0a0;
margin-bottom: 30px;
}
a {
display: inline-block;
background: #e94560;
color: #fff;
padding: 12px 30px;
text-decoration: none;
border-radius: 25px;
transition: transform 0.3s, box-shadow 0.3s;
}
a:hover {
transform: translateY(-3px);
box-shadow 0 10px 20px rgba(233, 69, 96, 0.3);
}
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<h2>Endpoint Not Found</h2>
<p>The API endpoint you're looking for doesn't exist.</p>
<a href="/">Go to API Documentation</a>
</div>
</body>
</html>
+218
View File
@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AniKatoAPI - Anime API</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
padding: 40px 0;
}
h1 {
font-size: 3rem;
margin-bottom: 10px;
background: linear-gradient(90deg, #e94560, #0f3460);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: #a0a0a0;
font-size: 1.2rem;
}
.api-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 30px;
margin: 20px 0;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.api-section h2 {
color: #e94560;
margin-bottom: 20px;
font-size: 1.5rem;
}
.endpoint {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 15px;
margin: 10px 0;
font-family: 'Courier New', monospace;
}
.method {
background: #e94560;
color: #fff;
padding: 3px 8px;
border-radius: 4px;
font-weight: bold;
margin-right: 10px;
}
.path {
color: #0f3460;
font-weight: bold;
}
.description {
color: #a0a0a0;
margin-top: 10px;
font-size: 0.9rem;
}
.footer {
text-align: center;
padding: 40px 0;
color: #a0a0a0;
}
.badge {
display: inline-block;
background: #e94560;
color: #fff;
padding: 5px 15px;
border-radius: 20px;
margin: 5px;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>AniKatoAPI</h1>
<p class="subtitle">RESTful API for Anime Data from anikototv.to</p>
<div style="margin-top: 20px;">
<span class="badge">Node.js</span>
<span class="badge">Express</span>
<span class="badge">Cheerio</span>
<span class="badge">Vercel</span>
</div>
</header>
<div class="api-section">
<h2>Base URL</h2>
<div class="endpoint">
<span class="path">https://anikatoapi.vercel.app/api</span>
</div>
</div>
<div class="api-section">
<h2>API Endpoints</h2>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/</span>
<p class="description">Get home page data including spotlight, trending, top airing, etc.</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/info?id={anime-id}</span>
<p class="description">Get detailed information about a specific anime</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/search?keyword={keyword}&page={page}</span>
<p class="description">Search for anime by keyword</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/search/suggest?keyword={keyword}</span>
<p class="description">Get search suggestions</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/episodes/{anime-id}</span>
<p class="description">Get episode list for an anime</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/stream?id={episode-id}&server={server}&type={sub/dub}</span>
<p class="description">Get streaming information for an episode</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/schedule?date={YYYY-MM-DD}</span>
<p class="description">Get anime schedule for a specific date</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/spotlight</span>
<p class="description">Get spotlight/featured anime</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/trending</span>
<p class="description">Get trending anime</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/top-ten</span>
<p class="description">Get top 10 anime (today, week, month)</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/suggestions?keyword={keyword}</span>
<p class="description">Get anime suggestions</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/random</span>
<p class="description">Get random anime information</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/most-popular?page={page}</span>
<p class="description">Get most popular anime</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/filter?{params}</span>
<p class="description">Filter anime by type, status, genre, etc.</p>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/{category}?page={page}</span>
<p class="description">Get anime by category (movie, tv, ova, ona, etc.)</p>
</div>
</div>
<div class="api-section">
<h2>Categories</h2>
<div class="endpoint">
<span class="path">top-airing, most-popular, most-favorite, completed, recently-updated, recently-added, subbed-anime, dubbed-anime, movie, tv, ova, ona, special, music</span>
</div>
</div>
<div class="footer">
<p>Made by Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ</p>
<p style="margin-top: 10px; font-size: 0.8rem;">This API is for educational purposes only</p>
</div>
</div>
</body>
</html>
+9
View File
@@ -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
+63
View File
@@ -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}`);
});
+34
View File
@@ -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 };
+10
View File
@@ -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 };
+25
View File
@@ -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 };
+27
View File
@@ -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 };
+26
View File
@@ -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 };
+21
View File
@@ -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 };
+20
View File
@@ -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 };
+21
View File
@@ -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 };
+20
View File
@@ -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 };
+26
View File
@@ -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 };
+46
View File
@@ -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 };
+20
View File
@@ -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 };
+26
View File
@@ -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 };
+26
View File
@@ -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 };
+20
View File
@@ -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 };
+20
View File
@@ -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 };
+103
View File
@@ -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 };
+44
View File
@@ -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 };
+40
View File
@@ -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 };
+60
View File
@@ -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 };
+124
View File
@@ -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 };
+41
View File
@@ -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 };
+40
View File
@@ -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 };
+39
View File
@@ -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 };
+67
View File
@@ -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 };
+40
View File
@@ -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 };
+58
View File
@@ -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 };
+32
View File
@@ -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 };
+57
View File
@@ -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 };
+30
View File
@@ -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 };
+21
View File
@@ -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 };
+12
View File
@@ -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 };
+16
View File
@@ -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 };
+9
View File
@@ -0,0 +1,9 @@
const formatTitle = (title) => {
if (!title) return "";
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
};
export { formatTitle };
+132
View File
@@ -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 };
+13
View File
@@ -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 };
+18
View File
@@ -0,0 +1,18 @@
{
"version": 2,
"builds": [
{
"src": "server.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "server.js",
"headers": {
"Access-Control-Allow-Origin": "*"
}
}
]
}