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:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-08 16:14:01 +05:30
parent 87d6d4dbf2
commit 0034617c3b
55 changed files with 2965 additions and 108 deletions
+13
View File
@@ -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/),
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
### Fixed
+68
View File
@@ -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 express from "express";
import cors from "cors";
@@ -9,12 +27,21 @@ import { createApiRoutes } from "./src/routes/apiRoutes.js";
dotenv.config();
// ══════════════════════════════════════════════════════════════
// SERVER CONFIGURATION
// ══════════════════════════════════════════════════════════════
const app = express();
const PORT = process.env.PORT || 4444;
const __filename = fileURLToPath(import.meta.url);
const publicDir = path.join(dirname(__filename), "public");
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
// ══════════════════════════════════════════════════════════════
// CORS MIDDLEWARE
// ══════════════════════════════════════════════════════════════
// NOTE: CORS is configured to only allow GET requests from specified origins
app.use(
cors({
origin: allowedOrigins?.includes("*") ? "*" : allowedOrigins || [],
@@ -22,6 +49,7 @@ app.use(
})
);
// ---- FEATURE: Custom origin validation middleware ----
app.use((req, res, next) => {
const origin = req.headers.origin;
if (
@@ -39,16 +67,50 @@ app.use((req, res, next) => {
.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 }));
// ══════════════════════════════════════════════════════════════
// 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) =>
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) =>
res.status(status).json({ success: false, message });
// ══════════════════════════════════════════════════════════════
// API ROUTES
// ══════════════════════════════════════════════════════════════
createApiRoutes(app, jsonResponse, jsonError);
// ══════════════════════════════════════════════════════════════
// 404 HANDLER
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Catch-all 404 handler for undefined routes ----
app.use((req, res) => {
const filePath = path.join(publicDir, "404.html");
if (fs.existsSync(filePath)) {
@@ -58,6 +120,12 @@ app.use((req, res) => {
}
});
// ══════════════════════════════════════════════════════════════
// SERVER START
// ══════════════════════════════════════════════════════════════
app.listen(PORT, () => {
console.info(`AniKatoAPI listening at ${PORT}`);
});
// ══════════════════════════════════════════════════════════════ END: server.js
+94
View File
@@ -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";
// ---- 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 = [
"https://anikoto.cz",
"https://anikoto.me",
@@ -7,6 +50,56 @@ const ALT_DOMAINS = [
"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 = {
home: `${BASE_URL}/home`,
search: `${BASE_URL}/filter`,
@@ -24,3 +117,4 @@ const URLS = {
};
export { BASE_URL, ALT_DOMAINS, URLS };
// ══════════════════════════════════════════════════════════════ END: dataUrl.js
+41
View File
@@ -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 = {
"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",
@@ -8,3 +48,4 @@ const headers = {
};
export { headers };
// ══════════════════════════════════════════════════════════════ END: header.config.js
+134
View File
@@ -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 = {
"action": "1",
"adventure": "2",
@@ -44,6 +89,29 @@ const GENRE_IDS = {
"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 = {
"movie": "1",
"music": "2",
@@ -53,12 +121,52 @@ const TYPE_IDS = {
"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 = {
"currently-airing": "1",
"finished-airing": "2",
"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 = {
"g": "1",
"pg": "2",
@@ -68,6 +176,31 @@ const RATING_IDS = {
"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 = {
"default": "",
"latest-updated": "1",
@@ -80,3 +213,4 @@ const SORT_IDS = {
};
export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS };
// ══════════════════════════════════════════════════════════════ END: ids.config.js
+40 -2
View File
@@ -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 { 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) => {
try {
const { id } = req.query;
if (!id) {
return res.status(400).json({ success: false, message: "Anime slug is required" });
}
const cacheKey = `anime_${id}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractAnimeInfo(id);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -23,3 +59,5 @@ const getAnimeInfo = async (req, res, next) => {
};
export { getAnimeInfo };
// ══════════════════════════════════════════════════════════════ END: animeInfo.controller.js
+42 -2
View File
@@ -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 { 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) => {
try {
const { letter = "all" } = req.params;
const { page = 1 } = req.query;
const cacheKey = `azlist_${letter}_${page}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractAzList(letter, page);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -21,3 +59,5 @@ const getAzList = async (req, res, next) => {
};
export { getAzList };
// ══════════════════════════════════════════════════════════════ END: azList.controller.js
+43 -3
View File
@@ -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 { 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) => {
try {
const { category } = req.params;
const { page = 1 } = req.query;
if (!category) {
return res.status(400).json({ success: false, message: "Category is required" });
}
const cacheKey = `category_${category}_${page}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractCategory(category, page);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -25,3 +63,5 @@ const getCategory = async (req, res, next) => {
};
export { getCategory };
// ══════════════════════════════════════════════════════════════ END: category.controller.js
+41 -3
View File
@@ -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 { 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) => {
try {
const { id } = req.params;
if (!id) {
return res.status(400).json({ success: false, message: "Anime slug is required" });
}
const cacheKey = `episodes_${id}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractEpisodeList(id);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -24,3 +60,5 @@ const getEpisodeList = async (req, res, next) => {
};
export { getEpisodeList };
// ══════════════════════════════════════════════════════════════ END: episodeList.controller.js
+44 -3
View File
@@ -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 { 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) => {
try {
const { id } = req.params;
const { style, vrf } = req.query;
if (!id) {
return res.status(400).json({ success: false, message: "Anime ID is required" });
}
const cacheKey = `episodes_ajax_${id}_${style || ""}_${vrf || ""}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractEpisodeListAjax(id, style, vrf);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -25,3 +64,5 @@ const getEpisodeListAjax = async (req, res, next) => {
};
export { getEpisodeListAjax };
// ══════════════════════════════════════════════════════════════ END: episodeListAjax.controller.js
+45 -1
View File
@@ -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 { 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) => {
try {
const params = req.query;
@@ -9,7 +52,6 @@ const getFilter = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractFilter(params);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -19,3 +61,5 @@ const getFilter = async (req, res, next) => {
};
export { getFilter };
// ══════════════════════════════════════════════════════════════ END: filter.controller.js
+37 -1
View File
@@ -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 { 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) => {
try {
const cacheKey = "home";
@@ -8,7 +43,6 @@ const getHomeInfo = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractHomeInfo();
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -18,3 +52,5 @@ const getHomeInfo = async (req, res, next) => {
};
export { getHomeInfo };
// ══════════════════════════════════════════════════════════════ END: homeInfo.controller.js
+59 -2
View File
@@ -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 { 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) => {
try {
const { page = 1 } = req.query;
@@ -9,7 +47,6 @@ const getNewRelease = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractNewRelease(page);
setCache(cacheKey, 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) => {
try {
const { page = 1 } = req.query;
@@ -26,7 +82,6 @@ const getNewlyAdded = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractNewlyAdded(page);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -36,3 +91,5 @@ const getNewlyAdded = async (req, res, next) => {
};
export { getNewRelease, getNewlyAdded };
// ══════════════════════════════════════════════════════════════ END: newRelease.controller.js
+39 -1
View File
@@ -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 { 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) => {
try {
const { page = 1 } = req.query;
@@ -9,7 +46,6 @@ const getPopular = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractPopular(page);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -19,3 +55,5 @@ const getPopular = async (req, res, next) => {
};
export { getPopular };
// ══════════════════════════════════════════════════════════════ END: popular.controller.js
+37 -1
View File
@@ -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 { 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) => {
try {
const cacheKey = "random";
@@ -8,7 +43,6 @@ const getRandom = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractRandom();
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -18,3 +52,5 @@ const getRandom = async (req, res, next) => {
};
export { getRandom };
// ══════════════════════════════════════════════════════════════ END: random.controller.js
+42 -3
View File
@@ -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 { 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) => {
try {
const { date } = req.query;
if (!date) {
return res.status(400).json({ success: false, message: "Date is required (YYYY-MM-DD)" });
}
const cacheKey = `schedule_${date}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractSchedule(date);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -24,3 +61,5 @@ const getSchedule = async (req, res, next) => {
};
export { getSchedule };
// ══════════════════════════════════════════════════════════════ END: schedule.controller.js
+61 -4
View File
@@ -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 { 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) => {
try {
const { keyword, page = 1 } = req.query;
if (!keyword) {
return res.status(400).json({ success: false, message: "Keyword is required" });
}
const cacheKey = `search_${keyword}_${page}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractSearchResults(keyword, page);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -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) => {
try {
const { keyword } = req.query;
if (!keyword) {
return res.status(400).json({ success: false, message: "Keyword is required" });
}
const cacheKey = `suggest_${keyword}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractSearchSuggestions(keyword);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -44,3 +99,5 @@ const getSearchSuggestions = async (req, res, next) => {
};
export { getSearchResults, getSearchSuggestions };
// ══════════════════════════════════════════════════════════════ END: search.controller.js
+41 -3
View File
@@ -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 { 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) => {
try {
const { id } = req.params;
if (!id) {
return res.status(400).json({ success: false, message: "Anime ID is required" });
}
const cacheKey = `seasons_${id}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractSeasons(id);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -24,3 +60,5 @@ const getSeasons = async (req, res, next) => {
};
export { getSeasons };
// ══════════════════════════════════════════════════════════════ END: seasons.controller.js
+38 -1
View File
@@ -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 { 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) => {
try {
const cacheKey = "spotlight";
@@ -8,7 +44,6 @@ const getSpotlight = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractSpotlight();
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -18,3 +53,5 @@ const getSpotlight = async (req, res, next) => {
};
export { getSpotlight };
// ══════════════════════════════════════════════════════════════ END: spotlight.controller.js
+43 -3
View File
@@ -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 { 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) => {
try {
const { status } = req.params;
const { page = 1 } = req.query;
if (!status) {
return res.status(400).json({ success: false, message: "Status is required (currently-airing, finished-airing, not-yet-aired)" });
}
const cacheKey = `status_${status}_${page}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractStatus(status, page);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -25,3 +63,5 @@ const getStatus = async (req, res, next) => {
};
export { getStatus };
// ══════════════════════════════════════════════════════════════ END: status.controller.js
+88 -9
View File
@@ -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 { 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) => {
try {
const { id } = req.query;
if (!id) {
return res.status(400).json({ success: false, message: "Link ID is required" });
}
const cacheKey = `stream_${id}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractStreamInfo(id);
setCache(cacheKey, 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) => {
try {
const { ids } = req.query;
if (!ids) {
return res.status(400).json({ success: false, message: "Episode IDs are required" });
}
const cacheKey = `servers_${ids}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractServerList(ids);
setCache(cacheKey, 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&timestamp=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) => {
try {
const { malId, slug, timestamp } = req.query;
if (!malId || !slug || !timestamp) {
return res.status(400).json({ success: false, message: "malId, slug, and timestamp are required" });
}
const cacheKey = `mapper_${malId}_${slug}_${timestamp}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractMapperServers(malId, slug, timestamp);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -68,3 +145,5 @@ const getMapperServers = async (req, res, next) => {
};
export { getStreamInfo, getServerList, getMapperServers };
// ══════════════════════════════════════════════════════════════ END: streamInfo.controller.js
+41 -3
View File
@@ -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 { 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) => {
try {
const { keyword } = req.query;
if (!keyword) {
return res.status(400).json({ success: false, message: "Keyword is required" });
}
const cacheKey = `suggest_${keyword}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractSuggestions(keyword);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -24,3 +60,5 @@ const getSuggestions = async (req, res, next) => {
};
export { getSuggestions };
// ══════════════════════════════════════════════════════════════ END: suggestion.controller.js
+37 -1
View File
@@ -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 { 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) => {
try {
const cacheKey = "topTen";
@@ -8,7 +43,6 @@ const getTopTen = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractTopTen();
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -18,3 +52,5 @@ const getTopTen = async (req, res, next) => {
};
export { getTopTen };
// ══════════════════════════════════════════════════════════════ END: topten.controller.js
+38 -1
View File
@@ -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 { 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) => {
try {
const cacheKey = "trending";
@@ -8,7 +44,6 @@ const getTrending = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractTrending();
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -18,3 +53,5 @@ const getTrending = async (req, res, next) => {
};
export { getTrending };
// ══════════════════════════════════════════════════════════════ END: trending.controller.js
+37 -1
View File
@@ -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 { 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) => {
try {
const cacheKey = "trendingSidebar";
@@ -8,7 +43,6 @@ const getTrendingSidebar = async (req, res, next) => {
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractTrendingSidebar();
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -18,3 +52,5 @@ const getTrendingSidebar = async (req, res, next) => {
};
export { getTrendingSidebar };
// ══════════════════════════════════════════════════════════════ END: trendingSidebar.controller.js
+42 -3
View File
@@ -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 { 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) => {
try {
const { id } = req.params;
if (!id) {
return res.status(400).json({ success: false, message: "Anime ID is required" });
}
const cacheKey = `watchorder_${id}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractWatchOrder(id);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -24,3 +61,5 @@ const getWatchOrder = async (req, res, next) => {
};
export { getWatchOrder };
// ══════════════════════════════════════════════════════════════ END: watchOrder.controller.js
+43 -3
View File
@@ -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 { 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) => {
try {
const { slug, ep } = req.query;
if (!slug || !ep) {
return res.status(400).json({ success: false, message: "Slug and episode number are required" });
}
const cacheKey = `watch_${slug}_${ep}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractWatchPage(slug, ep);
setCache(cacheKey, data);
res.json({ success: true, results: data });
@@ -24,3 +62,5 @@ const getWatchPage = async (req, res, next) => {
};
export { getWatchPage };
// ══════════════════════════════════════════════════════════════ END: watchPage.controller.js
+59
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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) => {
try {
const url = URLS.watch(slug);
const { data } = await axios.get(url, { headers });
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() || "";
// NOTE: data-jp attribute holds the Japanese title variant
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
const altNames = $(".names.font-italic").text().trim() || "";
const poster = $("img[itemprop='image']").attr("src") || "";
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 ratingValue = $("#w-rating span[itemprop='ratingValue']").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;
// ---- 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 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 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 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() || "";
// ---- FEATURE: Extract studios list ----
// NOTE: itemprop='director' links on studio spans — itemprop='name' inside holds the name
const studios = [];
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
studios.push($(el).text().trim());
});
// ---- FEATURE: Extract producers list ----
// NOTE: Same selector pattern as studios but from the 5th child (producers row)
const producers = [];
$(".bmeta .meta:nth-child(2) > div:nth-child(5) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
producers.push($(el).text().trim());
});
// ---- FEATURE: Extract genres ----
// NOTE: Genre links contain '/genre/' in their href — filters out non-genre links
const genres = [];
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
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] || "";
return {
@@ -72,3 +129,5 @@ const extractAnimeInfo = async (slug) => {
};
export { extractAnimeInfo };
// ══════════════════════════════════════════════════════════════ END: animeInfo.extractor.js
+53
View File
@@ -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 axios from "axios";
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 { 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) => {
try {
// NOTE: URLS.azList generates the correct endpoint for the given letter
const url = URLS.azList(letter);
const $ = await extractPages(url, page);
const totalPages = countPages($);
const results = [];
// NOTE: Multiple selectors to handle different page layouts
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
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 rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
results.push({
slug,
@@ -45,3 +96,5 @@ const extractAzList = async (letter = "all", page = 1) => {
};
export { extractAzList };
// ══════════════════════════════════════════════════════════════ END: azList.extractor.js
+56
View File
@@ -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 { headers } from "../configs/header.config.js";
import { URLS } from "../configs/dataUrl.js";
import { countPages } from "../helper/countPages.helper.js";
import { extractPages } from "../helper/extractPages.helper.js";
// ══════════════════════════════════════════════════════════════
// 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) => {
try {
// NOTE: Category type is determined by prefix in the category string
let url;
if (category.startsWith("genre/")) {
url = URLS.genre(category.replace("genre/", ""));
@@ -14,6 +64,7 @@ const extractCategory = async (category, page = 1) => {
} else if (category.startsWith("status/")) {
url = URLS.status(category.replace("status/", ""));
} else {
// NOTE: Fallback to direct URL construction for unknown category types
url = `${URLS.home}/${category}`;
}
@@ -21,6 +72,8 @@ const extractCategory = async (category, page = 1) => {
const totalPages = countPages($);
const results = [];
// NOTE: The list items are contained in #list-items container
$("#list-items > .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
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 rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
results.push({
slug,
@@ -56,3 +110,5 @@ const extractCategory = async (category, page = 1) => {
};
export { extractCategory };
// ══════════════════════════════════════════════════════════════ END: category.extractor.js
+46
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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) => {
try {
// NOTE: First fetch the watch page to extract the internal anime ID
const infoUrl = URLS.watch(slug);
const { data: infoData } = await axios.get(infoUrl, { headers });
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;
if (!animeId) {
@@ -16,6 +54,7 @@ const extractEpisodeList = async (slug) => {
}
try {
// NOTE: AJAX endpoint requires X-Requested-With header to return HTML fragment
const ajaxUrl = `${BASE_URL}/ajax/episode/list/${animeId}`;
const { data: ajaxData } = await axios.get(ajaxUrl, {
headers: {
@@ -26,13 +65,17 @@ const extractEpisodeList = async (slug) => {
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 = [];
$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 epNum = parseInt($ep(el).attr("data-num")) || i + 1;
const epSlug = $ep(el).attr("data-slug") || "";
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() || "";
// NOTE: Active class indicates the currently-playing episode
const isActive = $ep(el).hasClass("active") || false;
episodes.push({
@@ -52,6 +95,7 @@ const extractEpisodeList = async (slug) => {
episodes
};
} catch (ajaxError) {
// NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue)
return {
animeId,
slug,
@@ -65,3 +109,5 @@ const extractEpisodeList = async (slug) => {
};
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 { headers } from "../configs/header.config.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 = "") => {
try {
let url = `${BASE_URL}/ajax/episode/list/${animeId}`;
// NOTE: Optional params are appended only if non-empty to avoid trailing ?/&
const params = [];
if (style) params.push(`style=${style}`);
if (vrf) params.push(`vrf=${vrf}`);
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, {
headers: {
...headers,
@@ -24,3 +67,5 @@ const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
};
export { extractEpisodeListAjax };
// ══════════════════════════════════════════════════════════════ END: episodeListAjax.extractor.js
+61
View File
@@ -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 { headers } from "../configs/header.config.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 { 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) => {
try {
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) {
queryParams.set("keyword", params.keyword);
} else {
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) {
const genres = params.genre.split(",").map(g => GENRE_IDS[g.trim().toLowerCase()] || g.trim());
genres.forEach(g => queryParams.append("genre[]", g));
}
// ---- FEATURE: Map type names to IDs and append ----
if (params.type) {
const types = params.type.split(",").map(t => TYPE_IDS[t.trim().toLowerCase()] || t.trim());
types.forEach(t => queryParams.append("term_type[]", t));
}
// ---- FEATURE: Map status names to IDs and append ----
if (params.status) {
const statuses = params.status.split(",").map(s => STATUS_IDS[s.trim().toLowerCase()] || s.trim());
statuses.forEach(s => queryParams.append("status[]", s));
}
// ---- FEATURE: Append language filter ----
// NOTE: Languages are passed directly without ID mapping
if (params.language) {
const langs = params.language.split(",");
langs.forEach(l => queryParams.append("language[]", l.trim()));
}
// ---- FEATURE: Map rating names to IDs and append ----
if (params.rating) {
const ratings = params.rating.split(",").map(r => RATING_IDS[r.trim().toLowerCase()] || r.trim());
ratings.forEach(r => queryParams.append("rating[]", r));
}
// ---- FEATURE: Map sort key to sort value ----
if (params.sort) {
const sort = SORT_IDS[params.sort.trim().toLowerCase()] ?? params.sort;
if (sort) queryParams.set("sort", sort);
}
// ---- FEATURE: Append season filter ----
if (params.season) {
const seasons = params.season.split(",");
seasons.forEach(s => queryParams.append("season[]", s.trim()));
}
// ---- FEATURE: Append year filter ----
if (params.year) {
const years = params.year.split(",");
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 $ = await extractPages(filterUrl, params.page || 1);
const totalPages = countPages($);
@@ -95,3 +154,5 @@ const extractFilter = async (params) => {
};
export { extractFilter };
// ══════════════════════════════════════════════════════════════ END: filter.extractor.js
+47
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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 () => {
try {
const { data } = await axios.get(URLS.home, { headers });
const $ = cheerio.load(data);
// ---- FEATURE: Extract spotlight carousel items from #hotest section ----
// NOTE: Swiper slides contain the hero carousel on the homepage
const spotlights = [];
$("#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() || "";
// 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 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 description = $(el).find(".desc").text().trim() || "";
const rating = $(el).find("i.rating").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 dub = parseInt($(el).find("i.dub").text().trim()) || 0;
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 = [];
$(".section-updated .item, #recent-update .item").each((i, el) => {
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
const poster = $(el).find(".poster img").attr("src") || "";
const title = $(el).find("a.name.d-title").text().trim() || "";
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 dub = parseInt($(el).find(".ep-status.dub 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 = [];
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
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 = [];
$("#menu ul.c4 li a").each((i, el) => {
const genre = $(el).text().trim();
@@ -97,3 +142,5 @@ const extractHomeInfo = async () => {
};
export { extractHomeInfo };
// ══════════════════════════════════════════════════════════════ END: homeInfo.extractor.js
+83
View File
@@ -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 axios from "axios";
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 { 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) => {
try {
// NOTE: extractPages handles pagination and returns parsed Cheerio object
const $ = await extractPages(URLS.newRelease, page);
const totalPages = countPages($);
const results = [];
// NOTE: Multiple selectors to handle different page layouts
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
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 rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
results.push({
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) => {
try {
// NOTE: The latestUpdated endpoint with sort=added returns recently added anime
const $ = await extractPages(`${URLS.latestUpdated}?sort=added`, page);
const totalPages = countPages($);
const results = [];
// NOTE: Multiple selectors to handle different page layouts
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
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 type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
results.push({
slug,
@@ -80,3 +161,5 @@ const extractNewlyAdded = async (page = 1) => {
};
export { extractNewRelease, extractNewlyAdded };
// ══════════════════════════════════════════════════════════════ END: newRelease.extractor.js
+52
View File
@@ -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 { headers } from "../configs/header.config.js";
import { URLS } from "../configs/dataUrl.js";
import { countPages } from "../helper/countPages.helper.js";
import { extractPages } from "../helper/extractPages.helper.js";
// ══════════════════════════════════════════════════════════════
// 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) => {
try {
// NOTE: extractPages handles pagination and returns parsed Cheerio object
const $ = await extractPages(URLS.mostViewed, page);
const totalPages = countPages($);
const results = [];
// NOTE: The list items are contained in #list-items container
$("#list-items > .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
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 rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
results.push({
slug,
@@ -45,3 +95,5 @@ const extractPopular = async (page = 1) => {
};
export { extractPopular };
// ══════════════════════════════════════════════════════════════ END: popular.extractor.js
+46
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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 () => {
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, {
headers,
maxRedirects: 5,
validateStatus: (status) => status < 400
});
// NOTE: responseURL on the request object gives us the final resolved URL after redirects
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 $ = cheerio.load(data);
// ---- FEATURE: Extract anime title and identification ----
const title = $("h1[itemprop='name'].title.d-title").text().trim() || "";
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
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 synopsis = $(".synopsis .content").text().trim() || "";
const rating = $("#w-rating .score .value").text().trim() || "";
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 = [];
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
genres.push($(el).text().trim());
@@ -47,3 +91,5 @@ const extractRandom = async () => {
};
export { extractRandom };
// ══════════════════════════════════════════════════════════════ END: random.extractor.js
+44
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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) => {
try {
// NOTE: The schedule is fetched by appending date parameter to homepage
const url = `${URLS.home}?date=${date}`;
const { data } = await axios.get(url, { headers });
const $ = cheerio.load(data);
const schedule = [];
// NOTE: Schedule items use different selectors depending on page structure
$(".schedule-item, .anime-schedule .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
const title = $(el).find(".film-name a, .name").text().trim() || "";
const time = $(el).find(".time, .schedule-time").text().trim() || "";
const episodeNo = parseInt($(el).find(".episode-no, .ep-num").text().trim()) || 0;
// NOTE: Only include items with valid slugs
if (slug) {
schedule.push({
slug,
@@ -33,3 +75,5 @@ const extractSchedule = async (date) => {
};
export { extractSchedule };
// ══════════════════════════════════════════════════════════════ END: schedule.extractor.js
+60
View File
@@ -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 axios from "axios";
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 { 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) => {
try {
// WARNING: keyword param is required — omitting it may cause a 500 error from the source
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
const $ = await extractPages(url, page);
const totalPages = countPages($);
const results = [];
// NOTE: #list-items > .item is the standard result container on anikototv.to search pages
$("#list-items > .item").each((i, el) => {
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 title = $(el).find(".info .b1 a.name.d-title").text().trim() || "";
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 sub = parseInt($(el).find(".ep-status.sub 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;
// 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() || "";
// NOTE: Rating is in a dedicated .m-item.rated span
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
const genres = [];
$(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) => {
try {
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);
} catch (error) {
throw error;
@@ -76,3 +134,5 @@ const extractSearchSuggestions = async (keyword) => {
};
export { extractSearchResults, extractSearchSuggestions };
// ══════════════════════════════════════════════════════════════ END: search.extractor.js
+37
View File
@@ -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 { headers } from "../configs/header.config.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) => {
try {
// NOTE: This is an internal API endpoint requiring X-Requested-With header
const url = `${BASE_URL}/api/seasons/${animeId}`;
const { data } = await axios.get(url, {
headers: {
@@ -14,8 +48,11 @@ const extractSeasons = async (animeId) => {
return data || [];
} catch (error) {
// NOTE: Return empty array on error to maintain consistent API
return [];
}
};
export { extractSeasons };
// ══════════════════════════════════════════════════════════════ END: seasons.extractor.js
+49
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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 () => {
try {
// NOTE: The homepage contains the spotlight carousel in #hotest section
const { data } = await axios.get(URLS.home, { headers });
const $ = cheerio.load(data);
const spotlights = [];
// NOTE: Each slide in the swiper carousel represents a featured anime
$("#hotest .swiper-slide.item").each((i, el) => {
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
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 date = $(el).find("i.date").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
spotlights.push({
slug,
@@ -44,3 +91,5 @@ const extractSpotlight = async () => {
};
export { extractSpotlight };
// ══════════════════════════════════════════════════════════════ END: spotlight.extractor.js
+53
View File
@@ -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 axios from "axios";
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 { 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) => {
try {
// NOTE: URLS.status generates the correct endpoint for the given status
const url = URLS.status(status);
const $ = await extractPages(url, page);
const totalPages = countPages($);
const results = [];
// NOTE: Multiple selectors to handle different page layouts
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
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 rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
results.push({
slug,
@@ -45,3 +96,5 @@ const extractStatus = async (status, page = 1) => {
};
export { extractStatus };
// ══════════════════════════════════════════════════════════════ END: status.extractor.js
+76
View File
@@ -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 { headers } from "../configs/header.config.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) => {
try {
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, {
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) => {
try {
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) => {
try {
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") {
for (const [provider, sources] of Object.entries(data)) {
// NOTE: Each provider may have sub, dub, or both source types
if (sources && sources.sub) {
servers.push({
provider,
@@ -72,8 +145,11 @@ const extractMapperServers = async (malId, slug, timestamp) => {
return servers;
} catch (error) {
// NOTE: Returns empty array on failure — mapper API may be unavailable
return [];
}
};
export { extractStreamInfo, extractServerList, extractMapperServers };
// ══════════════════════════════════════════════════════════════ END: streamInfo.extractor.js
+48
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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) => {
try {
// NOTE: The search endpoint requires the 'keyword' parameter
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
const { data } = await axios.get(url, { headers });
const $ = cheerio.load(data);
const suggestions = [];
// NOTE: Search results use the same list structure as other pages
$("#list-items > .item").each((i, el) => {
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
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 dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
// NOTE: Only include items with valid slugs
if (slug) {
suggestions.push({ slug, poster, title, japaneseTitle, type, sub, dub });
}
});
// NOTE: Limit to 10 suggestions for autocomplete-style responses
return suggestions.slice(0, 10);
} catch (error) {
throw error;
@@ -31,3 +77,5 @@ const extractSuggestions = async (keyword) => {
};
export { extractSuggestions };
// ══════════════════════════════════════════════════════════════ END: suggestion.extractor.js
+49
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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 () => {
try {
// NOTE: The top 10 section uses tabbed content with data-name attributes
const { data } = await axios.get(URLS.home, { headers });
const $ = cheerio.load(data);
@@ -12,6 +55,7 @@ const extractTopTen = async () => {
const week = [];
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) => {
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
const poster = $(el).find(".poster img").attr("src") || "";
@@ -21,11 +65,13 @@ const extractTopTen = async () => {
const type = $(el).find(".type").text().trim() || "";
const rank = i + 1;
// NOTE: Only include items with valid slugs
if (slug) {
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) => {
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
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) => {
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
const poster = $(el).find(".poster img").attr("src") || "";
@@ -61,3 +108,5 @@ const extractTopTen = async () => {
};
export { extractTopTen };
// ══════════════════════════════════════════════════════════════ END: topten.extractor.js
+46
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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 () => {
try {
// NOTE: Trending data is split across multiple sections on the homepage
const { data } = await axios.get(URLS.home, { headers });
const $ = cheerio.load(data);
const trending = [];
// NOTE: This selector targets both the updated section and recent updates
$(".section-updated .item, #recent-update .item").each((i, el) => {
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
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 type = $(el).find(".meta .inner .right").text().trim() || "";
// NOTE: Only include items with valid slugs
if (slug) {
trending.push({
slug,
@@ -40,3 +84,5 @@ const extractTrending = async () => {
};
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 axios from "axios";
import { headers } from "../configs/header.config.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 () => {
try {
// NOTE: The sidebar content is embedded in the homepage HTML
const { data } = await axios.get(URLS.home, { headers });
const $ = cheerio.load(data);
@@ -12,6 +57,7 @@ const extractTrendingSidebar = async () => {
const week = [];
const month = [];
// NOTE: Daily rankings use the 'day' tab content
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
const poster = $(el).find(".poster img").attr("src") || "";
@@ -21,11 +67,13 @@ const extractTrendingSidebar = async () => {
const type = $(el).find(".type").text().trim() || "";
const rank = i + 1;
// NOTE: Only include items with valid slugs
if (slug) {
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) => {
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
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) => {
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
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 = [];
$("#recent-update .item, .section-updated .item").each((i, el) => {
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
@@ -77,3 +127,5 @@ const extractTrendingSidebar = async () => {
};
export { extractTrendingSidebar };
// ══════════════════════════════════════════════════════════════ END: trendingSidebar.extractor.js
+37
View File
@@ -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 { headers } from "../configs/header.config.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) => {
try {
// NOTE: This is an internal API endpoint requiring X-Requested-With header
const url = `${BASE_URL}/api/watch-order/${animeId}`;
const { data } = await axios.get(url, {
headers: {
@@ -14,8 +48,11 @@ const extractWatchOrder = async (animeId) => {
return data || [];
} catch (error) {
// NOTE: Return empty array on error to maintain consistent API
return [];
}
};
export { extractWatchOrder };
// ══════════════════════════════════════════════════════════════ END: watchOrder.extractor.js
+57
View File
@@ -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 axios from "axios";
import { headers } from "../configs/header.config.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) => {
try {
const url = URLS.episode(slug, ep);
const { data } = await axios.get(url, { headers });
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 animeUrl = $("#watch-main").attr("data-url") || "";
const title = $("h1[itemprop='name'].title.d-title").text().trim() || slug;
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
const episodeNumber = parseInt(ep) || 0;
// ---- FEATURE: Extract synopsis and rating ----
const synopsis = $(".synopsis .content").text().trim() || "";
const rating = $("#w-rating .score .value").text().trim() || "";
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] || "";
// ---- FEATURE: Extract anime metadata (type, status, MAL score, duration, episodes) ----
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 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 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 = [];
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
genres.push($(el).text().trim());
});
// ---- FEATURE: Extract studios ----
// NOTE: Uses itemprop='director' links with itemprop='name' spans inside
const studios = [];
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
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 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 = [];
$("#w-servers .servers .type li, #w-servers li[data-link-id]").each((i, el) => {
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 = [];
$(".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 trendPoster = $(el).find(".poster img").attr("src") || "";
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 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 = [];
$(".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() || "";
@@ -126,3 +181,5 @@ const extractWatchPage = async (slug, ep) => {
};
export { extractWatchPage };
// ══════════════════════════════════════════════════════════════ END: watchPage.extractor.js
+86
View File
@@ -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();
// ---- 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;
// ══════════════════════════════════════════════════════════════
// 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 item = cache.get(key);
if (item && Date.now() - item.timestamp < CACHE_TTL) {
@@ -10,12 +69,39 @@ const getCache = (key) => {
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) => {
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 = () => {
cache.clear();
};
export { getCache, setCache, clearCache };
// ══════════════════════════════════════════════════════════════ END: cache.helper.js
+42
View File
@@ -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 pages = [];
$(".pagination li a").each((i, el) => {
@@ -10,3 +51,4 @@ const countPages = ($) => {
};
export { countPages };
// ══════════════════════════════════════════════════════════════ END: countPages.helper.js
+77
View File
@@ -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";
// ---- 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";
// ---- 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";
// ══════════════════════════════════════════════════════════════
// 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) => {
try {
const finalUrl = page > 1 ? `${url}?page=${page}` : url;
@@ -14,3 +90,4 @@ const extractPages = async (url, page = 1) => {
};
export { extractPages };
// ══════════════════════════════════════════════════════════════ END: extractPages.helper.js
+53
View File
@@ -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) => {
if (!title) return "";
return title
@@ -7,3 +59,4 @@ const formatTitle = (title) => {
};
export { formatTitle };
// ══════════════════════════════════════════════════════════════ END: formatTitle.helper.js
+114
View File
@@ -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 { getAnimeInfo } from "../controllers/animeInfo.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 { 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) => {
// ══════════════════════════════════════════════════════════════
// HOME
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Homepage data endpoint ----
app.get("/api/", async (req, res, next) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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) => {
try {
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);
};
export { createApiRoutes };
// ══════════════════════════════════════════════════════════════ END: apiRoutes.js
+55
View File
@@ -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";
// ══════════════════════════════════════════════════════════════
// 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) => {
// ══════════════════════════════════════════════════════════════
// 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) => {
req.params.category = `genre/${req.params.genre}`;
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) => {
req.params.category = `type/${req.params.type}`;
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) => {
req.params.category = `status/${req.params.status}`;
try {
@@ -30,3 +83,5 @@ const categoryRoutes = (app, jsonResponse, jsonError) => {
};
export { categoryRoutes };
// ══════════════════════════════════════════════════════════════ END: category.route.js