From 0034617c3b752f96f3efc284fdcaaece3f4ba295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=C9=B4=E1=B4=87=C9=AA=20N=E1=B4=8F=E1=B4=9C?= =?UTF-8?q?=E1=B4=A2=E1=B4=87=C9=B4?= <157171073+Shineii86@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:14:01 +0530 Subject: [PATCH] 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> --- CHANGELOG.md | 13 ++ server.js | 70 ++++++++- src/configs/dataUrl.js | 96 ++++++++++++- src/configs/header.config.js | 43 +++++- src/configs/ids.config.js | 136 +++++++++++++++++- src/controllers/animeInfo.controller.js | 44 +++++- src/controllers/azList.controller.js | 46 +++++- src/controllers/category.controller.js | 48 ++++++- src/controllers/episodeList.controller.js | 46 +++++- src/controllers/episodeListAjax.controller.js | 49 ++++++- src/controllers/filter.controller.js | 48 ++++++- src/controllers/homeInfo.controller.js | 40 +++++- src/controllers/newRelease.controller.js | 63 +++++++- src/controllers/popular.controller.js | 42 +++++- src/controllers/random.controller.js | 40 +++++- src/controllers/schedule.controller.js | 47 +++++- src/controllers/search.controller.js | 67 ++++++++- src/controllers/seasons.controller.js | 46 +++++- src/controllers/spotlight.controller.js | 41 +++++- src/controllers/status.controller.js | 48 ++++++- src/controllers/streamInfo.controller.js | 99 +++++++++++-- src/controllers/suggestion.controller.js | 46 +++++- src/controllers/topten.controller.js | 40 +++++- src/controllers/trending.controller.js | 41 +++++- src/controllers/trendingSidebar.controller.js | 40 +++++- src/controllers/watchOrder.controller.js | 47 +++++- src/controllers/watchPage.controller.js | 48 ++++++- src/extractors/animeInfo.extractor.js | 61 +++++++- src/extractors/azList.extractor.js | 55 ++++++- src/extractors/category.extractor.js | 58 +++++++- src/extractors/episodeList.extractor.js | 48 ++++++- src/extractors/episodeListAjax.extractor.js | 47 +++++- src/extractors/filter.extractor.js | 63 +++++++- src/extractors/homeInfo.extractor.js | 49 ++++++- src/extractors/newRelease.extractor.js | 85 ++++++++++- src/extractors/popular.extractor.js | 54 ++++++- src/extractors/random.extractor.js | 48 ++++++- src/extractors/schedule.extractor.js | 46 +++++- src/extractors/search.extractor.js | 62 +++++++- src/extractors/seasons.extractor.js | 39 ++++- src/extractors/spotlight.extractor.js | 51 ++++++- src/extractors/status.extractor.js | 55 ++++++- src/extractors/streamInfo.extractor.js | 78 +++++++++- src/extractors/suggestion.extractor.js | 50 ++++++- src/extractors/topten.extractor.js | 51 ++++++- src/extractors/trending.extractor.js | 48 ++++++- src/extractors/trendingSidebar.extractor.js | 54 ++++++- src/extractors/watchOrder.extractor.js | 39 ++++- src/extractors/watchPage.extractor.js | 59 +++++++- src/helper/cache.helper.js | 88 +++++++++++- src/helper/countPages.helper.js | 44 +++++- src/helper/extractPages.helper.js | 79 +++++++++- src/helper/formatTitle.helper.js | 55 ++++++- src/routes/apiRoutes.js | 116 ++++++++++++++- src/routes/category.route.js | 57 +++++++- 55 files changed, 2965 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc7805b..497d667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/server.js b/server.js index ed9e92a..1b18d2b 100644 --- a/server.js +++ b/server.js @@ -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}`); -}); \ No newline at end of file +}); + +// ══════════════════════════════════════════════════════════════ END: server.js \ No newline at end of file diff --git a/src/configs/dataUrl.js b/src/configs/dataUrl.js index 869db39..cc2c82d 100644 --- a/src/configs/dataUrl.js +++ b/src/configs/dataUrl.js @@ -1,4 +1,47 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — dataUrl.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Central configuration file defining all URL patterns for the AniKato + * anime streaming site. Provides both base domain and alternative + * mirror domains for resilience and fallback purposes. + * + * @exports + * BASE_URL, ALT_DOMAINS, URLS + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +// ══════════════════════════════════════════════════════════════ +// BASE DOMAIN CONFIGURATION +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Primary base URL for the AniKato streaming service ---- +/** + * The primary base URL used for all API requests to AniKato. + * This serves as the canonical domain for the site. + * + * @type {string} + * @default "https://anikototv.to" + */ const BASE_URL = "https://anikototv.to"; + +// ---- 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`, @@ -23,4 +116,5 @@ const URLS = { serverList: `${BASE_URL}/ajax/server/list` }; -export { BASE_URL, ALT_DOMAINS, URLS }; \ No newline at end of file +export { BASE_URL, ALT_DOMAINS, URLS }; +// ══════════════════════════════════════════════════════════════ END: dataUrl.js \ No newline at end of file diff --git a/src/configs/header.config.js b/src/configs/header.config.js index f19bb46..e0098a8 100644 --- a/src/configs/header.config.js +++ b/src/configs/header.config.js @@ -1,3 +1,43 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — header.config.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * HTTP request header configuration for making API calls to the + * AniKato streaming site. Includes browser-like headers to + * mimic legitimate web traffic and avoid detection. + * + * @exports + * headers + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +// ══════════════════════════════════════════════════════════════ +// HTTP REQUEST HEADER CONFIGURATION +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Browser-mimicking headers for HTTP requests ---- +/** + * Standard HTTP headers that mimic a real browser request. + * Used with axios/fetch to make requests appear as legitimate + * browser traffic to the AniKato streaming site. + * + * @type {Object} + * @property {string} User-Agent - Browser identification string + * @property {string} Accept - Accepted response content types + * @property {string} Accept-Language - Preferred language settings + * @property {string} Accept-Encoding - Supported compression algorithms + * @property {string} Connection - Connection keep-alive preference + * @property {string} Upgrade-Insecure-Requests - HTTPS upgrade preference + * + * @example + * // Use with axios + * axios.get(url, { headers }) + */ const headers = { "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", @@ -7,4 +47,5 @@ const headers = { "Upgrade-Insecure-Requests": "1" }; -export { headers }; \ No newline at end of file +export { headers }; +// ══════════════════════════════════════════════════════════════ END: header.config.js \ No newline at end of file diff --git a/src/configs/ids.config.js b/src/configs/ids.config.js index 39f3bcc..9119545 100644 --- a/src/configs/ids.config.js +++ b/src/configs/ids.config.js @@ -1,3 +1,48 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — ids.config.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Mapping configuration for anime metadata IDs used by the AniKato + * site's filtering system. Contains human-readable slugs mapped + * to numeric IDs for genres, types, statuses, ratings, and sort options. + * + * @exports + * GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +// ══════════════════════════════════════════════════════════════ +// GENRE ID MAPPING +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Genre slug to numeric ID mapping ---- +/** + * Maps human-readable genre slugs to their corresponding numeric IDs + * used in AniKato's filtering system. Contains 43 different anime genres + * from action to vampire. + * + * @type {Object} + * @property {string} action - ID for action genre + * @property {string} adventure - ID for adventure genre + * @property {string} comedy - ID for comedy genre + * @property {string} drama - ID for drama genre + * @property {string} fantasy - ID for fantasy genre + * @property {string} horror - ID for horror genre + * @property {string} romance - ID for romance genre + * @property {string} sci-fi - ID for science fiction genre + * @property {string} slice-of-life - ID for slice of life genre + * @property {string} supernatural - ID for supernatural genre + * + * @example + * // Get genre ID for filtering + * GENRE_IDS["action"] // "1" + * GENRE_IDS["comedy"] // "4" + */ const GENRE_IDS = { "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", @@ -79,4 +212,5 @@ const SORT_IDS = { "number_of_episodes": "7" }; -export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS }; \ No newline at end of file +export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS }; +// ══════════════════════════════════════════════════════════════ END: ids.config.js \ No newline at end of file diff --git a/src/controllers/animeInfo.controller.js b/src/controllers/animeInfo.controller.js index a6ffb83..507c73a 100644 --- a/src/controllers/animeInfo.controller.js +++ b/src/controllers/animeInfo.controller.js @@ -1,19 +1,55 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — animeInfo.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for anime detail information. Extracts full anime + * metadata including title, synopsis, episodes, genres, etc. + * Requires an anime slug as query parameter. + * + * @exports + * getAnimeInfo - Express route handler for GET /api/info + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractAnimeInfo } from "../extractors/animeInfo.extractor.js"; import { 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 }); @@ -22,4 +58,6 @@ const getAnimeInfo = async (req, res, next) => { } }; -export { getAnimeInfo }; \ No newline at end of file +export { getAnimeInfo }; + +// ══════════════════════════════════════════════════════════════ END: animeInfo.controller.js \ No newline at end of file diff --git a/src/controllers/azList.controller.js b/src/controllers/azList.controller.js index ff020ab..728d740 100644 --- a/src/controllers/azList.controller.js +++ b/src/controllers/azList.controller.js @@ -1,17 +1,55 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — azList.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for alphabetical anime listing. Returns anime + * filtered by starting letter with pagination support. + * Default letter is 'all' if not specified. + * + * @exports + * getAzList - Express route handler for GET /api/az-list/:letter + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractAzList } from "../extractors/azList.extractor.js"; import { 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 }); @@ -20,4 +58,6 @@ const getAzList = async (req, res, next) => { } }; -export { getAzList }; \ No newline at end of file +export { getAzList }; + +// ══════════════════════════════════════════════════════════════ END: azList.controller.js \ No newline at end of file diff --git a/src/controllers/category.controller.js b/src/controllers/category.controller.js index 47bb13a..aebe49e 100644 --- a/src/controllers/category.controller.js +++ b/src/controllers/category.controller.js @@ -1,21 +1,59 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — category.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for category-based anime filtering. Handles genre, + * type, and status categories with a unified endpoint. The + * category path is set by the route before reaching this controller. + * + * @exports + * getCategory - Express route handler for category endpoints + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractCategory } from "../extractors/category.extractor.js"; import { 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 }); @@ -24,4 +62,6 @@ const getCategory = async (req, res, next) => { } }; -export { getCategory }; \ No newline at end of file +export { getCategory }; + +// ══════════════════════════════════════════════════════════════ END: category.controller.js \ No newline at end of file diff --git a/src/controllers/episodeList.controller.js b/src/controllers/episodeList.controller.js index 04ae8c2..0bf3bc0 100644 --- a/src/controllers/episodeList.controller.js +++ b/src/controllers/episodeList.controller.js @@ -1,20 +1,56 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — episodeList.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for fetching the episode list of a specific anime. + * Returns all available episodes with their numbers and titles. + * Requires an anime ID as a route parameter. + * + * @exports + * getEpisodeList - Express route handler for GET /api/episodes/:id + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractEpisodeList } from "../extractors/episodeList.extractor.js"; import { 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 }); @@ -23,4 +59,6 @@ const getEpisodeList = async (req, res, next) => { } }; -export { getEpisodeList }; \ No newline at end of file +export { getEpisodeList }; + +// ══════════════════════════════════════════════════════════════ END: episodeList.controller.js \ No newline at end of file diff --git a/src/controllers/episodeListAjax.controller.js b/src/controllers/episodeListAjax.controller.js index 19743d3..dfa55d6 100644 --- a/src/controllers/episodeListAjax.controller.js +++ b/src/controllers/episodeListAjax.controller.js @@ -1,21 +1,60 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — episodeListAjax.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for AJAX-loaded episode list data. Provides additional + * episode metadata that may not be available in the standard episode + * list endpoint. Supports style and VRF query parameters. + * + * @exports + * getEpisodeListAjax - Express route handler for GET /api/episodes-ajax/:id + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractEpisodeListAjax } from "../extractors/episodeListAjax.extractor.js"; import { 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 }); @@ -24,4 +63,6 @@ const getEpisodeListAjax = async (req, res, next) => { } }; -export { getEpisodeListAjax }; \ No newline at end of file +export { getEpisodeListAjax }; + +// ══════════════════════════════════════════════════════════════ END: episodeListAjax.controller.js \ No newline at end of file diff --git a/src/controllers/filter.controller.js b/src/controllers/filter.controller.js index d1b65d9..56d6ad9 100644 --- a/src/controllers/filter.controller.js +++ b/src/controllers/filter.controller.js @@ -1,6 +1,49 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — filter.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for advanced anime filtering. Accepts multiple query + * parameters to filter anime by genre, year, season, type, status, + * and more. All parameters are forwarded to the extractor. + * + * @exports + * getFilter - Express route handler for GET /api/filter + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractFilter } from "../extractors/filter.extractor.js"; import { 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 }); @@ -18,4 +60,6 @@ const getFilter = async (req, res, next) => { } }; -export { getFilter }; \ No newline at end of file +export { getFilter }; + +// ══════════════════════════════════════════════════════════════ END: filter.controller.js \ No newline at end of file diff --git a/src/controllers/homeInfo.controller.js b/src/controllers/homeInfo.controller.js index 43d8b9f..5cfc997 100644 --- a/src/controllers/homeInfo.controller.js +++ b/src/controllers/homeInfo.controller.js @@ -1,6 +1,41 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — homeInfo.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for the homepage data endpoint. Extracts featured + * anime, spotlight, trending, and other homepage sections. + * Uses in-memory caching to avoid repeated scraping. + * + * @exports + * getHomeInfo - Express route handler for GET /api/ + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractHomeInfo } from "../extractors/homeInfo.extractor.js"; import { 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 }); @@ -17,4 +51,6 @@ const getHomeInfo = async (req, res, next) => { } }; -export { getHomeInfo }; \ No newline at end of file +export { getHomeInfo }; + +// ══════════════════════════════════════════════════════════════ END: homeInfo.controller.js \ No newline at end of file diff --git a/src/controllers/newRelease.controller.js b/src/controllers/newRelease.controller.js index 1434bc0..8e2291d 100644 --- a/src/controllers/newRelease.controller.js +++ b/src/controllers/newRelease.controller.js @@ -1,6 +1,44 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — newRelease.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for new release and newly added anime. Provides + * two endpoints: one for recently released episodes and one + * for newly added anime series. Both support pagination. + * + * @exports + * getNewRelease - Express route handler for GET /api/new-release + * getNewlyAdded - Express route handler for GET /api/newly-added + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractNewRelease, extractNewlyAdded } from "../extractors/newRelease.extractor.js"; import { 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 }); @@ -35,4 +90,6 @@ const getNewlyAdded = async (req, res, next) => { } }; -export { getNewRelease, getNewlyAdded }; \ No newline at end of file +export { getNewRelease, getNewlyAdded }; + +// ══════════════════════════════════════════════════════════════ END: newRelease.controller.js \ No newline at end of file diff --git a/src/controllers/popular.controller.js b/src/controllers/popular.controller.js index 2cf6a76..1dd1b64 100644 --- a/src/controllers/popular.controller.js +++ b/src/controllers/popular.controller.js @@ -1,6 +1,43 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — popular.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for most popular anime list. Returns paginated list + * of the most popular anime from the source site. Supports page + * parameter for pagination. + * + * @exports + * getPopular - Express route handler for GET /api/most-popular + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractPopular } from "../extractors/popular.extractor.js"; import { 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 }); @@ -18,4 +54,6 @@ const getPopular = async (req, res, next) => { } }; -export { getPopular }; \ No newline at end of file +export { getPopular }; + +// ══════════════════════════════════════════════════════════════ END: popular.controller.js \ No newline at end of file diff --git a/src/controllers/random.controller.js b/src/controllers/random.controller.js index e20c54f..b237fb8 100644 --- a/src/controllers/random.controller.js +++ b/src/controllers/random.controller.js @@ -1,6 +1,41 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — random.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for random anime recommendation. Returns a single + * random anime from the source site. No parameters required. + * + * @exports + * getRandom - Express route handler for GET /api/random + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractRandom } from "../extractors/random.extractor.js"; import { 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 }); @@ -17,4 +51,6 @@ const getRandom = async (req, res, next) => { } }; -export { getRandom }; \ No newline at end of file +export { getRandom }; + +// ══════════════════════════════════════════════════════════════ END: random.controller.js \ No newline at end of file diff --git a/src/controllers/schedule.controller.js b/src/controllers/schedule.controller.js index 978fe23..ecab44e 100644 --- a/src/controllers/schedule.controller.js +++ b/src/controllers/schedule.controller.js @@ -1,20 +1,57 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — schedule.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for anime airing schedule. Returns anime episodes + * scheduled to air on a specific date. Requires date in + * YYYY-MM-DD format as a query parameter. + * + * @exports + * getSchedule - Express route handler for GET /api/schedule + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractSchedule } from "../extractors/schedule.extractor.js"; import { 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 }); @@ -23,4 +60,6 @@ const getSchedule = async (req, res, next) => { } }; -export { getSchedule }; \ No newline at end of file +export { getSchedule }; + +// ══════════════════════════════════════════════════════════════ END: schedule.controller.js \ No newline at end of file diff --git a/src/controllers/search.controller.js b/src/controllers/search.controller.js index d9e74f5..63d0a93 100644 --- a/src/controllers/search.controller.js +++ b/src/controllers/search.controller.js @@ -1,19 +1,56 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — search.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for search functionality. Provides both full search + * results with pagination and lightweight autocomplete suggestions. + * Both endpoints require a 'keyword' query parameter. + * + * @exports + * getSearchResults - Express route handler for GET /api/search + * getSearchSuggestions - Express route handler for GET /api/search/suggest + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractSearchResults, extractSearchSuggestions } from "../extractors/search.extractor.js"; import { 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 }); @@ -43,4 +98,6 @@ const getSearchSuggestions = async (req, res, next) => { } }; -export { getSearchResults, getSearchSuggestions }; \ No newline at end of file +export { getSearchResults, getSearchSuggestions }; + +// ══════════════════════════════════════════════════════════════ END: search.controller.js \ No newline at end of file diff --git a/src/controllers/seasons.controller.js b/src/controllers/seasons.controller.js index 63ca9f8..a3991bb 100644 --- a/src/controllers/seasons.controller.js +++ b/src/controllers/seasons.controller.js @@ -1,20 +1,56 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — seasons.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for anime season information. Returns season data + * for a specific anime including related seasons and episodes. + * Requires an anime ID as a route parameter. + * + * @exports + * getSeasons - Express route handler for GET /api/seasons/:id + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractSeasons } from "../extractors/seasons.extractor.js"; import { 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 }); @@ -23,4 +59,6 @@ const getSeasons = async (req, res, next) => { } }; -export { getSeasons }; \ No newline at end of file +export { getSeasons }; + +// ══════════════════════════════════════════════════════════════ END: seasons.controller.js \ No newline at end of file diff --git a/src/controllers/spotlight.controller.js b/src/controllers/spotlight.controller.js index 4d26180..265d039 100644 --- a/src/controllers/spotlight.controller.js +++ b/src/controllers/spotlight.controller.js @@ -1,6 +1,42 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — spotlight.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for spotlight/featured anime carousel data. + * Returns a list of featured anime with banners and descriptions. + * No parameters required - returns fixed spotlight content. + * + * @exports + * getSpotlight - Express route handler for GET /api/spotlight + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractSpotlight } from "../extractors/spotlight.extractor.js"; import { 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 }); @@ -17,4 +52,6 @@ const getSpotlight = async (req, res, next) => { } }; -export { getSpotlight }; \ No newline at end of file +export { getSpotlight }; + +// ══════════════════════════════════════════════════════════════ END: spotlight.controller.js \ No newline at end of file diff --git a/src/controllers/status.controller.js b/src/controllers/status.controller.js index 09d5da2..48be13a 100644 --- a/src/controllers/status.controller.js +++ b/src/controllers/status.controller.js @@ -1,21 +1,59 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — status.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for anime filtering by airing status. Returns anime + * filtered by their current airing status (currently-airing, + * finished-airing, not-yet-aired) with pagination support. + * + * @exports + * getStatus - Express route handler for GET /api/status/:status + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractStatus } from "../extractors/status.extractor.js"; import { 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 }); @@ -24,4 +62,6 @@ const getStatus = async (req, res, next) => { } }; -export { getStatus }; \ No newline at end of file +export { getStatus }; + +// ══════════════════════════════════════════════════════════════ END: status.controller.js \ No newline at end of file diff --git a/src/controllers/streamInfo.controller.js b/src/controllers/streamInfo.controller.js index b1d61ce..3d9ea00 100644 --- a/src/controllers/streamInfo.controller.js +++ b/src/controllers/streamInfo.controller.js @@ -1,20 +1,58 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — streamInfo.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for streaming information. Provides stream URLs, + * available servers, and mapped server data for episode playback. + * Three endpoints for different streaming data needs. + * + * @exports + * getStreamInfo - Express route handler for GET /api/stream + * getServerList - Express route handler for GET /api/servers + * getMapperServers - Express route handler for GET /api/mapper-servers + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractStreamInfo, extractServerList, extractMapperServers } from "../extractors/streamInfo.extractor.js"; import { 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×tamp=1234567890 + * Response: { success: true, results: [...] } + * + * @throws {400} If any required parameter is missing + * NOTE: All three parameters (malId, slug, timestamp) are required + */ const getMapperServers = async (req, res, next) => { 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 }); @@ -67,4 +144,6 @@ const getMapperServers = async (req, res, next) => { } }; -export { getStreamInfo, getServerList, getMapperServers }; \ No newline at end of file +export { getStreamInfo, getServerList, getMapperServers }; + +// ══════════════════════════════════════════════════════════════ END: streamInfo.controller.js \ No newline at end of file diff --git a/src/controllers/suggestion.controller.js b/src/controllers/suggestion.controller.js index 97dd4cf..d6b5ba6 100644 --- a/src/controllers/suggestion.controller.js +++ b/src/controllers/suggestion.controller.js @@ -1,20 +1,56 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — suggestion.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for anime suggestions/autocomplete. Returns a list + * of anime matching the given keyword for autocomplete features. + * Requires a 'keyword' query parameter. + * + * @exports + * getSuggestions - Express route handler for GET /api/suggestions + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractSuggestions } from "../extractors/suggestion.extractor.js"; import { 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 }); @@ -23,4 +59,6 @@ const getSuggestions = async (req, res, next) => { } }; -export { getSuggestions }; \ No newline at end of file +export { getSuggestions }; + +// ══════════════════════════════════════════════════════════════ END: suggestion.controller.js \ No newline at end of file diff --git a/src/controllers/topten.controller.js b/src/controllers/topten.controller.js index 07ef1a6..ade127f 100644 --- a/src/controllers/topten.controller.js +++ b/src/controllers/topten.controller.js @@ -1,6 +1,41 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — topten.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for top 10 ranked anime. Returns the current top 10 + * anime list from the source site. No parameters required. + * + * @exports + * getTopTen - Express route handler for GET /api/top-ten + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractTopTen } from "../extractors/topten.extractor.js"; import { 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 }); @@ -17,4 +51,6 @@ const getTopTen = async (req, res, next) => { } }; -export { getTopTen }; \ No newline at end of file +export { getTopTen }; + +// ══════════════════════════════════════════════════════════════ END: topten.controller.js \ No newline at end of file diff --git a/src/controllers/trending.controller.js b/src/controllers/trending.controller.js index 96d9fcb..6aedb12 100644 --- a/src/controllers/trending.controller.js +++ b/src/controllers/trending.controller.js @@ -1,6 +1,42 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — trending.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for currently trending anime data. Returns a list + * of anime that are currently popular/trending on the source site. + * No parameters required - returns fixed trending content. + * + * @exports + * getTrending - Express route handler for GET /api/trending + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractTrending } from "../extractors/trending.extractor.js"; import { 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 }); @@ -17,4 +52,6 @@ const getTrending = async (req, res, next) => { } }; -export { getTrending }; \ No newline at end of file +export { getTrending }; + +// ══════════════════════════════════════════════════════════════ END: trending.controller.js \ No newline at end of file diff --git a/src/controllers/trendingSidebar.controller.js b/src/controllers/trendingSidebar.controller.js index 3320101..ebdb97d 100644 --- a/src/controllers/trendingSidebar.controller.js +++ b/src/controllers/trendingSidebar.controller.js @@ -1,6 +1,41 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — trendingSidebar.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for trending sidebar widget data. Returns trending + * anime data formatted for sidebar display. No parameters required. + * + * @exports + * getTrendingSidebar - Express route handler for GET /api/trending-sidebar + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractTrendingSidebar } from "../extractors/trendingSidebar.extractor.js"; import { 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 }); @@ -17,4 +51,6 @@ const getTrendingSidebar = async (req, res, next) => { } }; -export { getTrendingSidebar }; \ No newline at end of file +export { getTrendingSidebar }; + +// ══════════════════════════════════════════════════════════════ END: trendingSidebar.controller.js \ No newline at end of file diff --git a/src/controllers/watchOrder.controller.js b/src/controllers/watchOrder.controller.js index 5294eef..9b5c425 100644 --- a/src/controllers/watchOrder.controller.js +++ b/src/controllers/watchOrder.controller.js @@ -1,20 +1,57 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — watchOrder.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for recommended watch order. Returns the optimal + * viewing order for anime with multiple seasons, movies, and OVAs. + * Requires an anime ID as a route parameter. + * + * @exports + * getWatchOrder - Express route handler for GET /api/watch-order/:id + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractWatchOrder } from "../extractors/watchOrder.extractor.js"; import { 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 }); @@ -23,4 +60,6 @@ const getWatchOrder = async (req, res, next) => { } }; -export { getWatchOrder }; \ No newline at end of file +export { getWatchOrder }; + +// ══════════════════════════════════════════════════════════════ END: watchOrder.controller.js \ No newline at end of file diff --git a/src/controllers/watchPage.controller.js b/src/controllers/watchPage.controller.js index 4ddcb4b..4bf6837 100644 --- a/src/controllers/watchPage.controller.js +++ b/src/controllers/watchPage.controller.js @@ -1,20 +1,58 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — watchPage.controller.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Controller for the watch page endpoint. Returns episode + * details, video player data, and related anime information. + * Requires both 'slug' and 'ep' query parameters. + * + * @exports + * getWatchPage - Express route handler for GET /api/watch + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { extractWatchPage } from "../extractors/watchPage.extractor.js"; import { 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 }); @@ -23,4 +61,6 @@ const getWatchPage = async (req, res, next) => { } }; -export { getWatchPage }; \ No newline at end of file +export { getWatchPage }; + +// ══════════════════════════════════════════════════════════════ END: watchPage.controller.js \ No newline at end of file diff --git a/src/extractors/animeInfo.extractor.js b/src/extractors/animeInfo.extractor.js index 49ce20c..28ded74 100644 --- a/src/extractors/animeInfo.extractor.js +++ b/src/extractors/animeInfo.extractor.js @@ -1,47 +1,104 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — animeInfo.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts detailed anime information from anikototv.to including title, + * synopsis, metadata (type, status, studios, producers, genres), ratings, + * and background artwork for a given anime slug. + * + * @exports + * extractAnimeInfo + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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} 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 { @@ -71,4 +128,6 @@ const extractAnimeInfo = async (slug) => { } }; -export { extractAnimeInfo }; \ No newline at end of file +export { extractAnimeInfo }; + +// ══════════════════════════════════════════════════════════════ END: animeInfo.extractor.js diff --git a/src/extractors/azList.extractor.js b/src/extractors/azList.extractor.js index 1b6f1a5..917815f 100644 --- a/src/extractors/azList.extractor.js +++ b/src/extractors/azList.extractor.js @@ -1,3 +1,20 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — azList.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts anime from A-Z listing pages with pagination. + * Supports filtering by letter (a-z) or "all" for complete list. + * + * @exports + * extractAzList + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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} 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, @@ -44,4 +95,6 @@ const extractAzList = async (letter = "all", page = 1) => { } }; -export { extractAzList }; \ No newline at end of file +export { extractAzList }; + +// ══════════════════════════════════════════════════════════════ END: azList.extractor.js diff --git a/src/extractors/category.extractor.js b/src/extractors/category.extractor.js index c8ca8d9..8e553c7 100644 --- a/src/extractors/category.extractor.js +++ b/src/extractors/category.extractor.js @@ -1,11 +1,61 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — category.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts anime filtered by category (genre, type, status, etc.). + * Supports multiple category types with automatic URL detection. + * + * @exports + * extractCategory + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import { 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 containing totalPages and data array + * @returns {number} return.totalPages - Total number of pages available + * @returns {Array} 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, @@ -55,4 +109,6 @@ const extractCategory = async (category, page = 1) => { } }; -export { extractCategory }; \ No newline at end of file +export { extractCategory }; + +// ══════════════════════════════════════════════════════════════ END: category.extractor.js diff --git a/src/extractors/episodeList.extractor.js b/src/extractors/episodeList.extractor.js index 60cb64f..870e5ae 100644 --- a/src/extractors/episodeList.extractor.js +++ b/src/extractors/episodeList.extractor.js @@ -1,14 +1,52 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — episodeList.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts the full episode list for a given anime from anikototv.to. + * First resolves the anime ID from the watch page, then fetches the + * episode list via AJAX endpoint. + * + * @exports + * extractEpisodeList + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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, @@ -64,4 +108,6 @@ const extractEpisodeList = async (slug) => { } }; -export { extractEpisodeList }; \ No newline at end of file +export { extractEpisodeList }; + +// ══════════════════════════════════════════════════════════════ END: episodeList.extractor.js diff --git a/src/extractors/episodeListAjax.extractor.js b/src/extractors/episodeListAjax.extractor.js index f9af932..04972c4 100644 --- a/src/extractors/episodeListAjax.extractor.js +++ b/src/extractors/episodeListAjax.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} 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, @@ -23,4 +66,6 @@ const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => { } }; -export { extractEpisodeListAjax }; \ No newline at end of file +export { extractEpisodeListAjax }; + +// ══════════════════════════════════════════════════════════════ END: episodeListAjax.extractor.js diff --git a/src/extractors/filter.extractor.js b/src/extractors/filter.extractor.js index e5ec49c..02867db 100644 --- a/src/extractors/filter.extractor.js +++ b/src/extractors/filter.extractor.js @@ -1,3 +1,21 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — filter.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Provides advanced anime filtering on anikototv.to — supports filtering + * by genre, type, status, language, rating, sort order, season, year, + * and keyword with full pagination support. + * + * @exports + * extractFilter + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import { 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 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($); @@ -94,4 +153,6 @@ const extractFilter = async (params) => { } }; -export { extractFilter }; \ No newline at end of file +export { extractFilter }; + +// ══════════════════════════════════════════════════════════════ END: filter.extractor.js diff --git a/src/extractors/homeInfo.extractor.js b/src/extractors/homeInfo.extractor.js index cdc2c0b..9755443 100644 --- a/src/extractors/homeInfo.extractor.js +++ b/src/extractors/homeInfo.extractor.js @@ -1,22 +1,60 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — homeInfo.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts homepage data from anikototv.to including spotlight carousel, + * trending anime, top airing list, and available genre filters. + * + * @exports + * extractHomeInfo + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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(); @@ -96,4 +141,6 @@ const extractHomeInfo = async () => { } }; -export { extractHomeInfo }; \ No newline at end of file +export { extractHomeInfo }; + +// ══════════════════════════════════════════════════════════════ END: homeInfo.extractor.js diff --git a/src/extractors/newRelease.extractor.js b/src/extractors/newRelease.extractor.js index d9b4594..9727881 100644 --- a/src/extractors/newRelease.extractor.js +++ b/src/extractors/newRelease.extractor.js @@ -1,3 +1,21 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — newRelease.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts newly released and newly added anime with pagination. + * Provides both new releases and sorted by addition date. + * + * @exports + * extractNewRelease + * extractNewlyAdded + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 containing totalPages and data array + * @returns {number} return.totalPages - Total number of pages available + * @returns {Array} 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 containing totalPages and data array + * @returns {number} return.totalPages - Total number of pages available + * @returns {Array} 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, @@ -79,4 +160,6 @@ const extractNewlyAdded = async (page = 1) => { } }; -export { extractNewRelease, extractNewlyAdded }; \ No newline at end of file +export { extractNewRelease, extractNewlyAdded }; + +// ══════════════════════════════════════════════════════════════ END: newRelease.extractor.js diff --git a/src/extractors/popular.extractor.js b/src/extractors/popular.extractor.js index 5b5decd..5c4088a 100644 --- a/src/extractors/popular.extractor.js +++ b/src/extractors/popular.extractor.js @@ -1,15 +1,64 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — popular.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts most viewed/popular anime with pagination support. + * Scrapes the most viewed section with page navigation. + * + * @exports + * extractPopular + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import { 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 containing totalPages and data array + * @returns {number} return.totalPages - Total number of pages available + * @returns {Array} 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, @@ -44,4 +94,6 @@ const extractPopular = async (page = 1) => { } }; -export { extractPopular }; \ No newline at end of file +export { extractPopular }; + +// ══════════════════════════════════════════════════════════════ END: popular.extractor.js diff --git a/src/extractors/random.extractor.js b/src/extractors/random.extractor.js index 5c89e9c..f1cb4ae 100644 --- a/src/extractors/random.extractor.js +++ b/src/extractors/random.extractor.js @@ -1,29 +1,73 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — random.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Fetches a random anime from anikototv.to by following the redirect + * chain from the random endpoint, then extracts the full anime detail + * page metadata from the resolved URL. + * + * @exports + * extractRandom + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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} 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()); @@ -46,4 +90,6 @@ const extractRandom = async () => { } }; -export { extractRandom }; \ No newline at end of file +export { extractRandom }; + +// ══════════════════════════════════════════════════════════════ END: random.extractor.js diff --git a/src/extractors/schedule.extractor.js b/src/extractors/schedule.extractor.js index c10e948..715b61b 100644 --- a/src/extractors/schedule.extractor.js +++ b/src/extractors/schedule.extractor.js @@ -1,21 +1,63 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — schedule.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts anime schedule for a specific date. + * Returns airing times and episode numbers for scheduled anime. + * + * @exports + * extractSchedule + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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, @@ -32,4 +74,6 @@ const extractSchedule = async (date) => { } }; -export { extractSchedule }; \ No newline at end of file +export { extractSchedule }; + +// ══════════════════════════════════════════════════════════════ END: schedule.extractor.js diff --git a/src/extractors/search.extractor.js b/src/extractors/search.extractor.js index f7f7e1d..70204be 100644 --- a/src/extractors/search.extractor.js +++ b/src/extractors/search.extractor.js @@ -1,3 +1,20 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — search.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Provides search functionality for anikototv.to — supports paginated + * search results and lightweight autocomplete suggestions. + * + * @exports + * extractSearchResults, extractSearchSuggestions + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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 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,10 +126,13 @@ const extractSearchSuggestions = async (keyword) => { } }); + // NOTE: Limit to 10 suggestions to keep autocomplete payloads small return suggestions.slice(0, 10); } catch (error) { throw error; } }; -export { extractSearchResults, extractSearchSuggestions }; \ No newline at end of file +export { extractSearchResults, extractSearchSuggestions }; + +// ══════════════════════════════════════════════════════════════ END: search.extractor.js diff --git a/src/extractors/seasons.extractor.js b/src/extractors/seasons.extractor.js index 51fb3f6..aa73843 100644 --- a/src/extractors/seasons.extractor.js +++ b/src/extractors/seasons.extractor.js @@ -1,9 +1,43 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — seasons.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts season information for a specific anime via API. + * Returns season data from the internal API endpoint. + * + * @exports + * extractSeasons + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import axios from "axios"; import { 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 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 }; \ No newline at end of file +export { extractSeasons }; + +// ══════════════════════════════════════════════════════════════ END: seasons.extractor.js diff --git a/src/extractors/spotlight.extractor.js b/src/extractors/spotlight.extractor.js index 25e369c..a5527e4 100644 --- a/src/extractors/spotlight.extractor.js +++ b/src/extractors/spotlight.extractor.js @@ -1,14 +1,60 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — spotlight.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts spotlight/featured anime items from the homepage. + * Scrapes the "hottest" carousel section for trending anime. + * + * @exports + * extractSpotlight + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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, @@ -43,4 +90,6 @@ const extractSpotlight = async () => { } }; -export { extractSpotlight }; \ No newline at end of file +export { extractSpotlight }; + +// ══════════════════════════════════════════════════════════════ END: spotlight.extractor.js diff --git a/src/extractors/status.extractor.js b/src/extractors/status.extractor.js index 60c7d3a..aea35f9 100644 --- a/src/extractors/status.extractor.js +++ b/src/extractors/status.extractor.js @@ -1,3 +1,20 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — status.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts anime filtered by status (ongoing, completed, etc.). + * Supports pagination for browsing large result sets. + * + * @exports + * extractStatus + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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} 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, @@ -44,4 +95,6 @@ const extractStatus = async (status, page = 1) => { } }; -export { extractStatus }; \ No newline at end of file +export { extractStatus }; + +// ══════════════════════════════════════════════════════════════ END: status.extractor.js diff --git a/src/extractors/streamInfo.extractor.js b/src/extractors/streamInfo.extractor.js index 37a5214..1abcca8 100644 --- a/src/extractors/streamInfo.extractor.js +++ b/src/extractors/streamInfo.extractor.js @@ -1,10 +1,46 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — streamInfo.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Handles server/stream resolution for episodes — fetches individual + * stream URLs, server lists via AJAX, and alternative servers from + * the nekostream mapper API. + * + * @exports + * extractStreamInfo, extractServerList, extractMapperServers + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import axios from "axios"; import { 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 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} 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 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 }; \ No newline at end of file +export { extractStreamInfo, extractServerList, extractMapperServers }; + +// ══════════════════════════════════════════════════════════════ END: streamInfo.extractor.js diff --git a/src/extractors/suggestion.extractor.js b/src/extractors/suggestion.extractor.js index 2c8644a..b6f7527 100644 --- a/src/extractors/suggestion.extractor.js +++ b/src/extractors/suggestion.extractor.js @@ -1,15 +1,59 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — suggestion.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts search suggestions/autocomplete results for a keyword. + * Returns up to 10 matching anime suggestions. + * + * @exports + * extractSuggestions + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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,15 +63,19 @@ 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; } }; -export { extractSuggestions }; \ No newline at end of file +export { extractSuggestions }; + +// ══════════════════════════════════════════════════════════════ END: suggestion.extractor.js diff --git a/src/extractors/topten.extractor.js b/src/extractors/topten.extractor.js index d6c25f7..aee7658 100644 --- a/src/extractors/topten.extractor.js +++ b/src/extractors/topten.extractor.js @@ -1,10 +1,53 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — topten.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts top 10 anime rankings from the homepage. + * Parses daily, weekly, and monthly top anime lists. + * + * @exports + * extractTopTen + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 containing today, week, and month arrays + * @returns {Array} return.today - Daily top 10 anime + * @returns {Array} return.week - Weekly top 10 anime + * @returns {Array} 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") || ""; @@ -60,4 +107,6 @@ const extractTopTen = async () => { } }; -export { extractTopTen }; \ No newline at end of file +export { extractTopTen }; + +// ══════════════════════════════════════════════════════════════ END: topten.extractor.js diff --git a/src/extractors/trending.extractor.js b/src/extractors/trending.extractor.js index fb88a5d..47cece2 100644 --- a/src/extractors/trending.extractor.js +++ b/src/extractors/trending.extractor.js @@ -1,14 +1,57 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — trending.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts trending and recently updated anime from the homepage. + * Scrapes both the updated section and recent updates section. + * + * @exports + * extractTrending + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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 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, @@ -39,4 +83,6 @@ const extractTrending = async () => { } }; -export { extractTrending }; \ No newline at end of file +export { extractTrending }; + +// ══════════════════════════════════════════════════════════════ END: trending.extractor.js diff --git a/src/extractors/trendingSidebar.extractor.js b/src/extractors/trendingSidebar.extractor.js index ca93ac3..66ef4ff 100644 --- a/src/extractors/trendingSidebar.extractor.js +++ b/src/extractors/trendingSidebar.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 containing day, week, month, and latestEpisodes + * @returns {Array} return.day - Daily top anime + * @returns {Array} return.week - Weekly top anime + * @returns {Array} return.month - Monthly top anime + * @returns {Array} 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() || ""; @@ -76,4 +126,6 @@ const extractTrendingSidebar = async () => { } }; -export { extractTrendingSidebar }; \ No newline at end of file +export { extractTrendingSidebar }; + +// ══════════════════════════════════════════════════════════════ END: trendingSidebar.extractor.js diff --git a/src/extractors/watchOrder.extractor.js b/src/extractors/watchOrder.extractor.js index e7e95d5..39ca3b7 100644 --- a/src/extractors/watchOrder.extractor.js +++ b/src/extractors/watchOrder.extractor.js @@ -1,9 +1,43 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — watchOrder.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts watch order information for a specific anime via API. + * Returns recommended viewing order from the internal API endpoint. + * + * @exports + * extractWatchOrder + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import axios from "axios"; import { 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 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 }; \ No newline at end of file +export { extractWatchOrder }; + +// ══════════════════════════════════════════════════════════════ END: watchOrder.extractor.js diff --git a/src/extractors/watchPage.extractor.js b/src/extractors/watchPage.extractor.js index cd839e0..0b0cb44 100644 --- a/src/extractors/watchPage.extractor.js +++ b/src/extractors/watchPage.extractor.js @@ -1,44 +1,94 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — watchPage.extractor.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Extracts the full watch/episode page data from anikototv.to including + * anime metadata, server list, trending sidebar, and recommended anime. + * + * @exports + * extractWatchPage + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import * as cheerio from "cheerio"; import 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} 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() || ""; @@ -125,4 +180,6 @@ const extractWatchPage = async (slug, ep) => { } }; -export { extractWatchPage }; \ No newline at end of file +export { extractWatchPage }; + +// ══════════════════════════════════════════════════════════════ END: watchPage.extractor.js diff --git a/src/helper/cache.helper.js b/src/helper/cache.helper.js index 86e5812..7b344b3 100644 --- a/src/helper/cache.helper.js +++ b/src/helper/cache.helper.js @@ -1,6 +1,65 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — cache.helper.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * In-memory caching utility for storing and retrieving API responses. + * Implements TTL-based cache invalidation to prevent stale data + * while reducing unnecessary requests to the source site. + * + * @exports + * getCache, setCache, clearCache + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +// ══════════════════════════════════════════════════════════════ +// CACHE STORAGE & CONFIGURATION +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: In-memory Map-based cache storage ---- +/** + * Internal cache storage using JavaScript Map object. + * Stores data with timestamps for TTL-based expiration. + * + * @type {Map} + */ 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 }; \ No newline at end of file +export { getCache, setCache, clearCache }; +// ══════════════════════════════════════════════════════════════ END: cache.helper.js \ No newline at end of file diff --git a/src/helper/countPages.helper.js b/src/helper/countPages.helper.js index b7b1b42..dee9414 100644 --- a/src/helper/countPages.helper.js +++ b/src/helper/countPages.helper.js @@ -1,3 +1,44 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — countPages.helper.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Utility function to extract total page count from paginated + * HTML content. Parses pagination links from Cheerio DOM to + * determine the maximum page number available. + * + * @exports + * countPages + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +// ══════════════════════════════════════════════════════════════ +// PAGINATION EXTRACTION +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Extract maximum page number from pagination elements ---- +/** + * Parses HTML pagination elements to extract the total number of pages. + * Looks for numeric page links in .pagination li a elements and + * returns the highest page number found. + * + * @param {CheerioAPI} $ - Cheerio instance with loaded HTML content + * @returns {number} Maximum page number (defaults to 1 if no pagination found) + * + * @example + * // Count pages from search results + * const $ = await extractPages(searchUrl); + * const totalPages = countPages($); + * // totalPages: 5 (if pages 1-5 exist) + * + * @note + * This function assumes standard pagination HTML structure with + * numeric page links inside .pagination > li > a elements. + */ const countPages = ($) => { const pages = []; $(".pagination li a").each((i, el) => { @@ -9,4 +50,5 @@ const countPages = ($) => { return pages.length > 0 ? Math.max(...pages) : 1; }; -export { countPages }; \ No newline at end of file +export { countPages }; +// ══════════════════════════════════════════════════════════════ END: countPages.helper.js \ No newline at end of file diff --git a/src/helper/extractPages.helper.js b/src/helper/extractPages.helper.js index 677dced..967555f 100644 --- a/src/helper/extractPages.helper.js +++ b/src/helper/extractPages.helper.js @@ -1,7 +1,83 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — extractPages.helper.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * HTTP client utility for fetching and parsing HTML content from + * paginated URLs. Uses axios for HTTP requests and Cheerio for + * DOM parsing, with support for page-based pagination. + * + * @exports + * extractPages + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +// ══════════════════════════════════════════════════════════════ +// DEPENDENCIES +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Cheerio HTML parser import ---- +/** + * Cheerio library for server-side DOM manipulation. + * Used to parse and query HTML content from fetched pages. + * + * @type {module:cheerio} + */ import * as cheerio from "cheerio"; + +// ---- 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} 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; @@ -13,4 +89,5 @@ const extractPages = async (url, page = 1) => { } }; -export { extractPages }; \ No newline at end of file +export { extractPages }; +// ══════════════════════════════════════════════════════════════ END: extractPages.helper.js \ No newline at end of file diff --git a/src/helper/formatTitle.helper.js b/src/helper/formatTitle.helper.js index eb438d2..6ca7712 100644 --- a/src/helper/formatTitle.helper.js +++ b/src/helper/formatTitle.helper.js @@ -1,3 +1,55 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — formatTitle.helper.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * String utility function for normalizing anime titles into + * URL-safe slug format. Converts titles to lowercase, replaces + * special characters with hyphens, and trims leading/trailing dashes. + * + * @exports + * formatTitle + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +// ══════════════════════════════════════════════════════════════ +// TITLE FORMATTING UTILITY +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Convert title string to URL-safe slug format ---- +/** + * Formats an anime title into a URL-safe slug format. + * - Converts to lowercase + * - Replaces non-alphanumeric characters with hyphens + * - Removes leading and trailing hyphens + * + * @param {string} title - The anime title to format + * @returns {string} URL-safe slug version of the title + * + * @example + * // Basic title formatting + * formatTitle("One Piece") // "one-piece" + * + * @example + * // Title with special characters + * formatTitle(" Attack on Titan: The Final Season! ") + * // "attack-on-titan-the-final-season" + * + * @example + * // Edge cases + * formatTitle("") // "" + * formatTitle(null) // "" + * formatTitle(" --- ") // "" + * + * @note + * This function preserves alphanumeric characters and replaces + * all other characters (spaces, punctuation, symbols) with hyphens. + * Multiple consecutive special characters become a single hyphen. + */ const formatTitle = (title) => { if (!title) return ""; return title @@ -6,4 +58,5 @@ const formatTitle = (title) => { .replace(/^-+|-+$/g, ""); }; -export { formatTitle }; \ No newline at end of file +export { formatTitle }; +// ══════════════════════════════════════════════════════════════ END: formatTitle.helper.js \ No newline at end of file diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js index 3ec8eb5..cdfad7e 100644 --- a/src/routes/apiRoutes.js +++ b/src/routes/apiRoutes.js @@ -1,3 +1,21 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — apiRoutes.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Central API router that maps all Express GET endpoints to their + * corresponding controller handlers. Each route wraps its controller + * in try/catch for consistent error handling. + * + * @exports + * createApiRoutes - Function that registers all API routes on the Express app + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { getHomeInfo } from "../controllers/homeInfo.controller.js"; import { 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 }; \ No newline at end of file +export { createApiRoutes }; + +// ══════════════════════════════════════════════════════════════ END: apiRoutes.js \ No newline at end of file diff --git a/src/routes/category.route.js b/src/routes/category.route.js index 6e9cf3c..6f05ad2 100644 --- a/src/routes/category.route.js +++ b/src/routes/category.route.js @@ -1,6 +1,47 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKatoAPI — category.route.js + * Repository: https://github.com/Shineii86/AniKatoAPI + * + * @description + * Defines category-based API routes for genre, type, and status + * filtering. Each route transforms its params into a unified + * category format before delegating to the category controller. + * + * @exports + * categoryRoutes - Function that registers category routes on the Express app + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + import { getCategory } from "../controllers/category.controller.js"; +// ══════════════════════════════════════════════════════════════ +// 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 { @@ -29,4 +82,6 @@ const categoryRoutes = (app, jsonResponse, jsonError) => { }); }; -export { categoryRoutes }; \ No newline at end of file +export { categoryRoutes }; + +// ══════════════════════════════════════════════════════════════ END: category.route.js \ No newline at end of file