Files
AniKotoAPI/src/extractors/episodeList.extractor.js
T
Shinei Nouzen a8f1917ce7 feat: v2.2.0 — Modularize codebase, unify mirror fallback, harden security
Major Changes:
- Add shared parseListItem.helper.js for unified list parsing (DRY)
- Migrate all 22 extractors to use fetchWithMirror() for consistent mirror fallback
- Remove unused cors dependency (custom CORS middleware handles it)
- Fix middleware ordering: timeout now registered before routes
- Fix CORS to allow POST for /api/mirrors/reset endpoint
- Remove redundant cache check in homeInfo extractor

New Features:
- Request ID tracking (X-Request-Id header) for debugging
- Graceful shutdown handling (SIGTERM/SIGINT) with 10s timeout
- CSP and HSTS security headers
- Rate limiter cleanup interval (every 5 min) to prevent memory leak
- Request body size limits (10kb) via express.json()

Security:
- Content-Security-Policy header for XSS protection
- Strict-Transport-Security for HTTPS enforcement
- X-Permitted-Cross-Domain-Policies: none
- Body size limits to prevent large payload attacks

Code Quality:
- All files follow strict documentation style (box headers, section separators,
  feature markers, JSDoc params/returns, inline notes, module footers)
- Eliminated ~200 lines of duplicated code across 8+ extractors
- Updated CHANGELOG.md with comprehensive change history
2026-07-28 19:28:21 +05:30

116 lines
4.1 KiB
JavaScript

/*
* ======= • ======= • ======= • ======= • =======• =======
* AniKotoAPI — episodeList.extractor.js
* Repository: https://github.com/Shineii86/AniKotoAPI
*
* @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 { headers } from "../configs/header.config.js";
import { URLS, BASE_URL } from "../configs/dataUrl.js";
import { fetchWithMirror } from "../helper/mirror.helper.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} slugOrId - The anime slug or numeric ID (e.g. "naruto-shippuden" or "12345")
* @returns {Promise<Object>} Object with animeId, slug, totalEpisodes, and episodes array
*
* @example
* const episodeData = await extractEpisodeList("one-piece");
* console.log(episodeData.totalEpisodes); // e.g. 1100+
* console.log(episodeData.episodes[0].episode_no); // 1
*/
const extractEpisodeList = async (slugOrId) => {
try {
let animeId = 0;
let resolvedSlug = slugOrId;
if (/^\d+$/.test(slugOrId)) {
animeId = parseInt(slugOrId);
} else {
const path = `/watch/${slugOrId}`;
const { data: infoData } = await fetchWithMirror(path);
const $ = cheerio.load(infoData);
animeId = parseInt($("#watch-main").attr("data-id")) || 0;
}
if (!animeId) {
return { animeId: 0, slug: slugOrId, totalEpisodes: 0, episodes: [] };
}
try {
const ajaxPath = `/ajax/episode/list/${animeId}`;
const { data: ajaxRaw } = await fetchWithMirror(ajaxPath, {
headers: { "X-Requested-With": "XMLHttpRequest" }
});
const ajaxHtml = typeof ajaxRaw === "string" ? ajaxRaw : (ajaxRaw?.result || "");
const $ep = cheerio.load(ajaxHtml);
const episodes = [];
$ep("a[data-num], a[data-ep-id], a[data-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") || "";
const epTitle = $ep(el).find(".ep-title, .ep-name").text().trim() || "";
const isActive = $ep(el).hasClass("active") || false;
const serverIds = $ep(el).attr("data-ids") || "";
const timestamp = $ep(el).attr("data-timestamp") || "";
const malId = $ep(el).attr("data-mal") || "";
episodes.push({
id: epId,
episode_no: epNum,
slug: epSlug,
title: epTitle,
active: isActive,
href,
server_ids: serverIds,
timestamp,
mal_id: malId
});
});
return {
animeId,
slug: slugOrId,
totalEpisodes: episodes.length,
episodes
};
} catch (ajaxError) {
return {
animeId,
slug: slugOrId,
totalEpisodes: 0,
episodes: []
};
}
} catch (error) {
throw error;
}
};
export { extractEpisodeList };
// ══════════════════════════════════════════════════════════════ END: episodeList.extractor.js