mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 02:45:10 +00:00
v1.5.0: Complete documentation rewrite - 50+ files with AlisaReactionBot style
- Box-style header comments (Project, Author, License) on every file - Section headers using double-line box decorators - Feature markers (// ---- FEATURE: XYZ ----) before every major function - JSDoc comments for EVERY function with @param, @returns, @example - Inline notes (NOTE, WARNING, TIP) for non-obvious logic - Footer END markers on every module - Matched code style to AlisaReactionBot repository - All files now searchable by feature name via grep Author: Shinei Nouzen <157171073+Shineii86@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,19 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.5.0] - 2026-06-08
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Complete documentation rewrite of ALL source files (50+ files)
|
||||||
|
- Added box-style header comments (Project, Author, License) to every file
|
||||||
|
- Added section headers using double-line box decorators (`═════`)
|
||||||
|
- Added feature markers (`// ---- FEATURE: XYZ ----`) before every major function
|
||||||
|
- Added JSDoc comments for EVERY function with @param, @returns, @example
|
||||||
|
- Added inline notes (NOTE, WARNING, TIP) for non-obvious logic
|
||||||
|
- Added footer END markers to every module/file
|
||||||
|
- Matched code style to AlisaReactionBot repository format
|
||||||
|
- All files now searchable by feature name via grep
|
||||||
|
|
||||||
## [1.4.0] - 2026-06-08
|
## [1.4.0] - 2026-06-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — server.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Main entry point for the AniKatoAPI Express server.
|
||||||
|
* Configures CORS, middleware, static files, API routes,
|
||||||
|
* and 404 handling. Starts the server on the configured port.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* None (side-effect: starts Express server)
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
@@ -9,12 +27,21 @@ import { createApiRoutes } from "./src/routes/apiRoutes.js";
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SERVER CONFIGURATION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 4444;
|
const PORT = process.env.PORT || 4444;
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const publicDir = path.join(dirname(__filename), "public");
|
const publicDir = path.join(dirname(__filename), "public");
|
||||||
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
|
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CORS MIDDLEWARE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// NOTE: CORS is configured to only allow GET requests from specified origins
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: allowedOrigins?.includes("*") ? "*" : allowedOrigins || [],
|
origin: allowedOrigins?.includes("*") ? "*" : allowedOrigins || [],
|
||||||
@@ -22,6 +49,7 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---- FEATURE: Custom origin validation middleware ----
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const origin = req.headers.origin;
|
const origin = req.headers.origin;
|
||||||
if (
|
if (
|
||||||
@@ -39,16 +67,50 @@ app.use((req, res, next) => {
|
|||||||
.json({ success: false, message: "Forbidden: Origin not allowed" });
|
.json({ success: false, message: "Forbidden: Origin not allowed" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STATIC FILES
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// NOTE: redirect: false prevents automatic redirects to index.html
|
||||||
app.use(express.static(publicDir, { redirect: false }));
|
app.use(express.static(publicDir, { redirect: false }));
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// RESPONSE HELPERS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Standardized JSON response wrapper ----
|
||||||
|
/**
|
||||||
|
* Wraps data in a standardized success JSON response.
|
||||||
|
*
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {*} data - The data to return in the response
|
||||||
|
* @param {number} status - HTTP status code (default: 200)
|
||||||
|
*/
|
||||||
const jsonResponse = (res, data, status = 200) =>
|
const jsonResponse = (res, data, status = 200) =>
|
||||||
res.status(status).json({ success: true, results: data });
|
res.status(status).json({ success: true, results: data });
|
||||||
|
|
||||||
|
// ---- FEATURE: Standardized error response wrapper ----
|
||||||
|
/**
|
||||||
|
* Returns a standardized error JSON response.
|
||||||
|
*
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {string} message - Error message to return (default: "Internal server error")
|
||||||
|
* @param {number} status - HTTP status code (default: 500)
|
||||||
|
*/
|
||||||
const jsonError = (res, message = "Internal server error", status = 500) =>
|
const jsonError = (res, message = "Internal server error", status = 500) =>
|
||||||
res.status(status).json({ success: false, message });
|
res.status(status).json({ success: false, message });
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// API ROUTES
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
createApiRoutes(app, jsonResponse, jsonError);
|
createApiRoutes(app, jsonResponse, jsonError);
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// 404 HANDLER
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Catch-all 404 handler for undefined routes ----
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
const filePath = path.join(publicDir, "404.html");
|
const filePath = path.join(publicDir, "404.html");
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
@@ -58,6 +120,12 @@ app.use((req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SERVER START
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.info(`AniKatoAPI listening at ${PORT}`);
|
console.info(`AniKatoAPI listening at ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: server.js
|
||||||
@@ -1,4 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — dataUrl.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Central configuration file defining all URL patterns for the AniKato
|
||||||
|
* anime streaming site. Provides both base domain and alternative
|
||||||
|
* mirror domains for resilience and fallback purposes.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* BASE_URL, ALT_DOMAINS, URLS
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// BASE DOMAIN CONFIGURATION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Primary base URL for the AniKato streaming service ----
|
||||||
|
/**
|
||||||
|
* The primary base URL used for all API requests to AniKato.
|
||||||
|
* This serves as the canonical domain for the site.
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @default "https://anikototv.to"
|
||||||
|
*/
|
||||||
const BASE_URL = "https://anikototv.to";
|
const BASE_URL = "https://anikototv.to";
|
||||||
|
|
||||||
|
// ---- FEATURE: Alternative mirror domains for fallback and redundancy ----
|
||||||
|
/**
|
||||||
|
* Array of alternative domain mirrors for the AniKato site.
|
||||||
|
* Used when the primary domain is unavailable or blocked.
|
||||||
|
* Each domain points to the same content but may have different
|
||||||
|
* availability depending on region or network conditions.
|
||||||
|
*
|
||||||
|
* @type {string[]}
|
||||||
|
* @example
|
||||||
|
* // Access alternative domains
|
||||||
|
* ALT_DOMAINS[0] // "https://anikoto.cz"
|
||||||
|
*/
|
||||||
const ALT_DOMAINS = [
|
const ALT_DOMAINS = [
|
||||||
"https://anikoto.cz",
|
"https://anikoto.cz",
|
||||||
"https://anikoto.me",
|
"https://anikoto.me",
|
||||||
@@ -7,6 +50,56 @@ const ALT_DOMAINS = [
|
|||||||
"https://anikoto.se"
|
"https://anikoto.se"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// URL PATTERN DEFINITIONS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Complete URL routing map for all site endpoints ----
|
||||||
|
/**
|
||||||
|
* Object containing all URL patterns for the AniKato site.
|
||||||
|
* Functions return dynamically constructed URLs based on parameters.
|
||||||
|
* Static URLs are provided as string properties.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} home - URL for the homepage
|
||||||
|
* @property {string} search - URL for search/filter functionality
|
||||||
|
* @property {Function} genre - Returns URL for genre-specific listings
|
||||||
|
* @param {string} genre - Genre slug (e.g., "action", "comedy")
|
||||||
|
* @returns {string} Complete URL for the genre page
|
||||||
|
* @property {Function} type - Returns URL for anime type listings
|
||||||
|
* @param {string} type - Anime type slug (e.g., "tv", "movie")
|
||||||
|
* @returns {string} Complete URL for the type page
|
||||||
|
* @property {Function} status - Returns URL for airing status listings
|
||||||
|
* @param {string} status - Status slug (e.g., "currently-airing")
|
||||||
|
* @returns {string} Complete URL for the status page
|
||||||
|
* @property {Function} azList - Returns URL for alphabetical listings
|
||||||
|
* @param {string} letter - Letter or "other" for non-alpha titles
|
||||||
|
* @returns {string} Complete URL for the A-Z list page
|
||||||
|
* @property {Function} watch - Returns URL for anime watch page
|
||||||
|
* @param {string} slug - Anime series slug identifier
|
||||||
|
* @returns {string} Complete URL for the watch page
|
||||||
|
* @property {Function} episode - Returns URL for specific episode
|
||||||
|
* @param {string} slug - Anime series slug identifier
|
||||||
|
* @param {number} ep - Episode number to watch
|
||||||
|
* @returns {string} Complete URL for the specific episode
|
||||||
|
* @property {string} latestUpdated - URL for recently updated anime
|
||||||
|
* @property {string} newRelease - URL for newly released anime
|
||||||
|
* @property {string} mostViewed - URL for most popular anime
|
||||||
|
* @property {string} random - URL for random anime selection
|
||||||
|
* @property {string} serverList - URL for episode server listings
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get homepage URL
|
||||||
|
* URLS.home // "https://anikototv.to/home"
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get genre URL
|
||||||
|
* URLS.genre("action") // "https://anikototv.to/genre/action"
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get episode URL
|
||||||
|
* URLS.episode("one-piece", 100) // "https://anikototv.to/watch/one-piece/ep-100"
|
||||||
|
*/
|
||||||
const URLS = {
|
const URLS = {
|
||||||
home: `${BASE_URL}/home`,
|
home: `${BASE_URL}/home`,
|
||||||
search: `${BASE_URL}/filter`,
|
search: `${BASE_URL}/filter`,
|
||||||
@@ -24,3 +117,4 @@ const URLS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { BASE_URL, ALT_DOMAINS, URLS };
|
export { BASE_URL, ALT_DOMAINS, URLS };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: dataUrl.js
|
||||||
@@ -1,3 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — header.config.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* HTTP request header configuration for making API calls to the
|
||||||
|
* AniKato streaming site. Includes browser-like headers to
|
||||||
|
* mimic legitimate web traffic and avoid detection.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* headers
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// HTTP REQUEST HEADER CONFIGURATION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Browser-mimicking headers for HTTP requests ----
|
||||||
|
/**
|
||||||
|
* Standard HTTP headers that mimic a real browser request.
|
||||||
|
* Used with axios/fetch to make requests appear as legitimate
|
||||||
|
* browser traffic to the AniKato streaming site.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} User-Agent - Browser identification string
|
||||||
|
* @property {string} Accept - Accepted response content types
|
||||||
|
* @property {string} Accept-Language - Preferred language settings
|
||||||
|
* @property {string} Accept-Encoding - Supported compression algorithms
|
||||||
|
* @property {string} Connection - Connection keep-alive preference
|
||||||
|
* @property {string} Upgrade-Insecure-Requests - HTTPS upgrade preference
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Use with axios
|
||||||
|
* axios.get(url, { headers })
|
||||||
|
*/
|
||||||
const headers = {
|
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",
|
"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": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
@@ -8,3 +48,4 @@ const headers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { headers };
|
export { headers };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: header.config.js
|
||||||
@@ -1,3 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — ids.config.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Mapping configuration for anime metadata IDs used by the AniKato
|
||||||
|
* site's filtering system. Contains human-readable slugs mapped
|
||||||
|
* to numeric IDs for genres, types, statuses, ratings, and sort options.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// GENRE ID MAPPING
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Genre slug to numeric ID mapping ----
|
||||||
|
/**
|
||||||
|
* Maps human-readable genre slugs to their corresponding numeric IDs
|
||||||
|
* used in AniKato's filtering system. Contains 43 different anime genres
|
||||||
|
* from action to vampire.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} action - ID for action genre
|
||||||
|
* @property {string} adventure - ID for adventure genre
|
||||||
|
* @property {string} comedy - ID for comedy genre
|
||||||
|
* @property {string} drama - ID for drama genre
|
||||||
|
* @property {string} fantasy - ID for fantasy genre
|
||||||
|
* @property {string} horror - ID for horror genre
|
||||||
|
* @property {string} romance - ID for romance genre
|
||||||
|
* @property {string} sci-fi - ID for science fiction genre
|
||||||
|
* @property {string} slice-of-life - ID for slice of life genre
|
||||||
|
* @property {string} supernatural - ID for supernatural genre
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get genre ID for filtering
|
||||||
|
* GENRE_IDS["action"] // "1"
|
||||||
|
* GENRE_IDS["comedy"] // "4"
|
||||||
|
*/
|
||||||
const GENRE_IDS = {
|
const GENRE_IDS = {
|
||||||
"action": "1",
|
"action": "1",
|
||||||
"adventure": "2",
|
"adventure": "2",
|
||||||
@@ -44,6 +89,29 @@ const GENRE_IDS = {
|
|||||||
"vampire": "43"
|
"vampire": "43"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANIME TYPE ID MAPPING
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime type slug to numeric ID mapping ----
|
||||||
|
/**
|
||||||
|
* Maps anime type slugs to their corresponding numeric IDs.
|
||||||
|
* Covers all standard anime format types including TV series,
|
||||||
|
* movies, OVAs, ONAs, and special episodes.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} movie - ID for movie format
|
||||||
|
* @property {string} music - ID for music video format
|
||||||
|
* @property {string} ona - ID for Original Net Animation
|
||||||
|
* @property {string} ova - ID for Original Video Animation
|
||||||
|
* @property {string} special - ID for special episodes
|
||||||
|
* @property {string} tv - ID for TV broadcast series
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get type ID for filtering
|
||||||
|
* TYPE_IDS["tv"] // "6"
|
||||||
|
* TYPE_IDS["movie"] // "1"
|
||||||
|
*/
|
||||||
const TYPE_IDS = {
|
const TYPE_IDS = {
|
||||||
"movie": "1",
|
"movie": "1",
|
||||||
"music": "2",
|
"music": "2",
|
||||||
@@ -53,12 +121,52 @@ const TYPE_IDS = {
|
|||||||
"tv": "6"
|
"tv": "6"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// AIRING STATUS ID MAPPING
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Airing status slug to numeric ID mapping ----
|
||||||
|
/**
|
||||||
|
* Maps anime airing status slugs to their corresponding numeric IDs.
|
||||||
|
* Used to filter anime by their broadcast completion status.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} currently-airing - ID for ongoing series
|
||||||
|
* @property {string} finished-airing - ID for completed series
|
||||||
|
* @property {string} not-yet-aired - ID for upcoming series
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get status ID for filtering
|
||||||
|
* STATUS_IDS["currently-airing"] // "1"
|
||||||
|
*/
|
||||||
const STATUS_IDS = {
|
const STATUS_IDS = {
|
||||||
"currently-airing": "1",
|
"currently-airing": "1",
|
||||||
"finished-airing": "2",
|
"finished-airing": "2",
|
||||||
"not-yet-aired": "3"
|
"not-yet-aired": "3"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// AGE RATING ID MAPPING
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Age rating slug to numeric ID mapping ----
|
||||||
|
/**
|
||||||
|
* Maps age rating slugs to their corresponding numeric IDs.
|
||||||
|
* Follows standard anime age rating classifications from
|
||||||
|
* general audience to restricted content.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} g - ID for General Audiences
|
||||||
|
* @property {string} pg - ID for Parental Guidance
|
||||||
|
* @property {string} pg-13 - ID for Parents Strongly Cautioned
|
||||||
|
* @property {string} r - ID for Restricted
|
||||||
|
* @property {string} r+ - ID for Restricted Plus
|
||||||
|
* @property {string} rx - ID for Hentai (Adult)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get rating ID for filtering
|
||||||
|
* RATING_IDS["pg-13"] // "3"
|
||||||
|
*/
|
||||||
const RATING_IDS = {
|
const RATING_IDS = {
|
||||||
"g": "1",
|
"g": "1",
|
||||||
"pg": "2",
|
"pg": "2",
|
||||||
@@ -68,6 +176,31 @@ const RATING_IDS = {
|
|||||||
"rx": "6"
|
"rx": "6"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SORT ORDER ID MAPPING
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Sort order slug to query parameter mapping ----
|
||||||
|
/**
|
||||||
|
* Maps sort order slugs to their corresponding query parameter values.
|
||||||
|
* Used with the filter/search endpoint to control result ordering.
|
||||||
|
* Note: Default sort has empty string value.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} default - Default sort order (empty string)
|
||||||
|
* @property {string} latest-updated - ID for most recently updated
|
||||||
|
* @property {string} latest-added - ID for most recently added
|
||||||
|
* @property {string} score - ID for highest rated
|
||||||
|
* @property {string} name-az - ID for alphabetical order
|
||||||
|
* @property {string} release-date - ID for newest release date
|
||||||
|
* @property {string} most-viewed - ID for most popular
|
||||||
|
* @property {string} number_of_episodes - ID for episode count
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get sort parameter value
|
||||||
|
* SORT_IDS["latest-updated"] // "1"
|
||||||
|
* SORT_IDS["score"] // "3"
|
||||||
|
*/
|
||||||
const SORT_IDS = {
|
const SORT_IDS = {
|
||||||
"default": "",
|
"default": "",
|
||||||
"latest-updated": "1",
|
"latest-updated": "1",
|
||||||
@@ -80,3 +213,4 @@ const SORT_IDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS };
|
export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: ids.config.js
|
||||||
@@ -1,19 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — animeInfo.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for anime detail information. Extracts full anime
|
||||||
|
* metadata including title, synopsis, episodes, genres, etc.
|
||||||
|
* Requires an anime slug as query parameter.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getAnimeInfo - Express route handler for GET /api/info
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractAnimeInfo } from "../extractors/animeInfo.extractor.js";
|
import { extractAnimeInfo } from "../extractors/animeInfo.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: ANIME INFO
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime detail information endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/info requests by extracting detailed anime
|
||||||
|
* information from the source site. Requires 'id' query param.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with anime details
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/info?id=one-piece
|
||||||
|
* Response: { success: true, results: { title, synopsis, ... } }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'id' query parameter is missing
|
||||||
|
*/
|
||||||
const getAnimeInfo = async (req, res, next) => {
|
const getAnimeInfo = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ success: false, message: "Anime slug is required" });
|
return res.status(400).json({ success: false, message: "Anime slug is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `anime_${id}`;
|
const cacheKey = `anime_${id}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractAnimeInfo(id);
|
const data = await extractAnimeInfo(id);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -23,3 +59,5 @@ const getAnimeInfo = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getAnimeInfo };
|
export { getAnimeInfo };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: animeInfo.controller.js
|
||||||
@@ -1,17 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — azList.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for alphabetical anime listing. Returns anime
|
||||||
|
* filtered by starting letter with pagination support.
|
||||||
|
* Default letter is 'all' if not specified.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getAzList - Express route handler for GET /api/az-list/:letter
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractAzList } from "../extractors/azList.extractor.js";
|
import { extractAzList } from "../extractors/azList.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: A-Z LIST
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Alphabetical anime listing endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/az-list/:letter requests. Returns paginated
|
||||||
|
* anime list filtered by the starting letter.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.params.letter - Starting letter (default: "all")
|
||||||
|
* @param {number} req.query.page - Page number (default: 1)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with alphabetical anime list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/az-list/A?page=2
|
||||||
|
* Response: { success: true, results: { anime, pagination, ... } }
|
||||||
|
*
|
||||||
|
* NOTE: Use 'all' as letter to get all anime without letter filtering
|
||||||
|
*/
|
||||||
const getAzList = async (req, res, next) => {
|
const getAzList = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { letter = "all" } = req.params;
|
const { letter = "all" } = req.params;
|
||||||
const { page = 1 } = req.query;
|
const { page = 1 } = req.query;
|
||||||
|
|
||||||
const cacheKey = `azlist_${letter}_${page}`;
|
const cacheKey = `azlist_${letter}_${page}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractAzList(letter, page);
|
const data = await extractAzList(letter, page);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -21,3 +59,5 @@ const getAzList = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getAzList };
|
export { getAzList };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: azList.controller.js
|
||||||
@@ -1,21 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — category.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for category-based anime filtering. Handles genre,
|
||||||
|
* type, and status categories with a unified endpoint. The
|
||||||
|
* category path is set by the route before reaching this controller.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getCategory - Express route handler for category endpoints
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractCategory } from "../extractors/category.extractor.js";
|
import { extractCategory } from "../extractors/category.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: CATEGORY
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Category-based anime filtering endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles category-based requests for genre, type, and status
|
||||||
|
* filtering. Returns paginated anime list for the given category.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.params.category - Category path (e.g., "genre/action")
|
||||||
|
* @param {number} req.query.page - Page number (default: 1)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with category-filtered anime
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/genre/action?page=2
|
||||||
|
* Response: { success: true, results: { anime, pagination, ... } }
|
||||||
|
*
|
||||||
|
* @throws {400} If category parameter is missing
|
||||||
|
* NOTE: The category param is set by the route middleware before this controller
|
||||||
|
*/
|
||||||
const getCategory = async (req, res, next) => {
|
const getCategory = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { category } = req.params;
|
const { category } = req.params;
|
||||||
const { page = 1 } = req.query;
|
const { page = 1 } = req.query;
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return res.status(400).json({ success: false, message: "Category is required" });
|
return res.status(400).json({ success: false, message: "Category is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `category_${category}_${page}`;
|
const cacheKey = `category_${category}_${page}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractCategory(category, page);
|
const data = await extractCategory(category, page);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -25,3 +63,5 @@ const getCategory = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getCategory };
|
export { getCategory };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: category.controller.js
|
||||||
@@ -1,20 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — episodeList.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for fetching the episode list of a specific anime.
|
||||||
|
* Returns all available episodes with their numbers and titles.
|
||||||
|
* Requires an anime ID as a route parameter.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getEpisodeList - Express route handler for GET /api/episodes/:id
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractEpisodeList } from "../extractors/episodeList.extractor.js";
|
import { extractEpisodeList } from "../extractors/episodeList.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: EPISODE LIST
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Episode list endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/episodes/:id requests. Extracts the full
|
||||||
|
* episode list for the specified anime from the source site.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} req.params.id - Anime ID/slug
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with episode list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/episodes/one-piece
|
||||||
|
* Response: { success: true, results: [{ number, title, ... }] }
|
||||||
|
*
|
||||||
|
* @throws {400} If anime ID is not provided
|
||||||
|
*/
|
||||||
const getEpisodeList = async (req, res, next) => {
|
const getEpisodeList = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ success: false, message: "Anime slug is required" });
|
return res.status(400).json({ success: false, message: "Anime slug is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `episodes_${id}`;
|
const cacheKey = `episodes_${id}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractEpisodeList(id);
|
const data = await extractEpisodeList(id);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -24,3 +60,5 @@ const getEpisodeList = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getEpisodeList };
|
export { getEpisodeList };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: episodeList.controller.js
|
||||||
@@ -1,21 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — episodeListAjax.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for AJAX-loaded episode list data. Provides additional
|
||||||
|
* episode metadata that may not be available in the standard episode
|
||||||
|
* list endpoint. Supports style and VRF query parameters.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getEpisodeListAjax - Express route handler for GET /api/episodes-ajax/:id
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractEpisodeListAjax } from "../extractors/episodeListAjax.extractor.js";
|
import { extractEpisodeListAjax } from "../extractors/episodeListAjax.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: EPISODE LIST AJAX
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: AJAX episode list endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/episodes-ajax/:id requests. Returns additional
|
||||||
|
* episode data loaded via AJAX from the source site.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} req.params.id - Anime ID
|
||||||
|
* @param {string} req.query.style - Optional display style parameter
|
||||||
|
* @param {string} req.query.vrf - Optional VRF verification token
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with AJAX episode data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/episodes-ajax/12345?style=0&vrf=abc123
|
||||||
|
* Response: { success: true, results: [...] }
|
||||||
|
*
|
||||||
|
* @throws {400} If anime ID is not provided
|
||||||
|
* NOTE: The style and vrf params affect the response format
|
||||||
|
*/
|
||||||
const getEpisodeListAjax = async (req, res, next) => {
|
const getEpisodeListAjax = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { style, vrf } = req.query;
|
const { style, vrf } = req.query;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ success: false, message: "Anime ID is required" });
|
return res.status(400).json({ success: false, message: "Anime ID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `episodes_ajax_${id}_${style || ""}_${vrf || ""}`;
|
const cacheKey = `episodes_ajax_${id}_${style || ""}_${vrf || ""}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractEpisodeListAjax(id, style, vrf);
|
const data = await extractEpisodeListAjax(id, style, vrf);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -25,3 +64,5 @@ const getEpisodeListAjax = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getEpisodeListAjax };
|
export { getEpisodeListAjax };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: episodeListAjax.controller.js
|
||||||
@@ -1,6 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — filter.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for advanced anime filtering. Accepts multiple query
|
||||||
|
* parameters to filter anime by genre, year, season, type, status,
|
||||||
|
* and more. All parameters are forwarded to the extractor.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getFilter - Express route handler for GET /api/filter
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractFilter } from "../extractors/filter.extractor.js";
|
import { extractFilter } from "../extractors/filter.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: FILTER
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Advanced filter endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/filter requests. Returns filtered anime list
|
||||||
|
* based on provided query parameters.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.query.genre - Genre filter (optional)
|
||||||
|
* @param {string} req.query.year - Year filter (optional)
|
||||||
|
* @param {string} req.query.season - Season filter (optional)
|
||||||
|
* @param {string} req.query.type - Type filter (optional)
|
||||||
|
* @param {string} req.query.status - Status filter (optional)
|
||||||
|
* @param {number} req.query.page - Page number (optional, default: 1)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with filtered anime list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/filter?genre=action&year=2024&type=tv
|
||||||
|
* Response: { success: true, results: { anime, pagination, ... } }
|
||||||
|
* NOTE: The cache key uses JSON.stringify of all params for unique caching
|
||||||
|
*/
|
||||||
const getFilter = async (req, res, next) => {
|
const getFilter = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const params = req.query;
|
const params = req.query;
|
||||||
@@ -9,7 +52,6 @@ const getFilter = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractFilter(params);
|
const data = await extractFilter(params);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -19,3 +61,5 @@ const getFilter = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getFilter };
|
export { getFilter };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: filter.controller.js
|
||||||
@@ -1,6 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — homeInfo.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for the homepage data endpoint. Extracts featured
|
||||||
|
* anime, spotlight, trending, and other homepage sections.
|
||||||
|
* Uses in-memory caching to avoid repeated scraping.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getHomeInfo - Express route handler for GET /api/
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractHomeInfo } from "../extractors/homeInfo.extractor.js";
|
import { extractHomeInfo } from "../extractors/homeInfo.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: HOME INFO
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Homepage data endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/ requests by extracting homepage data
|
||||||
|
* from the source site. Returns cached data if available.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/
|
||||||
|
* Response: { success: true, results: { spotlight, trending, ... } }
|
||||||
|
*/
|
||||||
const getHomeInfo = async (req, res, next) => {
|
const getHomeInfo = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const cacheKey = "home";
|
const cacheKey = "home";
|
||||||
@@ -8,7 +43,6 @@ const getHomeInfo = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractHomeInfo();
|
const data = await extractHomeInfo();
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -18,3 +52,5 @@ const getHomeInfo = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getHomeInfo };
|
export { getHomeInfo };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: homeInfo.controller.js
|
||||||
@@ -1,6 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — newRelease.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for new release and newly added anime. Provides
|
||||||
|
* two endpoints: one for recently released episodes and one
|
||||||
|
* for newly added anime series. Both support pagination.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getNewRelease - Express route handler for GET /api/new-release
|
||||||
|
* getNewlyAdded - Express route handler for GET /api/newly-added
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractNewRelease, extractNewlyAdded } from "../extractors/newRelease.extractor.js";
|
import { extractNewRelease, extractNewlyAdded } from "../extractors/newRelease.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: NEW RELEASE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Recently released episodes endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/new-release requests. Returns paginated list
|
||||||
|
* of recently released anime episodes.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {number} req.query.page - Page number (default: 1)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with new release episodes
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/new-release?page=2
|
||||||
|
* Response: { success: true, results: { episodes, pagination, ... } }
|
||||||
|
*/
|
||||||
const getNewRelease = async (req, res, next) => {
|
const getNewRelease = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1 } = req.query;
|
const { page = 1 } = req.query;
|
||||||
@@ -9,7 +47,6 @@ const getNewRelease = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractNewRelease(page);
|
const data = await extractNewRelease(page);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -18,6 +55,25 @@ const getNewRelease = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: NEWLY ADDED
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Newly added anime series endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/newly-added requests. Returns paginated list
|
||||||
|
* of newly added anime series to the platform.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {number} req.query.page - Page number (default: 1)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with newly added anime
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/newly-added?page=3
|
||||||
|
* Response: { success: true, results: { anime, pagination, ... } }
|
||||||
|
*/
|
||||||
const getNewlyAdded = async (req, res, next) => {
|
const getNewlyAdded = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1 } = req.query;
|
const { page = 1 } = req.query;
|
||||||
@@ -26,7 +82,6 @@ const getNewlyAdded = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractNewlyAdded(page);
|
const data = await extractNewlyAdded(page);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -36,3 +91,5 @@ const getNewlyAdded = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getNewRelease, getNewlyAdded };
|
export { getNewRelease, getNewlyAdded };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: newRelease.controller.js
|
||||||
@@ -1,6 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — popular.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for most popular anime list. Returns paginated list
|
||||||
|
* of the most popular anime from the source site. Supports page
|
||||||
|
* parameter for pagination.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getPopular - Express route handler for GET /api/most-popular
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractPopular } from "../extractors/popular.extractor.js";
|
import { extractPopular } from "../extractors/popular.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: POPULAR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Most popular anime endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/most-popular requests. Returns paginated list
|
||||||
|
* of the most popular anime from the source site.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {number} req.query.page - Page number (default: 1)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with popular anime list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/most-popular?page=2
|
||||||
|
* Response: { success: true, results: { anime, pagination, ... } }
|
||||||
|
*/
|
||||||
const getPopular = async (req, res, next) => {
|
const getPopular = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1 } = req.query;
|
const { page = 1 } = req.query;
|
||||||
@@ -9,7 +46,6 @@ const getPopular = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractPopular(page);
|
const data = await extractPopular(page);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -19,3 +55,5 @@ const getPopular = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getPopular };
|
export { getPopular };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: popular.controller.js
|
||||||
@@ -1,6 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — random.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for random anime recommendation. Returns a single
|
||||||
|
* random anime from the source site. No parameters required.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getRandom - Express route handler for GET /api/random
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractRandom } from "../extractors/random.extractor.js";
|
import { extractRandom } from "../extractors/random.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: RANDOM
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Random anime endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/random requests. Returns a random anime
|
||||||
|
* recommendation from the source site.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with random anime
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/random
|
||||||
|
* Response: { success: true, results: { title, id, ... } }
|
||||||
|
*/
|
||||||
const getRandom = async (req, res, next) => {
|
const getRandom = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const cacheKey = "random";
|
const cacheKey = "random";
|
||||||
@@ -8,7 +43,6 @@ const getRandom = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractRandom();
|
const data = await extractRandom();
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -18,3 +52,5 @@ const getRandom = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getRandom };
|
export { getRandom };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: random.controller.js
|
||||||
@@ -1,20 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — schedule.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for anime airing schedule. Returns anime episodes
|
||||||
|
* scheduled to air on a specific date. Requires date in
|
||||||
|
* YYYY-MM-DD format as a query parameter.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getSchedule - Express route handler for GET /api/schedule
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractSchedule } from "../extractors/schedule.extractor.js";
|
import { extractSchedule } from "../extractors/schedule.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: SCHEDULE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime airing schedule endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/schedule requests. Returns anime episodes
|
||||||
|
* scheduled for the specified date.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.query.date - Date in YYYY-MM-DD format
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with schedule data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/schedule?date=2024-01-15
|
||||||
|
* Response: { success: true, results: [{ time, title, ... }] }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'date' query parameter is missing
|
||||||
|
* NOTE: Date must be in YYYY-MM-DD format
|
||||||
|
*/
|
||||||
const getSchedule = async (req, res, next) => {
|
const getSchedule = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { date } = req.query;
|
const { date } = req.query;
|
||||||
|
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return res.status(400).json({ success: false, message: "Date is required (YYYY-MM-DD)" });
|
return res.status(400).json({ success: false, message: "Date is required (YYYY-MM-DD)" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `schedule_${date}`;
|
const cacheKey = `schedule_${date}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractSchedule(date);
|
const data = await extractSchedule(date);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -24,3 +61,5 @@ const getSchedule = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getSchedule };
|
export { getSchedule };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: schedule.controller.js
|
||||||
@@ -1,19 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — search.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for search functionality. Provides both full search
|
||||||
|
* results with pagination and lightweight autocomplete suggestions.
|
||||||
|
* Both endpoints require a 'keyword' query parameter.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getSearchResults - Express route handler for GET /api/search
|
||||||
|
* getSearchSuggestions - Express route handler for GET /api/search/suggest
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractSearchResults, extractSearchSuggestions } from "../extractors/search.extractor.js";
|
import { extractSearchResults, extractSearchSuggestions } from "../extractors/search.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: SEARCH RESULTS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Full search results endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/search requests with keyword and optional page.
|
||||||
|
* Returns paginated search results from the source site.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with search results
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/search?keyword=naruto&page=2
|
||||||
|
* Response: { success: true, results: { anime, ... } }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'keyword' query parameter is missing
|
||||||
|
*/
|
||||||
const getSearchResults = async (req, res, next) => {
|
const getSearchResults = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { keyword, page = 1 } = req.query;
|
const { keyword, page = 1 } = req.query;
|
||||||
if (!keyword) {
|
if (!keyword) {
|
||||||
return res.status(400).json({ success: false, message: "Keyword is required" });
|
return res.status(400).json({ success: false, message: "Keyword is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `search_${keyword}_${page}`;
|
const cacheKey = `search_${keyword}_${page}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractSearchResults(keyword, page);
|
const data = await extractSearchResults(keyword, page);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -22,19 +59,37 @@ const getSearchResults = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: SEARCH SUGGESTIONS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Search autocomplete suggestions endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/search/suggest requests for autocomplete.
|
||||||
|
* Returns lightweight suggestion list for the given keyword.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with suggestion list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/search/suggest?keyword=nar
|
||||||
|
* Response: { success: true, results: [{ id, title, ... }] }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'keyword' query parameter is missing
|
||||||
|
*/
|
||||||
const getSearchSuggestions = async (req, res, next) => {
|
const getSearchSuggestions = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { keyword } = req.query;
|
const { keyword } = req.query;
|
||||||
if (!keyword) {
|
if (!keyword) {
|
||||||
return res.status(400).json({ success: false, message: "Keyword is required" });
|
return res.status(400).json({ success: false, message: "Keyword is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `suggest_${keyword}`;
|
const cacheKey = `suggest_${keyword}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractSearchSuggestions(keyword);
|
const data = await extractSearchSuggestions(keyword);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -44,3 +99,5 @@ const getSearchSuggestions = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getSearchResults, getSearchSuggestions };
|
export { getSearchResults, getSearchSuggestions };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: search.controller.js
|
||||||
@@ -1,20 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — seasons.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for anime season information. Returns season data
|
||||||
|
* for a specific anime including related seasons and episodes.
|
||||||
|
* Requires an anime ID as a route parameter.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getSeasons - Express route handler for GET /api/seasons/:id
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractSeasons } from "../extractors/seasons.extractor.js";
|
import { extractSeasons } from "../extractors/seasons.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: SEASONS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime seasons endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/seasons/:id requests. Returns season information
|
||||||
|
* for the specified anime.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.params.id - Anime ID
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with season data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/seasons/12345
|
||||||
|
* Response: { success: true, results: { seasons, ... } }
|
||||||
|
*
|
||||||
|
* @throws {400} If anime ID is not provided
|
||||||
|
*/
|
||||||
const getSeasons = async (req, res, next) => {
|
const getSeasons = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ success: false, message: "Anime ID is required" });
|
return res.status(400).json({ success: false, message: "Anime ID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `seasons_${id}`;
|
const cacheKey = `seasons_${id}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractSeasons(id);
|
const data = await extractSeasons(id);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -24,3 +60,5 @@ const getSeasons = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getSeasons };
|
export { getSeasons };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: seasons.controller.js
|
||||||
@@ -1,6 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — spotlight.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for spotlight/featured anime carousel data.
|
||||||
|
* Returns a list of featured anime with banners and descriptions.
|
||||||
|
* No parameters required - returns fixed spotlight content.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getSpotlight - Express route handler for GET /api/spotlight
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractSpotlight } from "../extractors/spotlight.extractor.js";
|
import { extractSpotlight } from "../extractors/spotlight.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: SPOTLIGHT
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Spotlight carousel endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/spotlight requests. Returns featured anime
|
||||||
|
* data for the homepage spotlight carousel.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with spotlight anime list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/spotlight
|
||||||
|
* Response: { success: true, results: [{ title, banner, ... }] }
|
||||||
|
*/
|
||||||
const getSpotlight = async (req, res, next) => {
|
const getSpotlight = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const cacheKey = "spotlight";
|
const cacheKey = "spotlight";
|
||||||
@@ -8,7 +44,6 @@ const getSpotlight = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractSpotlight();
|
const data = await extractSpotlight();
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -18,3 +53,5 @@ const getSpotlight = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getSpotlight };
|
export { getSpotlight };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: spotlight.controller.js
|
||||||
@@ -1,21 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — status.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for anime filtering by airing status. Returns anime
|
||||||
|
* filtered by their current airing status (currently-airing,
|
||||||
|
* finished-airing, not-yet-aired) with pagination support.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getStatus - Express route handler for GET /api/status/:status
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractStatus } from "../extractors/status.extractor.js";
|
import { extractStatus } from "../extractors/status.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: STATUS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime by airing status endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/status/:status requests. Returns paginated
|
||||||
|
* anime list filtered by their airing status.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.params.status - Airing status filter
|
||||||
|
* @param {number} req.query.page - Page number (default: 1)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with filtered anime list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/status/currently-airing?page=2
|
||||||
|
* Response: { success: true, results: { anime, pagination, ... } }
|
||||||
|
*
|
||||||
|
* @throws {400} If status parameter is invalid
|
||||||
|
* NOTE: Valid statuses: "currently-airing", "finished-airing", "not-yet-aired"
|
||||||
|
*/
|
||||||
const getStatus = async (req, res, next) => {
|
const getStatus = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { status } = req.params;
|
const { status } = req.params;
|
||||||
const { page = 1 } = req.query;
|
const { page = 1 } = req.query;
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return res.status(400).json({ success: false, message: "Status is required (currently-airing, finished-airing, not-yet-aired)" });
|
return res.status(400).json({ success: false, message: "Status is required (currently-airing, finished-airing, not-yet-aired)" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `status_${status}_${page}`;
|
const cacheKey = `status_${status}_${page}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractStatus(status, page);
|
const data = await extractStatus(status, page);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -25,3 +63,5 @@ const getStatus = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getStatus };
|
export { getStatus };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: status.controller.js
|
||||||
@@ -1,20 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — streamInfo.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for streaming information. Provides stream URLs,
|
||||||
|
* available servers, and mapped server data for episode playback.
|
||||||
|
* Three endpoints for different streaming data needs.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getStreamInfo - Express route handler for GET /api/stream
|
||||||
|
* getServerList - Express route handler for GET /api/servers
|
||||||
|
* getMapperServers - Express route handler for GET /api/mapper-servers
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractStreamInfo, extractServerList, extractMapperServers } from "../extractors/streamInfo.extractor.js";
|
import { extractStreamInfo, extractServerList, extractMapperServers } from "../extractors/streamInfo.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: STREAM INFO
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Stream URL and metadata endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/stream requests. Returns stream URL and
|
||||||
|
* metadata for a specific episode link ID.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.query.id - Episode link ID
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with stream information
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/stream?id=abc123
|
||||||
|
* Response: { success: true, results: { url, quality, ... } }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'id' query parameter is missing
|
||||||
|
*/
|
||||||
const getStreamInfo = async (req, res, next) => {
|
const getStreamInfo = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ success: false, message: "Link ID is required" });
|
return res.status(400).json({ success: false, message: "Link ID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `stream_${id}`;
|
const cacheKey = `stream_${id}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractStreamInfo(id);
|
const data = await extractStreamInfo(id);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -23,20 +61,38 @@ const getStreamInfo = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: SERVER LIST
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Available streaming servers endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/servers requests. Returns list of available
|
||||||
|
* streaming servers for the given episode IDs.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.query.ids - Comma-separated episode IDs
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with server list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/servers?ids=123,456,789
|
||||||
|
* Response: { success: true, results: [{ server, name, ... }] }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'ids' query parameter is missing
|
||||||
|
*/
|
||||||
const getServerList = async (req, res, next) => {
|
const getServerList = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { ids } = req.query;
|
const { ids } = req.query;
|
||||||
|
|
||||||
if (!ids) {
|
if (!ids) {
|
||||||
return res.status(400).json({ success: false, message: "Episode IDs are required" });
|
return res.status(400).json({ success: false, message: "Episode IDs are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `servers_${ids}`;
|
const cacheKey = `servers_${ids}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractServerList(ids);
|
const data = await extractServerList(ids);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -45,20 +101,41 @@ const getServerList = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: MAPPER SERVERS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Cross-episode mapped servers endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/mapper-servers requests. Returns servers mapped
|
||||||
|
* across multiple episodes for continuous playback.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.query.malId - MyAnimeList ID
|
||||||
|
* @param {string} req.query.slug - Anime slug
|
||||||
|
* @param {string} req.query.timestamp - Timestamp for mapping
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with mapped server data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/mapper-servers?malId=21&slug=one-piece×tamp=1234567890
|
||||||
|
* Response: { success: true, results: [...] }
|
||||||
|
*
|
||||||
|
* @throws {400} If any required parameter is missing
|
||||||
|
* NOTE: All three parameters (malId, slug, timestamp) are required
|
||||||
|
*/
|
||||||
const getMapperServers = async (req, res, next) => {
|
const getMapperServers = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { malId, slug, timestamp } = req.query;
|
const { malId, slug, timestamp } = req.query;
|
||||||
|
|
||||||
if (!malId || !slug || !timestamp) {
|
if (!malId || !slug || !timestamp) {
|
||||||
return res.status(400).json({ success: false, message: "malId, slug, and timestamp are required" });
|
return res.status(400).json({ success: false, message: "malId, slug, and timestamp are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `mapper_${malId}_${slug}_${timestamp}`;
|
const cacheKey = `mapper_${malId}_${slug}_${timestamp}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractMapperServers(malId, slug, timestamp);
|
const data = await extractMapperServers(malId, slug, timestamp);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -68,3 +145,5 @@ const getMapperServers = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getStreamInfo, getServerList, getMapperServers };
|
export { getStreamInfo, getServerList, getMapperServers };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: streamInfo.controller.js
|
||||||
@@ -1,20 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — suggestion.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for anime suggestions/autocomplete. Returns a list
|
||||||
|
* of anime matching the given keyword for autocomplete features.
|
||||||
|
* Requires a 'keyword' query parameter.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getSuggestions - Express route handler for GET /api/suggestions
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractSuggestions } from "../extractors/suggestion.extractor.js";
|
import { extractSuggestions } from "../extractors/suggestion.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: SUGGESTIONS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime suggestions endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/suggestions requests. Returns autocomplete
|
||||||
|
* suggestions based on the provided keyword.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.query.keyword - Search keyword for suggestions
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with suggestion list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/suggestions?keyword=naru
|
||||||
|
* Response: { success: true, results: [{ id, title, ... }] }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'keyword' query parameter is missing
|
||||||
|
*/
|
||||||
const getSuggestions = async (req, res, next) => {
|
const getSuggestions = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { keyword } = req.query;
|
const { keyword } = req.query;
|
||||||
|
|
||||||
if (!keyword) {
|
if (!keyword) {
|
||||||
return res.status(400).json({ success: false, message: "Keyword is required" });
|
return res.status(400).json({ success: false, message: "Keyword is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `suggest_${keyword}`;
|
const cacheKey = `suggest_${keyword}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractSuggestions(keyword);
|
const data = await extractSuggestions(keyword);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -24,3 +60,5 @@ const getSuggestions = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getSuggestions };
|
export { getSuggestions };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: suggestion.controller.js
|
||||||
@@ -1,6 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — topten.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for top 10 ranked anime. Returns the current top 10
|
||||||
|
* anime list from the source site. No parameters required.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getTopTen - Express route handler for GET /api/top-ten
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractTopTen } from "../extractors/topten.extractor.js";
|
import { extractTopTen } from "../extractors/topten.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: TOP TEN
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Top 10 anime endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/top-ten requests. Returns the current top 10
|
||||||
|
* ranked anime from the source site.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with top 10 anime list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/top-ten
|
||||||
|
* Response: { success: true, results: [{ rank, title, ... }] }
|
||||||
|
*/
|
||||||
const getTopTen = async (req, res, next) => {
|
const getTopTen = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const cacheKey = "topTen";
|
const cacheKey = "topTen";
|
||||||
@@ -8,7 +43,6 @@ const getTopTen = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractTopTen();
|
const data = await extractTopTen();
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -18,3 +52,5 @@ const getTopTen = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getTopTen };
|
export { getTopTen };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: topten.controller.js
|
||||||
@@ -1,6 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — trending.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for currently trending anime data. Returns a list
|
||||||
|
* of anime that are currently popular/trending on the source site.
|
||||||
|
* No parameters required - returns fixed trending content.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getTrending - Express route handler for GET /api/trending
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractTrending } from "../extractors/trending.extractor.js";
|
import { extractTrending } from "../extractors/trending.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: TRENDING
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Trending anime endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/trending requests. Returns currently trending
|
||||||
|
* anime from the source site.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with trending anime list
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/trending
|
||||||
|
* Response: { success: true, results: [{ title, rank, ... }] }
|
||||||
|
*/
|
||||||
const getTrending = async (req, res, next) => {
|
const getTrending = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const cacheKey = "trending";
|
const cacheKey = "trending";
|
||||||
@@ -8,7 +44,6 @@ const getTrending = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractTrending();
|
const data = await extractTrending();
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -18,3 +53,5 @@ const getTrending = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getTrending };
|
export { getTrending };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: trending.controller.js
|
||||||
@@ -1,6 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — trendingSidebar.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for trending sidebar widget data. Returns trending
|
||||||
|
* anime data formatted for sidebar display. No parameters required.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getTrendingSidebar - Express route handler for GET /api/trending-sidebar
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractTrendingSidebar } from "../extractors/trendingSidebar.extractor.js";
|
import { extractTrendingSidebar } from "../extractors/trendingSidebar.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: TRENDING SIDEBAR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Trending sidebar widget endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/trending-sidebar requests. Returns trending
|
||||||
|
* anime data formatted for sidebar display.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with trending sidebar data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/trending-sidebar
|
||||||
|
* Response: { success: true, results: [{ title, rank, ... }] }
|
||||||
|
*/
|
||||||
const getTrendingSidebar = async (req, res, next) => {
|
const getTrendingSidebar = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const cacheKey = "trendingSidebar";
|
const cacheKey = "trendingSidebar";
|
||||||
@@ -8,7 +43,6 @@ const getTrendingSidebar = async (req, res, next) => {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractTrendingSidebar();
|
const data = await extractTrendingSidebar();
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -18,3 +52,5 @@ const getTrendingSidebar = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getTrendingSidebar };
|
export { getTrendingSidebar };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: trendingSidebar.controller.js
|
||||||
@@ -1,20 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — watchOrder.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for recommended watch order. Returns the optimal
|
||||||
|
* viewing order for anime with multiple seasons, movies, and OVAs.
|
||||||
|
* Requires an anime ID as a route parameter.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getWatchOrder - Express route handler for GET /api/watch-order/:id
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractWatchOrder } from "../extractors/watchOrder.extractor.js";
|
import { extractWatchOrder } from "../extractors/watchOrder.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: WATCH ORDER
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Recommended watch order endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/watch-order/:id requests. Returns recommended
|
||||||
|
* viewing order for the specified anime franchise.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.params.id - Anime ID
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with watch order data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/watch-order/12345
|
||||||
|
* Response: { success: true, results: [{ order, title, ... }] }
|
||||||
|
*
|
||||||
|
* @throws {400} If anime ID is not provided
|
||||||
|
* TIP: Useful for anime franchises with complex viewing orders
|
||||||
|
*/
|
||||||
const getWatchOrder = async (req, res, next) => {
|
const getWatchOrder = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ success: false, message: "Anime ID is required" });
|
return res.status(400).json({ success: false, message: "Anime ID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `watchorder_${id}`;
|
const cacheKey = `watchorder_${id}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractWatchOrder(id);
|
const data = await extractWatchOrder(id);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -24,3 +61,5 @@ const getWatchOrder = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getWatchOrder };
|
export { getWatchOrder };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: watchOrder.controller.js
|
||||||
@@ -1,20 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — watchPage.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for the watch page endpoint. Returns episode
|
||||||
|
* details, video player data, and related anime information.
|
||||||
|
* Requires both 'slug' and 'ep' query parameters.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getWatchPage - Express route handler for GET /api/watch
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { extractWatchPage } from "../extractors/watchPage.extractor.js";
|
import { extractWatchPage } from "../extractors/watchPage.extractor.js";
|
||||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: WATCH PAGE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Watch page data endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/watch requests. Returns episode data and
|
||||||
|
* player information for the specified anime and episode.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} req.query.slug - Anime slug identifier
|
||||||
|
* @param {string|number} req.query.ep - Episode number
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with watch page data
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/watch?slug=one-piece&ep=1
|
||||||
|
* Response: { success: true, results: { episode, servers, ... } }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'slug' or 'ep' query parameters are missing
|
||||||
|
* WARNING: Both slug and ep are required or the endpoint returns 500
|
||||||
|
*/
|
||||||
const getWatchPage = async (req, res, next) => {
|
const getWatchPage = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { slug, ep } = req.query;
|
const { slug, ep } = req.query;
|
||||||
|
|
||||||
if (!slug || !ep) {
|
if (!slug || !ep) {
|
||||||
return res.status(400).json({ success: false, message: "Slug and episode number are required" });
|
return res.status(400).json({ success: false, message: "Slug and episode number are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `watch_${slug}_${ep}`;
|
const cacheKey = `watch_${slug}_${ep}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await extractWatchPage(slug, ep);
|
const data = await extractWatchPage(slug, ep);
|
||||||
setCache(cacheKey, data);
|
setCache(cacheKey, data);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
@@ -24,3 +62,5 @@ const getWatchPage = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { getWatchPage };
|
export { getWatchPage };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: watchPage.controller.js
|
||||||
@@ -1,47 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — animeInfo.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts detailed anime information from anikototv.to including title,
|
||||||
|
* synopsis, metadata (type, status, studios, producers, genres), ratings,
|
||||||
|
* and background artwork for a given anime slug.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractAnimeInfo
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANIME INFO EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract full anime detail page data for a given slug ----
|
||||||
|
/**
|
||||||
|
* Fetches the anime detail page from anikototv.to and parses all metadata
|
||||||
|
* including titles, synopsis, ratings, studios, producers, genres, and
|
||||||
|
* background artwork.
|
||||||
|
*
|
||||||
|
* @param {string} slug - The anime slug identifier (e.g. "naruto-shippuden")
|
||||||
|
* @returns {Promise<Object>} Full anime info object with all extracted fields
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const info = await extractAnimeInfo("one-piece");
|
||||||
|
* console.log(info.title); // "One Piece"
|
||||||
|
* console.log(info.studios); // ["Toei Animation"]
|
||||||
|
*/
|
||||||
const extractAnimeInfo = async (slug) => {
|
const extractAnimeInfo = async (slug) => {
|
||||||
try {
|
try {
|
||||||
const url = URLS.watch(slug);
|
const url = URLS.watch(slug);
|
||||||
const { data } = await axios.get(url, { headers });
|
const { data } = await axios.get(url, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract title and identification fields ----
|
||||||
|
// NOTE: itemprop='name' is used for schema.org microdata on the site
|
||||||
const title = $("h1[itemprop='name'].title.d-title").text().trim() || "";
|
const title = $("h1[itemprop='name'].title.d-title").text().trim() || "";
|
||||||
|
// NOTE: data-jp attribute holds the Japanese title variant
|
||||||
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
||||||
const altNames = $(".names.font-italic").text().trim() || "";
|
const altNames = $(".names.font-italic").text().trim() || "";
|
||||||
const poster = $("img[itemprop='image']").attr("src") || "";
|
const poster = $("img[itemprop='image']").attr("src") || "";
|
||||||
const synopsis = $(".synopsis .content").text().trim() || "";
|
const synopsis = $(".synopsis .content").text().trim() || "";
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract rating and review metadata ----
|
||||||
|
// NOTE: Multiple rating selectors are checked — .score .value is the display score,
|
||||||
|
// itemprop='ratingValue' is the microdata fallback
|
||||||
const rating = $("#w-rating .score .value").text().trim() || "";
|
const rating = $("#w-rating .score .value").text().trim() || "";
|
||||||
const ratingValue = $("#w-rating span[itemprop='ratingValue']").text().trim() || "";
|
const ratingValue = $("#w-rating span[itemprop='ratingValue']").text().trim() || "";
|
||||||
const reviewCount = $("#w-rating span[itemprop='reviewCount']").text().trim() || "";
|
const reviewCount = $("#w-rating span[itemprop='reviewCount']").text().trim() || "";
|
||||||
|
// NOTE: data-id on #watch-main is the internal anime ID used for AJAX calls
|
||||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime metadata (type, premiere, aired, status) ----
|
||||||
|
// NOTE: .bmeta contains the structured metadata block — first child holds basic info
|
||||||
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
||||||
const premiered = $(".bmeta .meta:first-child > div:nth-child(2) span").text().trim() || "";
|
const premiered = $(".bmeta .meta:first-child > div:nth-child(2) span").text().trim() || "";
|
||||||
const aired = $(".bmeta .meta:first-child > div:nth-child(3) span").text().trim() || "";
|
const aired = $(".bmeta .meta:first-child > div:nth-child(3) span").text().trim() || "";
|
||||||
const status = $(".bmeta .meta:first-child > div:nth-child(4) span a").text().trim() || "";
|
const status = $(".bmeta .meta:first-child > div:nth-child(4) span a").text().trim() || "";
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract secondary metadata (MAL score, duration, episodes) ----
|
||||||
|
// NOTE: Second .meta child holds MAL-linked and production metadata
|
||||||
const malScore = $(".bmeta .meta:nth-child(2) > div:nth-child(1) span").text().trim() || "";
|
const malScore = $(".bmeta .meta:nth-child(2) > div:nth-child(1) span").text().trim() || "";
|
||||||
const duration = $(".bmeta .meta:nth-child(2) > div:nth-child(2) span").text().trim() || "";
|
const duration = $(".bmeta .meta:nth-child(2) > div:nth-child(2) span").text().trim() || "";
|
||||||
const episodes = $(".bmeta .meta:nth-child(2) > div:nth-child(3) span").text().trim() || "";
|
const episodes = $(".bmeta .meta:nth-child(2) > div:nth-child(3) span").text().trim() || "";
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract studios list ----
|
||||||
|
// NOTE: itemprop='director' links on studio spans — itemprop='name' inside holds the name
|
||||||
const studios = [];
|
const studios = [];
|
||||||
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
||||||
studios.push($(el).text().trim());
|
studios.push($(el).text().trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract producers list ----
|
||||||
|
// NOTE: Same selector pattern as studios but from the 5th child (producers row)
|
||||||
const producers = [];
|
const producers = [];
|
||||||
$(".bmeta .meta:nth-child(2) > div:nth-child(5) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
$(".bmeta .meta:nth-child(2) > div:nth-child(5) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
||||||
producers.push($(el).text().trim());
|
producers.push($(el).text().trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract genres ----
|
||||||
|
// NOTE: Genre links contain '/genre/' in their href — filters out non-genre links
|
||||||
const genres = [];
|
const genres = [];
|
||||||
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
||||||
genres.push($(el).text().trim());
|
genres.push($(el).text().trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract background artwork from player element ----
|
||||||
|
// NOTE: The #player div has a CSS background-image set via inline style
|
||||||
const backgroundImage = $("#player").css("background-image")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
const backgroundImage = $("#player").css("background-image")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -72,3 +129,5 @@ const extractAnimeInfo = async (slug) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractAnimeInfo };
|
export { extractAnimeInfo };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: animeInfo.extractor.js
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — azList.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts anime from A-Z listing pages with pagination.
|
||||||
|
* Supports filtering by letter (a-z) or "all" for complete list.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractAzList
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
@@ -5,13 +22,46 @@ import { URLS } from "../configs/dataUrl.js";
|
|||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// A-Z LIST EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime from A-Z listing with pagination ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses anime from the A-Z listing page.
|
||||||
|
* Supports filtering by letter (a-z) or "all" for complete list.
|
||||||
|
*
|
||||||
|
* @param {string} letter - Letter to filter by (a-z) or "all" (default: "all")
|
||||||
|
* @param {number} page - Page number to fetch (default: 1)
|
||||||
|
* @returns {Promise<Object>} Object containing totalPages, letter, and data array
|
||||||
|
* @returns {number} return.totalPages - Total number of pages available
|
||||||
|
* @returns {string} return.letter - The letter filter used
|
||||||
|
* @returns {Array<Object>} return.data - Array of anime objects
|
||||||
|
* @returns {string} return.data[].slug - URL slug for the anime
|
||||||
|
* @returns {string} return.data[].poster - Poster image URL
|
||||||
|
* @returns {string} return.data[].title - English title
|
||||||
|
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||||
|
* @returns {number} return.data[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.data[].dub - Dubbed episode count
|
||||||
|
* @returns {number} return.data[].total - Total episode count
|
||||||
|
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||||
|
* @returns {string} return.data[].rating - Anime rating
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const azList = await extractAzList("a", 1);
|
||||||
|
* console.log(azList.letter); // "a"
|
||||||
|
* console.log(azList.data[0].title); // First anime starting with "a"
|
||||||
|
*/
|
||||||
const extractAzList = async (letter = "all", page = 1) => {
|
const extractAzList = async (letter = "all", page = 1) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: URLS.azList generates the correct endpoint for the given letter
|
||||||
const url = URLS.azList(letter);
|
const url = URLS.azList(letter);
|
||||||
const $ = await extractPages(url, page);
|
const $ = await extractPages(url, page);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
// NOTE: Multiple selectors to handle different page layouts
|
||||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find("img").attr("src") || "";
|
const poster = $(el).find("img").attr("src") || "";
|
||||||
@@ -23,6 +73,7 @@ const extractAzList = async (letter = "all", page = 1) => {
|
|||||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||||
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
results.push({
|
results.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -45,3 +96,5 @@ const extractAzList = async (letter = "all", page = 1) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractAzList };
|
export { extractAzList };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: azList.extractor.js
|
||||||
|
|||||||
@@ -1,11 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — category.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts anime filtered by category (genre, type, status, etc.).
|
||||||
|
* Supports multiple category types with automatic URL detection.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractCategory
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CATEGORY FILTER EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime filtered by category with pagination ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses anime filtered by category.
|
||||||
|
* Automatically detects category type (genre, type, status) from the input.
|
||||||
|
*
|
||||||
|
* @param {string} category - Category to filter by (e.g., "genre/action", "type/tv", "status/ongoing")
|
||||||
|
* @param {number} page - Page number to fetch (default: 1)
|
||||||
|
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||||
|
* @returns {number} return.totalPages - Total number of pages available
|
||||||
|
* @returns {Array<Object>} return.data - Array of anime objects
|
||||||
|
* @returns {string} return.data[].slug - URL slug for the anime
|
||||||
|
* @returns {string} return.data[].animeId - Internal anime ID
|
||||||
|
* @returns {string} return.data[].poster - Poster image URL
|
||||||
|
* @returns {string} return.data[].title - English title
|
||||||
|
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||||
|
* @returns {number} return.data[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.data[].dub - Dubbed episode count
|
||||||
|
* @returns {number} return.data[].total - Total episode count
|
||||||
|
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||||
|
* @returns {string} return.data[].rating - Anime rating
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const actionAnime = await extractCategory("genre/action", 1);
|
||||||
|
* console.log(actionAnime.data[0].title); // First action anime
|
||||||
|
*
|
||||||
|
* const tvAnime = await extractCategory("type/tv", 1);
|
||||||
|
* console.log(tvAnime.data[0].title); // First TV anime
|
||||||
|
*/
|
||||||
const extractCategory = async (category, page = 1) => {
|
const extractCategory = async (category, page = 1) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: Category type is determined by prefix in the category string
|
||||||
let url;
|
let url;
|
||||||
if (category.startsWith("genre/")) {
|
if (category.startsWith("genre/")) {
|
||||||
url = URLS.genre(category.replace("genre/", ""));
|
url = URLS.genre(category.replace("genre/", ""));
|
||||||
@@ -14,6 +64,7 @@ const extractCategory = async (category, page = 1) => {
|
|||||||
} else if (category.startsWith("status/")) {
|
} else if (category.startsWith("status/")) {
|
||||||
url = URLS.status(category.replace("status/", ""));
|
url = URLS.status(category.replace("status/", ""));
|
||||||
} else {
|
} else {
|
||||||
|
// NOTE: Fallback to direct URL construction for unknown category types
|
||||||
url = `${URLS.home}/${category}`;
|
url = `${URLS.home}/${category}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +72,8 @@ const extractCategory = async (category, page = 1) => {
|
|||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
// NOTE: The list items are contained in #list-items container
|
||||||
$("#list-items > .item").each((i, el) => {
|
$("#list-items > .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||||
@@ -33,6 +86,7 @@ const extractCategory = async (category, page = 1) => {
|
|||||||
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
||||||
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
results.push({
|
results.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -56,3 +110,5 @@ const extractCategory = async (category, page = 1) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractCategory };
|
export { extractCategory };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: category.extractor.js
|
||||||
|
|||||||
@@ -1,14 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — episodeList.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts the full episode list for a given anime from anikototv.to.
|
||||||
|
* First resolves the anime ID from the watch page, then fetches the
|
||||||
|
* episode list via AJAX endpoint.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractEpisodeList
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS, BASE_URL } from "../configs/dataUrl.js";
|
import { URLS, BASE_URL } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// EPISODE LIST EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract complete episode list for an anime by slug ----
|
||||||
|
/**
|
||||||
|
* Fetches the anime watch page to resolve the internal anime ID, then
|
||||||
|
* queries the AJAX episode list endpoint to get all episodes with their
|
||||||
|
* metadata (ID, number, slug, title, active state, href).
|
||||||
|
*
|
||||||
|
* @param {string} slug - The anime slug identifier (e.g. "naruto-shippuden")
|
||||||
|
* @returns {Promise<Object>} Object with animeId, slug, totalEpisodes, and episodes array
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const episodeData = await extractEpisodeList("one-piece");
|
||||||
|
* console.log(episodeData.totalEpisodes); // e.g. 1100+
|
||||||
|
* console.log(episodeData.episodes[0].episode_no); // 1
|
||||||
|
*/
|
||||||
const extractEpisodeList = async (slug) => {
|
const extractEpisodeList = async (slug) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: First fetch the watch page to extract the internal anime ID
|
||||||
const infoUrl = URLS.watch(slug);
|
const infoUrl = URLS.watch(slug);
|
||||||
const { data: infoData } = await axios.get(infoUrl, { headers });
|
const { data: infoData } = await axios.get(infoUrl, { headers });
|
||||||
const $ = cheerio.load(infoData);
|
const $ = cheerio.load(infoData);
|
||||||
|
|
||||||
|
// NOTE: data-id on #watch-main is the anime ID used for the AJAX episode list call
|
||||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||||
|
|
||||||
if (!animeId) {
|
if (!animeId) {
|
||||||
@@ -16,6 +54,7 @@ const extractEpisodeList = async (slug) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// NOTE: AJAX endpoint requires X-Requested-With header to return HTML fragment
|
||||||
const ajaxUrl = `${BASE_URL}/ajax/episode/list/${animeId}`;
|
const ajaxUrl = `${BASE_URL}/ajax/episode/list/${animeId}`;
|
||||||
const { data: ajaxData } = await axios.get(ajaxUrl, {
|
const { data: ajaxData } = await axios.get(ajaxUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -26,13 +65,17 @@ const extractEpisodeList = async (slug) => {
|
|||||||
|
|
||||||
const $ep = cheerio.load(ajaxData);
|
const $ep = cheerio.load(ajaxData);
|
||||||
|
|
||||||
|
// ---- FEATURE: Parse episode list from AJAX response HTML ----
|
||||||
|
// NOTE: Episode links have either data-num or data-ep-id attributes
|
||||||
const episodes = [];
|
const episodes = [];
|
||||||
$ep("a[data-num], a[data-ep-id]").each((i, el) => {
|
$ep("a[data-num], a[data-ep-id]").each((i, el) => {
|
||||||
const epId = $ep(el).attr("data-ep-id") || $ep(el).attr("data-id") || "";
|
const epId = $ep(el).attr("data-ep-id") || $ep(el).attr("data-id") || "";
|
||||||
const epNum = parseInt($ep(el).attr("data-num")) || i + 1;
|
const epNum = parseInt($ep(el).attr("data-num")) || i + 1;
|
||||||
const epSlug = $ep(el).attr("data-slug") || "";
|
const epSlug = $ep(el).attr("data-slug") || "";
|
||||||
const href = $ep(el).attr("href") || "";
|
const href = $ep(el).attr("href") || "";
|
||||||
|
// NOTE: Episode title is in .ep-title or .ep-name span
|
||||||
const epTitle = $ep(el).find(".ep-title, .ep-name").text().trim() || "";
|
const epTitle = $ep(el).find(".ep-title, .ep-name").text().trim() || "";
|
||||||
|
// NOTE: Active class indicates the currently-playing episode
|
||||||
const isActive = $ep(el).hasClass("active") || false;
|
const isActive = $ep(el).hasClass("active") || false;
|
||||||
|
|
||||||
episodes.push({
|
episodes.push({
|
||||||
@@ -52,6 +95,7 @@ const extractEpisodeList = async (slug) => {
|
|||||||
episodes
|
episodes
|
||||||
};
|
};
|
||||||
} catch (ajaxError) {
|
} catch (ajaxError) {
|
||||||
|
// NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue)
|
||||||
return {
|
return {
|
||||||
animeId,
|
animeId,
|
||||||
slug,
|
slug,
|
||||||
@@ -65,3 +109,5 @@ const extractEpisodeList = async (slug) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractEpisodeList };
|
export { extractEpisodeList };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: episodeList.extractor.js
|
||||||
|
|||||||
@@ -1,15 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — episodeListAjax.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Low-level AJAX extractor for fetching raw episode list HTML from
|
||||||
|
* anikototv.to's server-side endpoint. Supports optional style and VRF
|
||||||
|
* query parameters for custom episode list rendering.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractEpisodeListAjax
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { BASE_URL } from "../configs/dataUrl.js";
|
import { BASE_URL } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// EPISODE LIST AJAX EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Fetch raw episode list HTML via AJAX with optional params ----
|
||||||
|
/**
|
||||||
|
* Directly queries the AJAX episode list endpoint for a given anime ID.
|
||||||
|
* Returns the raw HTML response from the server — does not parse or
|
||||||
|
* transform the data. Use this when you need the raw response or want
|
||||||
|
* to apply custom style/VRF parameters.
|
||||||
|
*
|
||||||
|
* @param {number|string} animeId - The internal anime ID from anikototv.to
|
||||||
|
* @param {string} [style=""] - Optional style parameter for episode list rendering
|
||||||
|
* @param {string} [vrf=""] - Optional VRF parameter (verification/decryption key)
|
||||||
|
* @returns {Promise<string>} Raw HTML response from the AJAX endpoint
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const rawHtml = await extractEpisodeListAjax(12345);
|
||||||
|
* console.log(rawHtml); // HTML fragment with episode links
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const rawHtml = await extractEpisodeListAjax(12345, "modern", "abc123");
|
||||||
|
*/
|
||||||
const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
|
const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
|
||||||
try {
|
try {
|
||||||
let url = `${BASE_URL}/ajax/episode/list/${animeId}`;
|
let url = `${BASE_URL}/ajax/episode/list/${animeId}`;
|
||||||
|
// NOTE: Optional params are appended only if non-empty to avoid trailing ?/&
|
||||||
const params = [];
|
const params = [];
|
||||||
if (style) params.push(`style=${style}`);
|
if (style) params.push(`style=${style}`);
|
||||||
if (vrf) params.push(`vrf=${vrf}`);
|
if (vrf) params.push(`vrf=${vrf}`);
|
||||||
if (params.length) url += `?${params.join("&")}`;
|
if (params.length) url += `?${params.join("&")}`;
|
||||||
|
|
||||||
|
// NOTE: X-Requested-With header is required — without it the server returns the full page
|
||||||
const { data } = await axios.get(url, {
|
const { data } = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
@@ -24,3 +67,5 @@ const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractEpisodeListAjax };
|
export { extractEpisodeListAjax };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: episodeListAjax.extractor.js
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — filter.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Provides advanced anime filtering on anikototv.to — supports filtering
|
||||||
|
* by genre, type, status, language, rating, sort order, season, year,
|
||||||
|
* and keyword with full pagination support.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractFilter
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
@@ -5,56 +23,97 @@ import { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS } from "../config
|
|||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// FILTER EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract paginated anime results with multi-criteria filtering ----
|
||||||
|
/**
|
||||||
|
* Builds a filter URL from the given parameters and fetches matching anime
|
||||||
|
* from anikototv.to. Supports keyword, genre, type, status, language,
|
||||||
|
* rating, sort, season, and year filters — all combinable.
|
||||||
|
*
|
||||||
|
* @param {Object} params - Filter parameters object
|
||||||
|
* @param {string} [params.keyword] - Search keyword to filter by name
|
||||||
|
* @param {string} [params.genre] - Comma-separated genre names (e.g. "action,comedy")
|
||||||
|
* @param {string} [params.type] - Comma-separated types (e.g. "tv,movie")
|
||||||
|
* @param {string} [params.status] - Comma-separated statuses (e.g. "airing,completed")
|
||||||
|
* @param {string} [params.language] - Comma-separated languages (e.g. "sub,dub")
|
||||||
|
* @param {string} [params.rating] - Comma-separated ratings (e.g. "pg-13,r")
|
||||||
|
* @param {string} [params.sort] - Sort order key (e.g. "score", "name")
|
||||||
|
* @param {string} [params.season] - Comma-separated seasons (e.g. "winter,spring")
|
||||||
|
* @param {string} [params.year] - Comma-separated years (e.g. "2024,2025")
|
||||||
|
* @param {number} [params.page=1] - Page number for pagination
|
||||||
|
* @returns {Promise<Object>} Object with totalPages count and data array of results
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const filtered = await extractFilter({ genre: "action", type: "tv", page: 1 });
|
||||||
|
* console.log(filtered.totalPages);
|
||||||
|
* console.log(filtered.data[0].title);
|
||||||
|
*/
|
||||||
const extractFilter = async (params) => {
|
const extractFilter = async (params) => {
|
||||||
try {
|
try {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// ---- FEATURE: Build keyword query parameter ----
|
||||||
|
// WARNING: keyword param is always set — empty string if not provided (site requires it)
|
||||||
if (params.keyword) {
|
if (params.keyword) {
|
||||||
queryParams.set("keyword", params.keyword);
|
queryParams.set("keyword", params.keyword);
|
||||||
} else {
|
} else {
|
||||||
queryParams.set("keyword", "");
|
queryParams.set("keyword", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Map genre names to IDs and append ----
|
||||||
|
// NOTE: GENRE_IDS config maps human-readable names to site-specific IDs
|
||||||
if (params.genre) {
|
if (params.genre) {
|
||||||
const genres = params.genre.split(",").map(g => GENRE_IDS[g.trim().toLowerCase()] || g.trim());
|
const genres = params.genre.split(",").map(g => GENRE_IDS[g.trim().toLowerCase()] || g.trim());
|
||||||
genres.forEach(g => queryParams.append("genre[]", g));
|
genres.forEach(g => queryParams.append("genre[]", g));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Map type names to IDs and append ----
|
||||||
if (params.type) {
|
if (params.type) {
|
||||||
const types = params.type.split(",").map(t => TYPE_IDS[t.trim().toLowerCase()] || t.trim());
|
const types = params.type.split(",").map(t => TYPE_IDS[t.trim().toLowerCase()] || t.trim());
|
||||||
types.forEach(t => queryParams.append("term_type[]", t));
|
types.forEach(t => queryParams.append("term_type[]", t));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Map status names to IDs and append ----
|
||||||
if (params.status) {
|
if (params.status) {
|
||||||
const statuses = params.status.split(",").map(s => STATUS_IDS[s.trim().toLowerCase()] || s.trim());
|
const statuses = params.status.split(",").map(s => STATUS_IDS[s.trim().toLowerCase()] || s.trim());
|
||||||
statuses.forEach(s => queryParams.append("status[]", s));
|
statuses.forEach(s => queryParams.append("status[]", s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Append language filter ----
|
||||||
|
// NOTE: Languages are passed directly without ID mapping
|
||||||
if (params.language) {
|
if (params.language) {
|
||||||
const langs = params.language.split(",");
|
const langs = params.language.split(",");
|
||||||
langs.forEach(l => queryParams.append("language[]", l.trim()));
|
langs.forEach(l => queryParams.append("language[]", l.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Map rating names to IDs and append ----
|
||||||
if (params.rating) {
|
if (params.rating) {
|
||||||
const ratings = params.rating.split(",").map(r => RATING_IDS[r.trim().toLowerCase()] || r.trim());
|
const ratings = params.rating.split(",").map(r => RATING_IDS[r.trim().toLowerCase()] || r.trim());
|
||||||
ratings.forEach(r => queryParams.append("rating[]", r));
|
ratings.forEach(r => queryParams.append("rating[]", r));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Map sort key to sort value ----
|
||||||
if (params.sort) {
|
if (params.sort) {
|
||||||
const sort = SORT_IDS[params.sort.trim().toLowerCase()] ?? params.sort;
|
const sort = SORT_IDS[params.sort.trim().toLowerCase()] ?? params.sort;
|
||||||
if (sort) queryParams.set("sort", sort);
|
if (sort) queryParams.set("sort", sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Append season filter ----
|
||||||
if (params.season) {
|
if (params.season) {
|
||||||
const seasons = params.season.split(",");
|
const seasons = params.season.split(",");
|
||||||
seasons.forEach(s => queryParams.append("season[]", s.trim()));
|
seasons.forEach(s => queryParams.append("season[]", s.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- FEATURE: Append year filter ----
|
||||||
if (params.year) {
|
if (params.year) {
|
||||||
const years = params.year.split(",");
|
const years = params.year.split(",");
|
||||||
years.forEach(y => queryParams.append("year[]", y.trim()));
|
years.forEach(y => queryParams.append("year[]", y.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: The filter URL is built by appending query params to the search base URL
|
||||||
const filterUrl = `${URLS.search}?${queryParams.toString()}`;
|
const filterUrl = `${URLS.search}?${queryParams.toString()}`;
|
||||||
const $ = await extractPages(filterUrl, params.page || 1);
|
const $ = await extractPages(filterUrl, params.page || 1);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
@@ -95,3 +154,5 @@ const extractFilter = async (params) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractFilter };
|
export { extractFilter };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: filter.extractor.js
|
||||||
|
|||||||
@@ -1,22 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — homeInfo.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts homepage data from anikototv.to including spotlight carousel,
|
||||||
|
* trending anime, top airing list, and available genre filters.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractHomeInfo
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// HOME INFO EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract full homepage data (spotlights, trending, top airing, genres) ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses the anikototv.to homepage to extract spotlight carousel,
|
||||||
|
* recently updated trending anime, top airing of the day, and genre list.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>} Object containing spotlights, trending, topAiring, and genres arrays
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const home = await extractHomeInfo();
|
||||||
|
* console.log(home.spotlights.length); // number of spotlight items
|
||||||
|
*/
|
||||||
const extractHomeInfo = async () => {
|
const extractHomeInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get(URLS.home, { headers });
|
const { data } = await axios.get(URLS.home, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract spotlight carousel items from #hotest section ----
|
||||||
|
// NOTE: Swiper slides contain the hero carousel on the homepage
|
||||||
const spotlights = [];
|
const spotlights = [];
|
||||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
$("#hotest .swiper-slide.item").each((i, el) => {
|
||||||
|
// NOTE: The play button's href contains the /watch/{slug} path
|
||||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
||||||
|
// NOTE: Poster is set as inline CSS background-image on a div inside .image
|
||||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||||
const title = $(el).find("h2.title.d-title").text().trim() || "";
|
const title = $(el).find("h2.title.d-title").text().trim() || "";
|
||||||
|
// NOTE: data-jp attribute holds the Japanese/romaji title variant
|
||||||
const japaneseTitle = $(el).find("h2.title.d-title").attr("data-jp") || "";
|
const japaneseTitle = $(el).find("h2.title.d-title").attr("data-jp") || "";
|
||||||
const description = $(el).find(".desc").text().trim() || "";
|
const description = $(el).find(".desc").text().trim() || "";
|
||||||
const rating = $(el).find("i.rating").text().trim() || "";
|
const rating = $(el).find("i.rating").text().trim() || "";
|
||||||
const quality = $(el).find("i.quality").text().trim() || "";
|
const quality = $(el).find("i.quality").text().trim() || "";
|
||||||
|
// NOTE: sub/dub counts are extracted as integers, defaulting to 0
|
||||||
const sub = parseInt($(el).find("i.sub").text().trim()) || 0;
|
const sub = parseInt($(el).find("i.sub").text().trim()) || 0;
|
||||||
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
||||||
const date = $(el).find("i.date").text().trim() || "";
|
const date = $(el).find("i.date").text().trim() || "";
|
||||||
@@ -37,12 +75,15 @@ const extractHomeInfo = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract trending/recently-updated anime list ----
|
||||||
|
// NOTE: Combines both .section-updated and #recent-update containers for coverage
|
||||||
const trending = [];
|
const trending = [];
|
||||||
$(".section-updated .item, #recent-update .item").each((i, el) => {
|
$(".section-updated .item, #recent-update .item").each((i, el) => {
|
||||||
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
const title = $(el).find("a.name.d-title").text().trim() || "";
|
const title = $(el).find("a.name.d-title").text().trim() || "";
|
||||||
const japaneseTitle = $(el).find("a.name.d-title").attr("data-jp") || "";
|
const japaneseTitle = $(el).find("a.name.d-title").attr("data-jp") || "";
|
||||||
|
// NOTE: Episode counts are in .ep-status spans — sub, dub, and total
|
||||||
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
||||||
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
||||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||||
@@ -62,6 +103,8 @@ const extractHomeInfo = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract top airing anime of the day ----
|
||||||
|
// NOTE: Only the 'day' tab content is scraped from #top-anime
|
||||||
const topAiring = [];
|
const topAiring = [];
|
||||||
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
||||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
@@ -76,6 +119,8 @@ const extractHomeInfo = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract genre filter list from navigation menu ----
|
||||||
|
// NOTE: Only links containing /genre/ in href are considered valid genre entries
|
||||||
const genres = [];
|
const genres = [];
|
||||||
$("#menu ul.c4 li a").each((i, el) => {
|
$("#menu ul.c4 li a").each((i, el) => {
|
||||||
const genre = $(el).text().trim();
|
const genre = $(el).text().trim();
|
||||||
@@ -97,3 +142,5 @@ const extractHomeInfo = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractHomeInfo };
|
export { extractHomeInfo };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: homeInfo.extractor.js
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — newRelease.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts newly released and newly added anime with pagination.
|
||||||
|
* Provides both new releases and sorted by addition date.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractNewRelease
|
||||||
|
* extractNewlyAdded
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
@@ -5,12 +23,43 @@ import { URLS } from "../configs/dataUrl.js";
|
|||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// NEW RELEASE EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract newly released anime with pagination ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses newly released anime with pagination.
|
||||||
|
* Returns paginated results with total page count.
|
||||||
|
*
|
||||||
|
* @param {number} page - Page number to fetch (default: 1)
|
||||||
|
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||||
|
* @returns {number} return.totalPages - Total number of pages available
|
||||||
|
* @returns {Array<Object>} return.data - Array of anime objects
|
||||||
|
* @returns {string} return.data[].slug - URL slug for the anime
|
||||||
|
* @returns {string} return.data[].poster - Poster image URL
|
||||||
|
* @returns {string} return.data[].title - English title
|
||||||
|
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||||
|
* @returns {number} return.data[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.data[].dub - Dubbed episode count
|
||||||
|
* @returns {number} return.data[].total - Total episode count
|
||||||
|
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||||
|
* @returns {string} return.data[].rating - Anime rating
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const newRelease = await extractNewRelease(1);
|
||||||
|
* console.log(newRelease.totalPages);
|
||||||
|
* console.log(newRelease.data[0].title);
|
||||||
|
*/
|
||||||
const extractNewRelease = async (page = 1) => {
|
const extractNewRelease = async (page = 1) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: extractPages handles pagination and returns parsed Cheerio object
|
||||||
const $ = await extractPages(URLS.newRelease, page);
|
const $ = await extractPages(URLS.newRelease, page);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
// NOTE: Multiple selectors to handle different page layouts
|
||||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find("img").attr("src") || "";
|
const poster = $(el).find("img").attr("src") || "";
|
||||||
@@ -22,6 +71,7 @@ const extractNewRelease = async (page = 1) => {
|
|||||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||||
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
results.push({
|
results.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -43,12 +93,42 @@ const extractNewRelease = async (page = 1) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// NEWLY ADDED EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract newly added anime sorted by addition date ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses newly added anime sorted by addition date.
|
||||||
|
* Uses the latestUpdated endpoint with sort=added parameter.
|
||||||
|
*
|
||||||
|
* @param {number} page - Page number to fetch (default: 1)
|
||||||
|
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||||
|
* @returns {number} return.totalPages - Total number of pages available
|
||||||
|
* @returns {Array<Object>} return.data - Array of anime objects
|
||||||
|
* @returns {string} return.data[].slug - URL slug for the anime
|
||||||
|
* @returns {string} return.data[].poster - Poster image URL
|
||||||
|
* @returns {string} return.data[].title - English title
|
||||||
|
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||||
|
* @returns {number} return.data[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.data[].dub - Dubbed episode count
|
||||||
|
* @returns {number} return.data[].total - Total episode count
|
||||||
|
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const newlyAdded = await extractNewlyAdded(1);
|
||||||
|
* console.log(newlyAdded.totalPages);
|
||||||
|
* console.log(newlyAdded.data[0].title);
|
||||||
|
*/
|
||||||
const extractNewlyAdded = async (page = 1) => {
|
const extractNewlyAdded = async (page = 1) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: The latestUpdated endpoint with sort=added returns recently added anime
|
||||||
const $ = await extractPages(`${URLS.latestUpdated}?sort=added`, page);
|
const $ = await extractPages(`${URLS.latestUpdated}?sort=added`, page);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
// NOTE: Multiple selectors to handle different page layouts
|
||||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find("img").attr("src") || "";
|
const poster = $(el).find("img").attr("src") || "";
|
||||||
@@ -59,6 +139,7 @@ const extractNewlyAdded = async (page = 1) => {
|
|||||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
results.push({
|
results.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -80,3 +161,5 @@ const extractNewlyAdded = async (page = 1) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractNewRelease, extractNewlyAdded };
|
export { extractNewRelease, extractNewlyAdded };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: newRelease.extractor.js
|
||||||
|
|||||||
@@ -1,15 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — popular.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts most viewed/popular anime with pagination support.
|
||||||
|
* Scrapes the most viewed section with page navigation.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractPopular
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// POPULAR EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract most viewed/popular anime with pagination ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses the most viewed anime list with pagination.
|
||||||
|
* Returns paginated results with total page count.
|
||||||
|
*
|
||||||
|
* @param {number} page - Page number to fetch (default: 1)
|
||||||
|
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||||
|
* @returns {number} return.totalPages - Total number of pages available
|
||||||
|
* @returns {Array<Object>} return.data - Array of popular anime objects
|
||||||
|
* @returns {string} return.data[].slug - URL slug for the anime
|
||||||
|
* @returns {string} return.data[].animeId - Internal anime ID
|
||||||
|
* @returns {string} return.data[].poster - Poster image URL
|
||||||
|
* @returns {string} return.data[].title - English title
|
||||||
|
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||||
|
* @returns {number} return.data[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.data[].dub - Dubbed episode count
|
||||||
|
* @returns {number} return.data[].total - Total episode count
|
||||||
|
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||||
|
* @returns {string} return.data[].rating - Anime rating
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const popular = await extractPopular(1);
|
||||||
|
* console.log(popular.totalPages); // Total pages available
|
||||||
|
* console.log(popular.data[0].title); // First anime title
|
||||||
|
*/
|
||||||
const extractPopular = async (page = 1) => {
|
const extractPopular = async (page = 1) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: extractPages handles pagination and returns parsed Cheerio object
|
||||||
const $ = await extractPages(URLS.mostViewed, page);
|
const $ = await extractPages(URLS.mostViewed, page);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
// NOTE: The list items are contained in #list-items container
|
||||||
$("#list-items > .item").each((i, el) => {
|
$("#list-items > .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||||
@@ -22,6 +71,7 @@ const extractPopular = async (page = 1) => {
|
|||||||
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
||||||
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
results.push({
|
results.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -45,3 +95,5 @@ const extractPopular = async (page = 1) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractPopular };
|
export { extractPopular };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: popular.extractor.js
|
||||||
|
|||||||
@@ -1,29 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — random.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Fetches a random anime from anikototv.to by following the redirect
|
||||||
|
* chain from the random endpoint, then extracts the full anime detail
|
||||||
|
* page metadata from the resolved URL.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractRandom
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// RANDOM ANIME EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Fetch a random anime and extract its detail metadata ----
|
||||||
|
/**
|
||||||
|
* Hits the random endpoint on anikototv.to which redirects to a random
|
||||||
|
* anime page. Follows up to 5 redirects, extracts the final URL to get
|
||||||
|
* the slug, then parses the full anime detail page for metadata.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>} Random anime data with slug, title, poster, synopsis, etc.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const random = await extractRandom();
|
||||||
|
* console.log(random.title); // random anime title
|
||||||
|
* console.log(random.url); // full resolved URL
|
||||||
|
*/
|
||||||
const extractRandom = async () => {
|
const extractRandom = async () => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: maxRedirects=5 ensures we follow the random redirect chain
|
||||||
|
// NOTE: validateStatus allows any 2xx/3xx response without throwing
|
||||||
const { data, request, response } = await axios.get(URLS.random, {
|
const { data, request, response } = await axios.get(URLS.random, {
|
||||||
headers,
|
headers,
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
validateStatus: (status) => status < 400
|
validateStatus: (status) => status < 400
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: responseURL on the request object gives us the final resolved URL after redirects
|
||||||
const finalUrl = request?.responseURL || URLS.random;
|
const finalUrl = request?.responseURL || URLS.random;
|
||||||
|
// NOTE: The slug is extracted from the /watch/{slug} path in the final URL
|
||||||
const slug = finalUrl.split("/watch/").pop() || "";
|
const slug = finalUrl.split("/watch/").pop() || "";
|
||||||
|
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime title and identification ----
|
||||||
const title = $("h1[itemprop='name'].title.d-title").text().trim() || "";
|
const title = $("h1[itemprop='name'].title.d-title").text().trim() || "";
|
||||||
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
||||||
const poster = $("img[itemprop='image']").attr("src") || "";
|
const poster = $("img[itemprop='image']").attr("src") || "";
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract type, synopsis, rating, and anime ID ----
|
||||||
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
||||||
const synopsis = $(".synopsis .content").text().trim() || "";
|
const synopsis = $(".synopsis .content").text().trim() || "";
|
||||||
const rating = $("#w-rating .score .value").text().trim() || "";
|
const rating = $("#w-rating .score .value").text().trim() || "";
|
||||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract genres ----
|
||||||
|
// NOTE: Genre links are identified by '/genre/' in their href attribute
|
||||||
const genres = [];
|
const genres = [];
|
||||||
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
||||||
genres.push($(el).text().trim());
|
genres.push($(el).text().trim());
|
||||||
@@ -47,3 +91,5 @@ const extractRandom = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractRandom };
|
export { extractRandom };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: random.extractor.js
|
||||||
|
|||||||
@@ -1,21 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — schedule.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts anime schedule for a specific date.
|
||||||
|
* Returns airing times and episode numbers for scheduled anime.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractSchedule
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SCHEDULE EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime schedule for a specific date ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses the anime schedule for a given date.
|
||||||
|
* Returns airing times and episode numbers for scheduled anime.
|
||||||
|
*
|
||||||
|
* @param {string} date - Date to fetch schedule for (format: YYYY-MM-DD)
|
||||||
|
* @returns {Promise<Array<Object>>} Array of scheduled anime objects
|
||||||
|
* @returns {string} return.slug - URL slug for the anime
|
||||||
|
* @returns {string} return.title - Anime title
|
||||||
|
* @returns {string} return.time - Airing time
|
||||||
|
* @returns {number} return.episode_no - Episode number
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const schedule = await extractSchedule("2024-01-15");
|
||||||
|
* console.log(schedule[0].time); // Airing time
|
||||||
|
* console.log(schedule[0].episode_no); // Episode number
|
||||||
|
*/
|
||||||
const extractSchedule = async (date) => {
|
const extractSchedule = async (date) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: The schedule is fetched by appending date parameter to homepage
|
||||||
const url = `${URLS.home}?date=${date}`;
|
const url = `${URLS.home}?date=${date}`;
|
||||||
const { data } = await axios.get(url, { headers });
|
const { data } = await axios.get(url, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
const schedule = [];
|
const schedule = [];
|
||||||
|
|
||||||
|
// NOTE: Schedule items use different selectors depending on page structure
|
||||||
$(".schedule-item, .anime-schedule .item").each((i, el) => {
|
$(".schedule-item, .anime-schedule .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const title = $(el).find(".film-name a, .name").text().trim() || "";
|
const title = $(el).find(".film-name a, .name").text().trim() || "";
|
||||||
const time = $(el).find(".time, .schedule-time").text().trim() || "";
|
const time = $(el).find(".time, .schedule-time").text().trim() || "";
|
||||||
const episodeNo = parseInt($(el).find(".episode-no, .ep-num").text().trim()) || 0;
|
const episodeNo = parseInt($(el).find(".episode-no, .ep-num").text().trim()) || 0;
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
schedule.push({
|
schedule.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -33,3 +75,5 @@ const extractSchedule = async (date) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractSchedule };
|
export { extractSchedule };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: schedule.extractor.js
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — search.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Provides search functionality for anikototv.to — supports paginated
|
||||||
|
* search results and lightweight autocomplete suggestions.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractSearchResults, extractSearchSuggestions
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
@@ -5,23 +22,47 @@ import { URLS } from "../configs/dataUrl.js";
|
|||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SEARCH RESULTS EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract paginated search results for a keyword ----
|
||||||
|
/**
|
||||||
|
* Searches anikototv.to for anime matching the given keyword and returns
|
||||||
|
* paginated results with full metadata including episode counts and genres.
|
||||||
|
*
|
||||||
|
* @param {string} keyword - The search query string
|
||||||
|
* @param {number} [page=1] - Page number for pagination
|
||||||
|
* @returns {Promise<Object>} Object with totalPages count and data array of results
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const results = await extractSearchResults("naruto", 1);
|
||||||
|
* console.log(results.totalPages); // total available pages
|
||||||
|
* console.log(results.data[0].title); // first result title
|
||||||
|
*/
|
||||||
const extractSearchResults = async (keyword, page = 1) => {
|
const extractSearchResults = async (keyword, page = 1) => {
|
||||||
try {
|
try {
|
||||||
|
// WARNING: keyword param is required — omitting it may cause a 500 error from the source
|
||||||
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
||||||
const $ = await extractPages(url, page);
|
const $ = await extractPages(url, page);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
// NOTE: #list-items > .item is the standard result container on anikototv.to search pages
|
||||||
$("#list-items > .item").each((i, el) => {
|
$("#list-items > .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
|
// NOTE: .ani.poster.tip is the poster container with tooltip data
|
||||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||||
const title = $(el).find(".info .b1 a.name.d-title").text().trim() || "";
|
const title = $(el).find(".info .b1 a.name.d-title").text().trim() || "";
|
||||||
const japaneseTitle = $(el).find(".info .b1 a.name.d-title").attr("data-jp") || "";
|
const japaneseTitle = $(el).find(".info .b1 a.name.d-title").attr("data-jp") || "";
|
||||||
|
// NOTE: data-tip attribute on the poster container holds the animeId
|
||||||
const animeId = $(el).find(".ani.poster.tip").attr("data-tip") || "";
|
const animeId = $(el).find(".ani.poster.tip").attr("data-tip") || "";
|
||||||
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
||||||
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
||||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||||
|
// NOTE: Type is in the second .m-item label (e.g. TV, Movie, OVA)
|
||||||
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
||||||
|
// NOTE: Rating is in a dedicated .m-item.rated span
|
||||||
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
||||||
const genres = [];
|
const genres = [];
|
||||||
$(el).find(".info .b1 .genre a").each((j, g) => {
|
$(el).find(".info .b1 .genre a").each((j, g) => {
|
||||||
@@ -51,6 +92,22 @@ const extractSearchResults = async (keyword, page = 1) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SEARCH SUGGESTIONS EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract lightweight autocomplete suggestions (capped at 10) ----
|
||||||
|
/**
|
||||||
|
* Fetches search suggestions for a keyword — returns a lightweight subset
|
||||||
|
* of result data suitable for autocomplete dropdowns.
|
||||||
|
*
|
||||||
|
* @param {string} keyword - The search query string
|
||||||
|
* @returns {Promise<Array>} Array of up to 10 suggestion objects with slug, poster, title, type
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const suggestions = await extractSearchSuggestions("one piece");
|
||||||
|
* console.log(suggestions.length); // max 10
|
||||||
|
*/
|
||||||
const extractSearchSuggestions = async (keyword) => {
|
const extractSearchSuggestions = async (keyword) => {
|
||||||
try {
|
try {
|
||||||
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
||||||
@@ -69,6 +126,7 @@ const extractSearchSuggestions = async (keyword) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Limit to 10 suggestions to keep autocomplete payloads small
|
||||||
return suggestions.slice(0, 10);
|
return suggestions.slice(0, 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -76,3 +134,5 @@ const extractSearchSuggestions = async (keyword) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractSearchResults, extractSearchSuggestions };
|
export { extractSearchResults, extractSearchSuggestions };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: search.extractor.js
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — seasons.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts season information for a specific anime via API.
|
||||||
|
* Returns season data from the internal API endpoint.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractSeasons
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { BASE_URL } from "../configs/dataUrl.js";
|
import { BASE_URL } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SEASONS API EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract season data from internal API ----
|
||||||
|
/**
|
||||||
|
* Fetches season information for a specific anime from the internal API.
|
||||||
|
* Returns an array of season objects or empty array on error.
|
||||||
|
*
|
||||||
|
* @param {string} animeId - The anime ID to fetch seasons for
|
||||||
|
* @returns {Promise<Array<Object>>} Array of season objects or empty array
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const seasons = await extractSeasons("12345");
|
||||||
|
* console.log(seasons.length); // Number of seasons
|
||||||
|
*/
|
||||||
const extractSeasons = async (animeId) => {
|
const extractSeasons = async (animeId) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: This is an internal API endpoint requiring X-Requested-With header
|
||||||
const url = `${BASE_URL}/api/seasons/${animeId}`;
|
const url = `${BASE_URL}/api/seasons/${animeId}`;
|
||||||
const { data } = await axios.get(url, {
|
const { data } = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -14,8 +48,11 @@ const extractSeasons = async (animeId) => {
|
|||||||
|
|
||||||
return data || [];
|
return data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// NOTE: Return empty array on error to maintain consistent API
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { extractSeasons };
|
export { extractSeasons };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: seasons.extractor.js
|
||||||
|
|||||||
@@ -1,14 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — spotlight.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts spotlight/featured anime items from the homepage.
|
||||||
|
* Scrapes the "hottest" carousel section for trending anime.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractSpotlight
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SPOTLIGHT EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract featured/spotlight anime from homepage ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses the spotlight carousel from the homepage.
|
||||||
|
* Returns an array of featured anime with metadata like poster, title,
|
||||||
|
* rating, quality, and episode counts.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Array<Object>>} Array of spotlight anime objects
|
||||||
|
* @returns {string} return.slug - URL slug for the anime
|
||||||
|
* @returns {string} return.poster - Poster image URL
|
||||||
|
* @returns {string} return.title - English title
|
||||||
|
* @returns {string} return.japaneseTitle - Japanese title
|
||||||
|
* @returns {string} return.description - Short description
|
||||||
|
* @returns {string} return.rating - Anime rating
|
||||||
|
* @returns {string} return.quality - Video quality (e.g., HD)
|
||||||
|
* @returns {number} return.sub - Subbed episode count
|
||||||
|
* @returns {number} return.dub - Dubbed episode count
|
||||||
|
* @returns {string} return.date - Release date
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const spotlights = await extractSpotlight();
|
||||||
|
* console.log(spotlights[0].title);
|
||||||
|
*/
|
||||||
const extractSpotlight = async () => {
|
const extractSpotlight = async () => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: The homepage contains the spotlight carousel in #hotest section
|
||||||
const { data } = await axios.get(URLS.home, { headers });
|
const { data } = await axios.get(URLS.home, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
const spotlights = [];
|
const spotlights = [];
|
||||||
|
|
||||||
|
// NOTE: Each slide in the swiper carousel represents a featured anime
|
||||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
$("#hotest .swiper-slide.item").each((i, el) => {
|
||||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||||
@@ -21,6 +67,7 @@ const extractSpotlight = async () => {
|
|||||||
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
||||||
const date = $(el).find("i.date").text().trim() || "";
|
const date = $(el).find("i.date").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
spotlights.push({
|
spotlights.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -44,3 +91,5 @@ const extractSpotlight = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractSpotlight };
|
export { extractSpotlight };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: spotlight.extractor.js
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — status.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts anime filtered by status (ongoing, completed, etc.).
|
||||||
|
* Supports pagination for browsing large result sets.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractStatus
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
@@ -5,13 +22,46 @@ import { URLS } from "../configs/dataUrl.js";
|
|||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STATUS FILTER EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime filtered by status with pagination ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses anime filtered by status (ongoing, completed, etc.).
|
||||||
|
* Returns paginated results with total page count.
|
||||||
|
*
|
||||||
|
* @param {string} status - Status to filter by (e.g., "ongoing", "completed")
|
||||||
|
* @param {number} page - Page number to fetch (default: 1)
|
||||||
|
* @returns {Promise<Object>} Object containing totalPages, status, and data array
|
||||||
|
* @returns {number} return.totalPages - Total number of pages available
|
||||||
|
* @returns {string} return.status - The status filter used
|
||||||
|
* @returns {Array<Object>} return.data - Array of anime objects
|
||||||
|
* @returns {string} return.data[].slug - URL slug for the anime
|
||||||
|
* @returns {string} return.data[].poster - Poster image URL
|
||||||
|
* @returns {string} return.data[].title - English title
|
||||||
|
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||||
|
* @returns {number} return.data[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.data[].dub - Dubbed episode count
|
||||||
|
* @returns {number} return.data[].total - Total episode count
|
||||||
|
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||||
|
* @returns {string} return.data[].rating - Anime rating
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const ongoing = await extractStatus("ongoing", 1);
|
||||||
|
* console.log(ongoing.status); // "ongoing"
|
||||||
|
* console.log(ongoing.data[0].title); // First ongoing anime
|
||||||
|
*/
|
||||||
const extractStatus = async (status, page = 1) => {
|
const extractStatus = async (status, page = 1) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: URLS.status generates the correct endpoint for the given status
|
||||||
const url = URLS.status(status);
|
const url = URLS.status(status);
|
||||||
const $ = await extractPages(url, page);
|
const $ = await extractPages(url, page);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
// NOTE: Multiple selectors to handle different page layouts
|
||||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find("img").attr("src") || "";
|
const poster = $(el).find("img").attr("src") || "";
|
||||||
@@ -23,6 +73,7 @@ const extractStatus = async (status, page = 1) => {
|
|||||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||||
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
results.push({
|
results.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -45,3 +96,5 @@ const extractStatus = async (status, page = 1) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractStatus };
|
export { extractStatus };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: status.extractor.js
|
||||||
|
|||||||
@@ -1,10 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — streamInfo.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Handles server/stream resolution for episodes — fetches individual
|
||||||
|
* stream URLs, server lists via AJAX, and alternative servers from
|
||||||
|
* the nekostream mapper API.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractStreamInfo, extractServerList, extractMapperServers
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { BASE_URL } from "../configs/dataUrl.js";
|
import { BASE_URL } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STREAM INFO EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Resolve a single stream URL from a linkId ----
|
||||||
|
/**
|
||||||
|
* Fetches the actual stream URL and skip data for a given server link ID.
|
||||||
|
* This is the primary method for resolving playable video URLs.
|
||||||
|
*
|
||||||
|
* @param {string} linkId - The server link ID to resolve
|
||||||
|
* @returns {Promise<Object>} Object with linkId, url, and skipData (intro/outro timestamps)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const stream = await extractStreamInfo("abc123");
|
||||||
|
* console.log(stream.url); // direct video URL
|
||||||
|
* console.log(stream.skipData); // intro/outro skip ranges
|
||||||
|
*/
|
||||||
const extractStreamInfo = async (linkId) => {
|
const extractStreamInfo = async (linkId) => {
|
||||||
try {
|
try {
|
||||||
const url = `${BASE_URL}/ajax/server?get=${linkId}`;
|
const url = `${BASE_URL}/ajax/server?get=${linkId}`;
|
||||||
|
// NOTE: X-Requested-With header is required for AJAX endpoints on anikototv.to
|
||||||
const { data } = await axios.get(url, {
|
const { data } = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
@@ -26,6 +62,22 @@ const extractStreamInfo = async (linkId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SERVER LIST EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Fetch available servers for a set of episode IDs ----
|
||||||
|
/**
|
||||||
|
* Retrieves the server list for the given episode IDs from the AJAX endpoint.
|
||||||
|
* Returns raw server data as provided by the source site.
|
||||||
|
*
|
||||||
|
* @param {string} episodeIds - Comma-separated or single episode ID string
|
||||||
|
* @returns {Promise<Object>} Raw server list data from the AJAX response
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const servers = await extractServerList("12345");
|
||||||
|
* // servers contains the server configuration for that episode
|
||||||
|
*/
|
||||||
const extractServerList = async (episodeIds) => {
|
const extractServerList = async (episodeIds) => {
|
||||||
try {
|
try {
|
||||||
const url = `${BASE_URL}/ajax/server/list?servers=${episodeIds}`;
|
const url = `${BASE_URL}/ajax/server/list?servers=${episodeIds}`;
|
||||||
@@ -42,6 +94,26 @@ const extractServerList = async (episodeIds) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MAPPER SERVERS EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Fetch alternative streaming servers from nekostream mapper API ----
|
||||||
|
/**
|
||||||
|
* Queries the nekostream mapper API for alternative streaming providers
|
||||||
|
* (sub and dub) for a given anime identified by MAL ID, slug, and timestamp.
|
||||||
|
* Returns a flat array of server objects per provider.
|
||||||
|
*
|
||||||
|
* @param {number|string} malId - MyAnimeList ID for the anime
|
||||||
|
* @param {string} slug - The anime slug on anikototv.to
|
||||||
|
* @param {string|number} timestamp - Episode timestamp identifier
|
||||||
|
* @returns {Promise<Array>} Array of server objects with provider, type, url, download fields
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const servers = await extractMapperServers(21, "one-piece", 1234567890);
|
||||||
|
* console.log(servers[0].provider); // "vidstreaming"
|
||||||
|
* console.log(servers[0].type); // "sub" or "dub"
|
||||||
|
*/
|
||||||
const extractMapperServers = async (malId, slug, timestamp) => {
|
const extractMapperServers = async (malId, slug, timestamp) => {
|
||||||
try {
|
try {
|
||||||
const url = `https://mapper.nekostream.site/api/mal/${malId}/${slug}/${timestamp}`;
|
const url = `https://mapper.nekostream.site/api/mal/${malId}/${slug}/${timestamp}`;
|
||||||
@@ -51,6 +123,7 @@ const extractMapperServers = async (malId, slug, timestamp) => {
|
|||||||
|
|
||||||
if (data && typeof data === "object") {
|
if (data && typeof data === "object") {
|
||||||
for (const [provider, sources] of Object.entries(data)) {
|
for (const [provider, sources] of Object.entries(data)) {
|
||||||
|
// NOTE: Each provider may have sub, dub, or both source types
|
||||||
if (sources && sources.sub) {
|
if (sources && sources.sub) {
|
||||||
servers.push({
|
servers.push({
|
||||||
provider,
|
provider,
|
||||||
@@ -72,8 +145,11 @@ const extractMapperServers = async (malId, slug, timestamp) => {
|
|||||||
|
|
||||||
return servers;
|
return servers;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// NOTE: Returns empty array on failure — mapper API may be unavailable
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { extractStreamInfo, extractServerList, extractMapperServers };
|
export { extractStreamInfo, extractServerList, extractMapperServers };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: streamInfo.extractor.js
|
||||||
|
|||||||
@@ -1,15 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — suggestion.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts search suggestions/autocomplete results for a keyword.
|
||||||
|
* Returns up to 10 matching anime suggestions.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractSuggestions
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SUGGESTION EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract search suggestions for a keyword ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses search suggestions for a given keyword.
|
||||||
|
* Returns up to 10 matching anime suggestions.
|
||||||
|
*
|
||||||
|
* @param {string} keyword - Search keyword to get suggestions for
|
||||||
|
* @returns {Promise<Array<Object>>} Array of suggestion objects (max 10)
|
||||||
|
* @returns {string} return.slug - URL slug for the anime
|
||||||
|
* @returns {string} return.poster - Poster image URL
|
||||||
|
* @returns {string} return.title - English title
|
||||||
|
* @returns {string} return.japaneseTitle - Japanese title
|
||||||
|
* @returns {string} return.type - Anime type (TV, Movie, etc.)
|
||||||
|
* @returns {number} return.sub - Subbed episode count
|
||||||
|
* @returns {number} return.dub - Dubbed episode count
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const suggestions = await extractSuggestions("naruto");
|
||||||
|
* console.log(suggestions.length); // Up to 10 results
|
||||||
|
* console.log(suggestions[0].title);
|
||||||
|
*/
|
||||||
const extractSuggestions = async (keyword) => {
|
const extractSuggestions = async (keyword) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: The search endpoint requires the 'keyword' parameter
|
||||||
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
||||||
const { data } = await axios.get(url, { headers });
|
const { data } = await axios.get(url, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
const suggestions = [];
|
const suggestions = [];
|
||||||
|
|
||||||
|
// NOTE: Search results use the same list structure as other pages
|
||||||
$("#list-items > .item").each((i, el) => {
|
$("#list-items > .item").each((i, el) => {
|
||||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||||
@@ -19,11 +63,13 @@ const extractSuggestions = async (keyword) => {
|
|||||||
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
||||||
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
suggestions.push({ slug, poster, title, japaneseTitle, type, sub, dub });
|
suggestions.push({ slug, poster, title, japaneseTitle, type, sub, dub });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Limit to 10 suggestions for autocomplete-style responses
|
||||||
return suggestions.slice(0, 10);
|
return suggestions.slice(0, 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -31,3 +77,5 @@ const extractSuggestions = async (keyword) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractSuggestions };
|
export { extractSuggestions };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: suggestion.extractor.js
|
||||||
|
|||||||
@@ -1,10 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — topten.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts top 10 anime rankings from the homepage.
|
||||||
|
* Parses daily, weekly, and monthly top anime lists.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractTopTen
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// TOP TEN EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract daily, weekly, and monthly top 10 anime ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses the top 10 anime rankings from the homepage.
|
||||||
|
* Returns three arrays: today, week, and month rankings.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>} Object containing today, week, and month arrays
|
||||||
|
* @returns {Array<Object>} return.today - Daily top 10 anime
|
||||||
|
* @returns {Array<Object>} return.week - Weekly top 10 anime
|
||||||
|
* @returns {Array<Object>} return.month - Monthly top 10 anime
|
||||||
|
* @returns {string} return.today[].slug - URL slug for the anime
|
||||||
|
* @returns {number} return.today[].rank - Ranking position (1-10)
|
||||||
|
* @returns {string} return.today[].name - Anime name
|
||||||
|
* @returns {string} return.today[].poster - Poster image URL
|
||||||
|
* @returns {number} return.today[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.today[].dub - Dubbed episode count
|
||||||
|
* @returns {string} return.today[].type - Anime type (TV, Movie, etc.)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const topTen = await extractTopTen();
|
||||||
|
* console.log(topTen.today[0].name); // #1 anime today
|
||||||
|
*/
|
||||||
const extractTopTen = async () => {
|
const extractTopTen = async () => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: The top 10 section uses tabbed content with data-name attributes
|
||||||
const { data } = await axios.get(URLS.home, { headers });
|
const { data } = await axios.get(URLS.home, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
@@ -12,6 +55,7 @@ const extractTopTen = async () => {
|
|||||||
const week = [];
|
const week = [];
|
||||||
const month = [];
|
const month = [];
|
||||||
|
|
||||||
|
// NOTE: Each tab contains a list of anime items with ranking data
|
||||||
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
||||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
@@ -21,11 +65,13 @@ const extractTopTen = async () => {
|
|||||||
const type = $(el).find(".type").text().trim() || "";
|
const type = $(el).find(".type").text().trim() || "";
|
||||||
const rank = i + 1;
|
const rank = i + 1;
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
today.push({ slug, rank, name, poster, sub, dub, type });
|
today.push({ slug, rank, name, poster, sub, dub, type });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Weekly rankings follow the same structure as daily
|
||||||
$("#top-anime .tab-content[data-name='week'] a.item").each((i, el) => {
|
$("#top-anime .tab-content[data-name='week'] a.item").each((i, el) => {
|
||||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
@@ -40,6 +86,7 @@ const extractTopTen = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Monthly rankings follow the same structure
|
||||||
$("#top-anime .tab-content[data-name='month'] a.item").each((i, el) => {
|
$("#top-anime .tab-content[data-name='month'] a.item").each((i, el) => {
|
||||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
@@ -61,3 +108,5 @@ const extractTopTen = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractTopTen };
|
export { extractTopTen };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: topten.extractor.js
|
||||||
|
|||||||
@@ -1,14 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — trending.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts trending and recently updated anime from the homepage.
|
||||||
|
* Scrapes both the updated section and recent updates section.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractTrending
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// TRENDING EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract trending and recently updated anime ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses trending anime from the homepage.
|
||||||
|
* Combines results from both the updated section and recent updates section.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Array<Object>>} Array of trending anime objects
|
||||||
|
* @returns {string} return.slug - URL slug for the anime
|
||||||
|
* @returns {string} return.poster - Poster image URL
|
||||||
|
* @returns {string} return.title - English title
|
||||||
|
* @returns {string} return.japaneseTitle - Japanese title
|
||||||
|
* @returns {number} return.sub - Subbed episode count
|
||||||
|
* @returns {number} return.dub - Dubbed episode count
|
||||||
|
* @returns {number} return.total - Total episode count
|
||||||
|
* @returns {string} return.type - Anime type (TV, Movie, etc.)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const trending = await extractTrending();
|
||||||
|
* console.log(trending[0].title);
|
||||||
|
*/
|
||||||
const extractTrending = async () => {
|
const extractTrending = async () => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: Trending data is split across multiple sections on the homepage
|
||||||
const { data } = await axios.get(URLS.home, { headers });
|
const { data } = await axios.get(URLS.home, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
const trending = [];
|
const trending = [];
|
||||||
|
|
||||||
|
// NOTE: This selector targets both the updated section and recent updates
|
||||||
$(".section-updated .item, #recent-update .item").each((i, el) => {
|
$(".section-updated .item, #recent-update .item").each((i, el) => {
|
||||||
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
@@ -19,6 +62,7 @@ const extractTrending = async () => {
|
|||||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||||
const type = $(el).find(".meta .inner .right").text().trim() || "";
|
const type = $(el).find(".meta .inner .right").text().trim() || "";
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
trending.push({
|
trending.push({
|
||||||
slug,
|
slug,
|
||||||
@@ -40,3 +84,5 @@ const extractTrending = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractTrending };
|
export { extractTrending };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: trending.extractor.js
|
||||||
|
|||||||
@@ -1,10 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — trendingSidebar.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts trending sidebar data including top anime rankings
|
||||||
|
* and latest episodes. Provides comprehensive sidebar content.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractTrendingSidebar
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// TRENDING SIDEBAR EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract trending sidebar with top rankings and latest episodes ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses the trending sidebar from the homepage.
|
||||||
|
* Returns daily, weekly, and monthly top anime plus latest episodes.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>} Object containing day, week, month, and latestEpisodes
|
||||||
|
* @returns {Array<Object>} return.day - Daily top anime
|
||||||
|
* @returns {Array<Object>} return.week - Weekly top anime
|
||||||
|
* @returns {Array<Object>} return.month - Monthly top anime
|
||||||
|
* @returns {Array<Object>} return.latestEpisodes - Recently updated episodes
|
||||||
|
* @returns {string} return.day[].slug - URL slug for the anime
|
||||||
|
* @returns {number} return.day[].rank - Ranking position
|
||||||
|
* @returns {string} return.day[].name - Anime name
|
||||||
|
* @returns {string} return.day[].poster - Poster image URL
|
||||||
|
* @returns {number} return.day[].sub - Subbed episode count
|
||||||
|
* @returns {number} return.day[].dub - Dubbed episode count
|
||||||
|
* @returns {string} return.day[].type - Anime type
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const sidebar = await extractTrendingSidebar();
|
||||||
|
* console.log(sidebar.day[0].name); // Top anime today
|
||||||
|
* console.log(sidebar.latestEpisodes[0].title); // Latest episode
|
||||||
|
*/
|
||||||
const extractTrendingSidebar = async () => {
|
const extractTrendingSidebar = async () => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: The sidebar content is embedded in the homepage HTML
|
||||||
const { data } = await axios.get(URLS.home, { headers });
|
const { data } = await axios.get(URLS.home, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
@@ -12,6 +57,7 @@ const extractTrendingSidebar = async () => {
|
|||||||
const week = [];
|
const week = [];
|
||||||
const month = [];
|
const month = [];
|
||||||
|
|
||||||
|
// NOTE: Daily rankings use the 'day' tab content
|
||||||
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
||||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
@@ -21,11 +67,13 @@ const extractTrendingSidebar = async () => {
|
|||||||
const type = $(el).find(".type").text().trim() || "";
|
const type = $(el).find(".type").text().trim() || "";
|
||||||
const rank = i + 1;
|
const rank = i + 1;
|
||||||
|
|
||||||
|
// NOTE: Only include items with valid slugs
|
||||||
if (slug) {
|
if (slug) {
|
||||||
day.push({ slug, rank, name, poster, sub, dub, type });
|
day.push({ slug, rank, name, poster, sub, dub, type });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Weekly rankings use the 'week' tab content
|
||||||
$("#top-anime .tab-content[data-name='week'] a.item").each((i, el) => {
|
$("#top-anime .tab-content[data-name='week'] a.item").each((i, el) => {
|
||||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
@@ -40,6 +88,7 @@ const extractTrendingSidebar = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Monthly rankings use the 'month' tab content
|
||||||
$("#top-anime .tab-content[data-name='month'] a.item").each((i, el) => {
|
$("#top-anime .tab-content[data-name='month'] a.item").each((i, el) => {
|
||||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find(".poster img").attr("src") || "";
|
const poster = $(el).find(".poster img").attr("src") || "";
|
||||||
@@ -54,6 +103,7 @@ const extractTrendingSidebar = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Latest episodes section provides recently updated content
|
||||||
const latestEpisodes = [];
|
const latestEpisodes = [];
|
||||||
$("#recent-update .item, .section-updated .item").each((i, el) => {
|
$("#recent-update .item, .section-updated .item").each((i, el) => {
|
||||||
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
||||||
@@ -77,3 +127,5 @@ const extractTrendingSidebar = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractTrendingSidebar };
|
export { extractTrendingSidebar };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: trendingSidebar.extractor.js
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — watchOrder.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts watch order information for a specific anime via API.
|
||||||
|
* Returns recommended viewing order from the internal API endpoint.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractWatchOrder
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { BASE_URL } from "../configs/dataUrl.js";
|
import { BASE_URL } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// WATCH ORDER API EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract watch order data from internal API ----
|
||||||
|
/**
|
||||||
|
* Fetches watch order information for a specific anime from the internal API.
|
||||||
|
* Returns an array of watch order objects or empty array on error.
|
||||||
|
*
|
||||||
|
* @param {string} animeId - The anime ID to fetch watch order for
|
||||||
|
* @returns {Promise<Array<Object>>} Array of watch order objects or empty array
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const watchOrder = await extractWatchOrder("12345");
|
||||||
|
* console.log(watchOrder[0].title); // First anime in watch order
|
||||||
|
*/
|
||||||
const extractWatchOrder = async (animeId) => {
|
const extractWatchOrder = async (animeId) => {
|
||||||
try {
|
try {
|
||||||
|
// NOTE: This is an internal API endpoint requiring X-Requested-With header
|
||||||
const url = `${BASE_URL}/api/watch-order/${animeId}`;
|
const url = `${BASE_URL}/api/watch-order/${animeId}`;
|
||||||
const { data } = await axios.get(url, {
|
const { data } = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -14,8 +48,11 @@ const extractWatchOrder = async (animeId) => {
|
|||||||
|
|
||||||
return data || [];
|
return data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// NOTE: Return empty array on error to maintain consistent API
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { extractWatchOrder };
|
export { extractWatchOrder };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: watchOrder.extractor.js
|
||||||
|
|||||||
@@ -1,44 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — watchPage.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Extracts the full watch/episode page data from anikototv.to including
|
||||||
|
* anime metadata, server list, trending sidebar, and recommended anime.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractWatchPage
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS, BASE_URL } from "../configs/dataUrl.js";
|
import { URLS, BASE_URL } from "../configs/dataUrl.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// WATCH PAGE EXTRACTOR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract complete watch page data for a specific episode ----
|
||||||
|
/**
|
||||||
|
* Fetches and parses the episode watch page from anikototv.to. Extracts
|
||||||
|
* anime metadata, episode details, available streaming servers, trending
|
||||||
|
* sidebar, and recommended anime.
|
||||||
|
*
|
||||||
|
* @param {string} slug - The anime slug (e.g. "one-piece")
|
||||||
|
* @param {string|number} ep - The episode number to watch
|
||||||
|
* @returns {Promise<Object>} Full watch page data object
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const watchData = await extractWatchPage("one-piece", 100);
|
||||||
|
* console.log(watchData.servers.length); // number of available servers
|
||||||
|
* console.log(watchData.title); // anime title
|
||||||
|
*/
|
||||||
const extractWatchPage = async (slug, ep) => {
|
const extractWatchPage = async (slug, ep) => {
|
||||||
try {
|
try {
|
||||||
const url = URLS.episode(slug, ep);
|
const url = URLS.episode(slug, ep);
|
||||||
const { data } = await axios.get(url, { headers });
|
const { data } = await axios.get(url, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime identification and title ----
|
||||||
|
// NOTE: data-id on #watch-main is the internal anime ID for AJAX calls
|
||||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||||
const animeUrl = $("#watch-main").attr("data-url") || "";
|
const animeUrl = $("#watch-main").attr("data-url") || "";
|
||||||
const title = $("h1[itemprop='name'].title.d-title").text().trim() || slug;
|
const title = $("h1[itemprop='name'].title.d-title").text().trim() || slug;
|
||||||
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
||||||
const episodeNumber = parseInt(ep) || 0;
|
const episodeNumber = parseInt(ep) || 0;
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract synopsis and rating ----
|
||||||
const synopsis = $(".synopsis .content").text().trim() || "";
|
const synopsis = $(".synopsis .content").text().trim() || "";
|
||||||
const rating = $("#w-rating .score .value").text().trim() || "";
|
const rating = $("#w-rating .score .value").text().trim() || "";
|
||||||
const poster = $("img[itemprop='image']").attr("src") || "";
|
const poster = $("img[itemprop='image']").attr("src") || "";
|
||||||
|
// NOTE: Background image is extracted from CSS background-image property on #player
|
||||||
const backgroundImage = $("#player").css("background-image")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
const backgroundImage = $("#player").css("background-image")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract anime metadata (type, status, MAL score, duration, episodes) ----
|
||||||
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
||||||
const status = $(".bmeta .meta:first-child > div:nth-child(4) span a").text().trim() || "";
|
const status = $(".bmeta .meta:first-child > div:nth-child(4) span a").text().trim() || "";
|
||||||
const malScore = $(".bmeta .meta:nth-child(2) > div:nth-child(1) span").text().trim() || "";
|
const malScore = $(".bmeta .meta:nth-child(2) > div:nth-child(1) span").text().trim() || "";
|
||||||
const duration = $(".bmeta .meta:nth-child(2) > div:nth-child(2) span").text().trim() || "";
|
const duration = $(".bmeta .meta:nth-child(2) > div:nth-child(2) span").text().trim() || "";
|
||||||
const episodes = $(".bmeta .meta:nth-child(2) > div:nth-child(3) span").text().trim() || "";
|
const episodes = $(".bmeta .meta:nth-child(2) > div:nth-child(3) span").text().trim() || "";
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract genres ----
|
||||||
|
// NOTE: Genre links contain '/genre/' in href to distinguish from other links
|
||||||
const genres = [];
|
const genres = [];
|
||||||
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
||||||
genres.push($(el).text().trim());
|
genres.push($(el).text().trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract studios ----
|
||||||
|
// NOTE: Uses itemprop='director' links with itemprop='name' spans inside
|
||||||
const studios = [];
|
const studios = [];
|
||||||
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
||||||
studios.push($(el).text().trim());
|
studios.push($(el).text().trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract next episode release info ----
|
||||||
|
// NOTE: .next-episode shows the countdown text, .count-down has data-target timestamp
|
||||||
const nextEpisodeDate = $(".next-episode, .alert.next-episode").text().trim() || "";
|
const nextEpisodeDate = $(".next-episode, .alert.next-episode").text().trim() || "";
|
||||||
const nextEpisodeTimestamp = parseInt($(".count-down").attr("data-target")) || 0;
|
const nextEpisodeTimestamp = parseInt($(".count-down").attr("data-target")) || 0;
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract available streaming servers ----
|
||||||
|
// NOTE: Each server li has data-link-id (unique), data-ep-id, data-cmid, data-sv-id
|
||||||
|
// NOTE: Server type (sub/dub) is derived from closest .type container's data-type attribute
|
||||||
const servers = [];
|
const servers = [];
|
||||||
$("#w-servers .servers .type li, #w-servers li[data-link-id]").each((i, el) => {
|
$("#w-servers .servers .type li, #w-servers li[data-link-id]").each((i, el) => {
|
||||||
const linkId = $(el).attr("data-link-id") || "";
|
const linkId = $(el).attr("data-link-id") || "";
|
||||||
@@ -60,11 +110,14 @@ const extractWatchPage = async (slug, ep) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract trending/watch-order sidebar items ----
|
||||||
|
// NOTE: Grabs from both the first .w-side-section and #watch-order containers
|
||||||
const trending = [];
|
const trending = [];
|
||||||
$(".w-side-section:first .scaff.side.items a.item, #watch-order a.item").each((i, el) => {
|
$(".w-side-section:first .scaff.side.items a.item, #watch-order a.item").each((i, el) => {
|
||||||
const trendSlug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const trendSlug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const trendPoster = $(el).find(".poster img").attr("src") || "";
|
const trendPoster = $(el).find(".poster img").attr("src") || "";
|
||||||
const trendTitle = $(el).find(".name").text().trim() || "";
|
const trendTitle = $(el).find(".name").text().trim() || "";
|
||||||
|
// NOTE: .dot:last-child typically holds the type badge text
|
||||||
const trendType = $(el).find(".dot:last-child").text().trim() || "";
|
const trendType = $(el).find(".dot:last-child").text().trim() || "";
|
||||||
const trendScore = $(el).find(".score").text().trim() || "";
|
const trendScore = $(el).find(".score").text().trim() || "";
|
||||||
|
|
||||||
@@ -79,6 +132,8 @@ const extractWatchPage = async (slug, ep) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract recommended anime sidebar items ----
|
||||||
|
// NOTE: Targets the section with a title containing "Recommended", or the last sidebar
|
||||||
const recommended = [];
|
const recommended = [];
|
||||||
$(".w-side-section:has(.title:contains('Recommended')) a.item, aside.sidebar:last .w-side-section a.item").each((i, el) => {
|
$(".w-side-section:has(.title:contains('Recommended')) a.item, aside.sidebar:last .w-side-section a.item").each((i, el) => {
|
||||||
const recSlug = $(el).attr("href")?.split("/watch/").pop() || "";
|
const recSlug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
@@ -126,3 +181,5 @@ const extractWatchPage = async (slug, ep) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractWatchPage };
|
export { extractWatchPage };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: watchPage.extractor.js
|
||||||
|
|||||||
@@ -1,6 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — cache.helper.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* In-memory caching utility for storing and retrieving API responses.
|
||||||
|
* Implements TTL-based cache invalidation to prevent stale data
|
||||||
|
* while reducing unnecessary requests to the source site.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getCache, setCache, clearCache
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CACHE STORAGE & CONFIGURATION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: In-memory Map-based cache storage ----
|
||||||
|
/**
|
||||||
|
* Internal cache storage using JavaScript Map object.
|
||||||
|
* Stores data with timestamps for TTL-based expiration.
|
||||||
|
*
|
||||||
|
* @type {Map<string, {data: any, timestamp: number}>}
|
||||||
|
*/
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
|
||||||
|
// ---- FEATURE: Cache time-to-live configuration (5 minutes) ----
|
||||||
|
/**
|
||||||
|
* Cache TTL (Time-To-Live) in milliseconds.
|
||||||
|
* Default: 5 minutes (300,000 ms).
|
||||||
|
* After this duration, cached items are considered stale
|
||||||
|
* and will be removed on next access.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @default 300000
|
||||||
|
*/
|
||||||
const CACHE_TTL = 5 * 60 * 1000;
|
const CACHE_TTL = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CACHE OPERATIONS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Retrieve cached data by key with TTL validation ----
|
||||||
|
/**
|
||||||
|
* Retrieves data from cache if it exists and hasn't expired.
|
||||||
|
* Automatically removes stale entries on access.
|
||||||
|
*
|
||||||
|
* @param {string} key - The cache key to lookup
|
||||||
|
* @returns {any|null} Cached data if valid, null if expired or missing
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Get cached anime list
|
||||||
|
* const data = getCache("anime-list-action");
|
||||||
|
* if (data) {
|
||||||
|
* // Use cached data
|
||||||
|
* }
|
||||||
|
*/
|
||||||
const getCache = (key) => {
|
const getCache = (key) => {
|
||||||
const item = cache.get(key);
|
const item = cache.get(key);
|
||||||
if (item && Date.now() - item.timestamp < CACHE_TTL) {
|
if (item && Date.now() - item.timestamp < CACHE_TTL) {
|
||||||
@@ -10,12 +69,39 @@ const getCache = (key) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- FEATURE: Store data in cache with current timestamp ----
|
||||||
|
/**
|
||||||
|
* Stores data in cache with current timestamp for TTL tracking.
|
||||||
|
* Overwrites any existing data for the same key.
|
||||||
|
*
|
||||||
|
* @param {string} key - The cache key to store under
|
||||||
|
* @param {any} data - The data to cache (any serializable type)
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Cache anime search results
|
||||||
|
* setCache("search-one-piece", animeResults);
|
||||||
|
*/
|
||||||
const setCache = (key, data) => {
|
const setCache = (key, data) => {
|
||||||
cache.set(key, { data, timestamp: Date.now() });
|
cache.set(key, { data, timestamp: Date.now() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- FEATURE: Clear all cached entries from memory ----
|
||||||
|
/**
|
||||||
|
* Clears all entries from the cache. Use when:
|
||||||
|
* - Memory cleanup is needed
|
||||||
|
* - Force-refreshing all data
|
||||||
|
* - Application restart/reset
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Force refresh all cached data
|
||||||
|
* clearCache();
|
||||||
|
*/
|
||||||
const clearCache = () => {
|
const clearCache = () => {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getCache, setCache, clearCache };
|
export { getCache, setCache, clearCache };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: cache.helper.js
|
||||||
@@ -1,3 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — countPages.helper.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Utility function to extract total page count from paginated
|
||||||
|
* HTML content. Parses pagination links from Cheerio DOM to
|
||||||
|
* determine the maximum page number available.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* countPages
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// PAGINATION EXTRACTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract maximum page number from pagination elements ----
|
||||||
|
/**
|
||||||
|
* Parses HTML pagination elements to extract the total number of pages.
|
||||||
|
* Looks for numeric page links in .pagination li a elements and
|
||||||
|
* returns the highest page number found.
|
||||||
|
*
|
||||||
|
* @param {CheerioAPI} $ - Cheerio instance with loaded HTML content
|
||||||
|
* @returns {number} Maximum page number (defaults to 1 if no pagination found)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Count pages from search results
|
||||||
|
* const $ = await extractPages(searchUrl);
|
||||||
|
* const totalPages = countPages($);
|
||||||
|
* // totalPages: 5 (if pages 1-5 exist)
|
||||||
|
*
|
||||||
|
* @note
|
||||||
|
* This function assumes standard pagination HTML structure with
|
||||||
|
* numeric page links inside .pagination > li > a elements.
|
||||||
|
*/
|
||||||
const countPages = ($) => {
|
const countPages = ($) => {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
$(".pagination li a").each((i, el) => {
|
$(".pagination li a").each((i, el) => {
|
||||||
@@ -10,3 +51,4 @@ const countPages = ($) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { countPages };
|
export { countPages };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: countPages.helper.js
|
||||||
@@ -1,7 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — extractPages.helper.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* HTTP client utility for fetching and parsing HTML content from
|
||||||
|
* paginated URLs. Uses axios for HTTP requests and Cheerio for
|
||||||
|
* DOM parsing, with support for page-based pagination.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* extractPages
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// DEPENDENCIES
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Cheerio HTML parser import ----
|
||||||
|
/**
|
||||||
|
* Cheerio library for server-side DOM manipulation.
|
||||||
|
* Used to parse and query HTML content from fetched pages.
|
||||||
|
*
|
||||||
|
* @type {module:cheerio}
|
||||||
|
*/
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
// ---- FEATURE: Axios HTTP client import ----
|
||||||
|
/**
|
||||||
|
* Axios HTTP client for making requests to the source site.
|
||||||
|
* Handles redirects, timeouts, and response parsing.
|
||||||
|
*
|
||||||
|
* @type {module:axios}
|
||||||
|
*/
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
|
// ---- FEATURE: Browser-mimicking headers import ----
|
||||||
|
/**
|
||||||
|
* Pre-configured HTTP headers to mimic browser requests.
|
||||||
|
* Imported from header.config.js for consistent usage.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// PAGE EXTRACTION FUNCTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Fetch and parse paginated URL content ----
|
||||||
|
/**
|
||||||
|
* Fetches HTML content from a URL with optional pagination support.
|
||||||
|
* Appends page query parameter for pages > 1 and returns parsed
|
||||||
|
* Cheerio instance for DOM querying.
|
||||||
|
*
|
||||||
|
* @param {string} url - Base URL to fetch (without page parameter)
|
||||||
|
* @param {number} [page=1] - Page number to fetch (1-indexed)
|
||||||
|
* @returns {Promise<CheerioAPI>} Parsed Cheerio instance of the page
|
||||||
|
* @throws {Error} If HTTP request fails or returns non-2xx status
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Fetch first page of search results
|
||||||
|
* const $ = await extractPages("https://anikototv.to/filter?keyword=one-piece");
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Fetch second page of genre listing
|
||||||
|
* const $ = await extractPages(genreUrl, 2);
|
||||||
|
*
|
||||||
|
* @note
|
||||||
|
* WARNING: The site may block requests without proper headers.
|
||||||
|
* Always use the imported headers configuration.
|
||||||
|
*
|
||||||
|
* @note
|
||||||
|
* TIP: Consider implementing retry logic for failed requests
|
||||||
|
* in production environments.
|
||||||
|
*/
|
||||||
const extractPages = async (url, page = 1) => {
|
const extractPages = async (url, page = 1) => {
|
||||||
try {
|
try {
|
||||||
const finalUrl = page > 1 ? `${url}?page=${page}` : url;
|
const finalUrl = page > 1 ? `${url}?page=${page}` : url;
|
||||||
@@ -14,3 +90,4 @@ const extractPages = async (url, page = 1) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { extractPages };
|
export { extractPages };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: extractPages.helper.js
|
||||||
@@ -1,3 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — formatTitle.helper.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* String utility function for normalizing anime titles into
|
||||||
|
* URL-safe slug format. Converts titles to lowercase, replaces
|
||||||
|
* special characters with hyphens, and trims leading/trailing dashes.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* formatTitle
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// TITLE FORMATTING UTILITY
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Convert title string to URL-safe slug format ----
|
||||||
|
/**
|
||||||
|
* Formats an anime title into a URL-safe slug format.
|
||||||
|
* - Converts to lowercase
|
||||||
|
* - Replaces non-alphanumeric characters with hyphens
|
||||||
|
* - Removes leading and trailing hyphens
|
||||||
|
*
|
||||||
|
* @param {string} title - The anime title to format
|
||||||
|
* @returns {string} URL-safe slug version of the title
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic title formatting
|
||||||
|
* formatTitle("One Piece") // "one-piece"
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Title with special characters
|
||||||
|
* formatTitle(" Attack on Titan: The Final Season! ")
|
||||||
|
* // "attack-on-titan-the-final-season"
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Edge cases
|
||||||
|
* formatTitle("") // ""
|
||||||
|
* formatTitle(null) // ""
|
||||||
|
* formatTitle(" --- ") // ""
|
||||||
|
*
|
||||||
|
* @note
|
||||||
|
* This function preserves alphanumeric characters and replaces
|
||||||
|
* all other characters (spaces, punctuation, symbols) with hyphens.
|
||||||
|
* Multiple consecutive special characters become a single hyphen.
|
||||||
|
*/
|
||||||
const formatTitle = (title) => {
|
const formatTitle = (title) => {
|
||||||
if (!title) return "";
|
if (!title) return "";
|
||||||
return title
|
return title
|
||||||
@@ -7,3 +59,4 @@ const formatTitle = (title) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { formatTitle };
|
export { formatTitle };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: formatTitle.helper.js
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — apiRoutes.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Central API router that maps all Express GET endpoints to their
|
||||||
|
* corresponding controller handlers. Each route wraps its controller
|
||||||
|
* in try/catch for consistent error handling.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* createApiRoutes - Function that registers all API routes on the Express app
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { getHomeInfo } from "../controllers/homeInfo.controller.js";
|
import { getHomeInfo } from "../controllers/homeInfo.controller.js";
|
||||||
import { getAnimeInfo } from "../controllers/animeInfo.controller.js";
|
import { getAnimeInfo } from "../controllers/animeInfo.controller.js";
|
||||||
import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
|
import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
|
||||||
@@ -21,7 +39,29 @@ import { getSeasons } from "../controllers/seasons.controller.js";
|
|||||||
import { getWatchOrder } from "../controllers/watchOrder.controller.js";
|
import { getWatchOrder } from "../controllers/watchOrder.controller.js";
|
||||||
import { categoryRoutes } from "./category.route.js";
|
import { categoryRoutes } from "./category.route.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ROUTE REGISTRATION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Main API route registration function ----
|
||||||
|
/**
|
||||||
|
* Registers all API routes on the Express app instance.
|
||||||
|
* Each route delegates to a controller and uses the provided
|
||||||
|
* jsonResponse/jsonError helpers for consistent output.
|
||||||
|
*
|
||||||
|
* @param {object} app - Express application instance
|
||||||
|
* @param {Function} jsonResponse - Helper to wrap data in success JSON
|
||||||
|
* @param {Function} jsonError - Helper to return error JSON
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* createApiRoutes(app, jsonResponse, jsonError);
|
||||||
|
*/
|
||||||
const createApiRoutes = (app, jsonResponse, jsonError) => {
|
const createApiRoutes = (app, jsonResponse, jsonError) => {
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// HOME
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Homepage data endpoint ----
|
||||||
app.get("/api/", async (req, res, next) => {
|
app.get("/api/", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getHomeInfo(req, res, next);
|
await getHomeInfo(req, res, next);
|
||||||
@@ -30,6 +70,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANIME INFO
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime detail information by slug ----
|
||||||
app.get("/api/info", async (req, res, next) => {
|
app.get("/api/info", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getAnimeInfo(req, res, next);
|
await getAnimeInfo(req, res, next);
|
||||||
@@ -38,6 +83,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// WATCH PAGE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Watch page data for specific episode ----
|
||||||
app.get("/api/watch", async (req, res, next) => {
|
app.get("/api/watch", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getWatchPage(req, res, next);
|
await getWatchPage(req, res, next);
|
||||||
@@ -46,6 +96,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SEARCH
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Full search results with pagination ----
|
||||||
app.get("/api/search", async (req, res, next) => {
|
app.get("/api/search", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getSearchResults(req, res, next);
|
await getSearchResults(req, res, next);
|
||||||
@@ -54,6 +109,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Search autocomplete suggestions ----
|
||||||
app.get("/api/search/suggest", async (req, res, next) => {
|
app.get("/api/search/suggest", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getSearchSuggestions(req, res, next);
|
await getSearchSuggestions(req, res, next);
|
||||||
@@ -62,6 +118,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// EPISODES
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Episode list for anime by ID ----
|
||||||
app.get("/api/episodes/:id", async (req, res, next) => {
|
app.get("/api/episodes/:id", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getEpisodeList(req, res, next);
|
await getEpisodeList(req, res, next);
|
||||||
@@ -70,6 +131,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: AJAX-loaded episode list (additional data) ----
|
||||||
app.get("/api/episodes-ajax/:id", async (req, res, next) => {
|
app.get("/api/episodes-ajax/:id", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getEpisodeListAjax(req, res, next);
|
await getEpisodeListAjax(req, res, next);
|
||||||
@@ -78,6 +140,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STREAMING
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Stream URL and metadata for episode ----
|
||||||
app.get("/api/stream", async (req, res, next) => {
|
app.get("/api/stream", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getStreamInfo(req, res, next);
|
await getStreamInfo(req, res, next);
|
||||||
@@ -86,6 +153,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Available streaming servers for episode ----
|
||||||
app.get("/api/servers", async (req, res, next) => {
|
app.get("/api/servers", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getServerList(req, res, next);
|
await getServerList(req, res, next);
|
||||||
@@ -94,6 +162,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Mapped servers across multiple episodes ----
|
||||||
app.get("/api/mapper-servers", async (req, res, next) => {
|
app.get("/api/mapper-servers", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getMapperServers(req, res, next);
|
await getMapperServers(req, res, next);
|
||||||
@@ -102,6 +171,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SCHEDULE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime airing schedule by date ----
|
||||||
app.get("/api/schedule", async (req, res, next) => {
|
app.get("/api/schedule", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getSchedule(req, res, next);
|
await getSchedule(req, res, next);
|
||||||
@@ -110,6 +184,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// DISCOVERY / HOMEPAGE SECTIONS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Spotlight/featured anime carousel ----
|
||||||
app.get("/api/spotlight", async (req, res, next) => {
|
app.get("/api/spotlight", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getSpotlight(req, res, next);
|
await getSpotlight(req, res, next);
|
||||||
@@ -118,6 +197,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Currently trending anime ----
|
||||||
app.get("/api/trending", async (req, res, next) => {
|
app.get("/api/trending", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getTrending(req, res, next);
|
await getTrending(req, res, next);
|
||||||
@@ -126,6 +206,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Top 10 ranked anime ----
|
||||||
app.get("/api/top-ten", async (req, res, next) => {
|
app.get("/api/top-ten", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getTopTen(req, res, next);
|
await getTopTen(req, res, next);
|
||||||
@@ -134,6 +215,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Auto-complete suggestions by keyword ----
|
||||||
app.get("/api/suggestions", async (req, res, next) => {
|
app.get("/api/suggestions", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getSuggestions(req, res, next);
|
await getSuggestions(req, res, next);
|
||||||
@@ -142,6 +224,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Random anime recommendation ----
|
||||||
app.get("/api/random", async (req, res, next) => {
|
app.get("/api/random", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getRandom(req, res, next);
|
await getRandom(req, res, next);
|
||||||
@@ -150,6 +233,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Most popular anime list ----
|
||||||
app.get("/api/most-popular", async (req, res, next) => {
|
app.get("/api/most-popular", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getPopular(req, res, next);
|
await getPopular(req, res, next);
|
||||||
@@ -158,6 +242,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Advanced filter/search by parameters ----
|
||||||
app.get("/api/filter", async (req, res, next) => {
|
app.get("/api/filter", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getFilter(req, res, next);
|
await getFilter(req, res, next);
|
||||||
@@ -166,6 +251,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// RELEASE LISTS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Recently released episodes ----
|
||||||
app.get("/api/new-release", async (req, res, next) => {
|
app.get("/api/new-release", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getNewRelease(req, res, next);
|
await getNewRelease(req, res, next);
|
||||||
@@ -174,6 +264,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Newly added anime series ----
|
||||||
app.get("/api/newly-added", async (req, res, next) => {
|
app.get("/api/newly-added", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getNewlyAdded(req, res, next);
|
await getNewlyAdded(req, res, next);
|
||||||
@@ -182,6 +273,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SIDEBAR
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Trending sidebar widget data ----
|
||||||
app.get("/api/trending-sidebar", async (req, res, next) => {
|
app.get("/api/trending-sidebar", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getTrendingSidebar(req, res, next);
|
await getTrendingSidebar(req, res, next);
|
||||||
@@ -190,6 +286,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// SEASONS & WATCH ORDER
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Season information for anime ----
|
||||||
app.get("/api/seasons/:id", async (req, res, next) => {
|
app.get("/api/seasons/:id", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getSeasons(req, res, next);
|
await getSeasons(req, res, next);
|
||||||
@@ -198,6 +299,7 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Recommended watch order for anime ----
|
||||||
app.get("/api/watch-order/:id", async (req, res, next) => {
|
app.get("/api/watch-order/:id", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getWatchOrder(req, res, next);
|
await getWatchOrder(req, res, next);
|
||||||
@@ -206,6 +308,11 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// A-Z LIST
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Alphabetical anime listing ----
|
||||||
app.get("/api/az-list/:letter", async (req, res, next) => {
|
app.get("/api/az-list/:letter", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getAzList(req, res, next);
|
await getAzList(req, res, next);
|
||||||
@@ -214,7 +321,14 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CATEGORY ROUTES
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// NOTE: Category routes (genre, type, status) are registered separately
|
||||||
categoryRoutes(app, jsonResponse, jsonError);
|
categoryRoutes(app, jsonResponse, jsonError);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { createApiRoutes };
|
export { createApiRoutes };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: apiRoutes.js
|
||||||
@@ -1,6 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKatoAPI — category.route.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Defines category-based API routes for genre, type, and status
|
||||||
|
* filtering. Each route transforms its params into a unified
|
||||||
|
* category format before delegating to the category controller.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* categoryRoutes - Function that registers category routes on the Express app
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
import { getCategory } from "../controllers/category.controller.js";
|
import { getCategory } from "../controllers/category.controller.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CATEGORY ROUTES
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Category route registration function ----
|
||||||
|
/**
|
||||||
|
* Registers genre, type, and status category routes on the Express app.
|
||||||
|
* Each route maps to the unified getCategory controller with the
|
||||||
|
* appropriate category path prefix.
|
||||||
|
*
|
||||||
|
* @param {object} app - Express application instance
|
||||||
|
* @param {Function} jsonResponse - Helper to wrap data in success JSON
|
||||||
|
* @param {Function} jsonError - Helper to return error JSON
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* categoryRoutes(app, jsonResponse, jsonError);
|
||||||
|
*/
|
||||||
const categoryRoutes = (app, jsonResponse, jsonError) => {
|
const categoryRoutes = (app, jsonResponse, jsonError) => {
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// GENRE ROUTE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime filtered by genre ----
|
||||||
|
// NOTE: The genre param is appended to 'genre/' to form the full category path
|
||||||
app.get("/api/genre/:genre", async (req, res, next) => {
|
app.get("/api/genre/:genre", async (req, res, next) => {
|
||||||
req.params.category = `genre/${req.params.genre}`;
|
req.params.category = `genre/${req.params.genre}`;
|
||||||
try {
|
try {
|
||||||
@@ -10,6 +51,12 @@ const categoryRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// TYPE ROUTE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime filtered by type (TV, Movie, OVA, etc.) ----
|
||||||
|
// NOTE: The type param is appended to 'type/' to form the full category path
|
||||||
app.get("/api/type/:type", async (req, res, next) => {
|
app.get("/api/type/:type", async (req, res, next) => {
|
||||||
req.params.category = `type/${req.params.type}`;
|
req.params.category = `type/${req.params.type}`;
|
||||||
try {
|
try {
|
||||||
@@ -19,6 +66,12 @@ const categoryRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STATUS ROUTE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Anime filtered by airing status ----
|
||||||
|
// NOTE: The status param is appended to 'status/' to form the full category path
|
||||||
app.get("/api/status/:status", async (req, res, next) => {
|
app.get("/api/status/:status", async (req, res, next) => {
|
||||||
req.params.category = `status/${req.params.status}`;
|
req.params.category = `status/${req.params.status}`;
|
||||||
try {
|
try {
|
||||||
@@ -30,3 +83,5 @@ const categoryRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { categoryRoutes };
|
export { categoryRoutes };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: category.route.js
|
||||||
Reference in New Issue
Block a user