mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 19:05:11 +00:00
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
116 lines
4.1 KiB
JavaScript
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
|