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
This commit is contained in:
Shinei Nouzen
2026-07-28 19:28:21 +05:30
parent 7f07833c14
commit a8f1917ce7
22 changed files with 269 additions and 418 deletions
+104
View File
@@ -0,0 +1,104 @@
/*
* ======= • ======= • ======= • ======= • =======• =======
* AniKotoAPI — parseListItem.helper.js
* Repository: https://github.com/Shineii86/AniKotoAPI
*
* @description
* Shared helper for parsing anime list items from Cheerio DOM.
* Eliminates code duplication across multiple extractors by
* providing a unified parsing function for standard list items.
*
* @exports
* parseListItems, parseListItem
*
* @author Shinei Nouzen
* @license MIT
* ======= • ======= • ======= • ======= • =======• =======
*/
// ══════════════════════════════════════════════════════════════
// SINGLE ITEM PARSER
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Parse a single anime list item from a Cheerio element ----
/**
* Parses a single anime list item from a Cheerio element.
* Handles multiple CSS selector patterns for different page layouts
* by falling through comma-separated selectors until one matches.
*
* @param {CheerioAPI} $ - Cheerio instance with loaded HTML
* @param {CheerioElement} el - DOM element to parse
* @param {object} [options] - Parsing options
* @param {boolean} [options.includeAnimeId=false] - Include animeId field from data-tip attribute
* @param {boolean} [options.includeRating=true] - Include rating field from .rated span
* @returns {object|null} Parsed anime object or null if slug is missing (invalid item)
*
* @example
* const item = parseListItem($, el, { includeAnimeId: true });
* if (item) results.push(item);
*/
const parseListItem = ($, el, options = {}) => {
const { includeAnimeId = false, includeRating = true } = options;
// NOTE: slug is the primary key — skip items without a valid watch link
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
if (!slug) return null;
// NOTE: Multiple selectors handle different page layouts (#list-items vs .film_list-wrap vs .item)
const poster = $(el).find(".ani.poster.tip > a > img, img").attr("src") || "";
const title = $(el).find(".info .b1 a.name.d-title, .film-name a, .name.d-title, a.name").text().trim() || "";
const japaneseTitle = $(el).find(".info .b1 a.name.d-title, .name.d-title, a.name").attr("data-jp") || "";
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;
const type = $(el).find(".info .meta .m-item:nth-child(2) label, .meta .inner .right, .fdi-item:nth-child(2), .type").text().trim() || "";
const item = { slug, poster, title, japaneseTitle, sub, dub, total, type };
// NOTE: animeId comes from data-tip on .ani.poster.tip — only present on some page layouts
if (includeAnimeId) {
item.animeId = $(el).find(".ani.poster.tip").attr("data-tip") || "";
}
// NOTE: Rating uses different selectors depending on whether it's a list page or detail page
if (includeRating) {
item.rating = $(el).find(".info .meta .m-item.rated span, .rating, .fdi-item:nth-child(3)").text().trim() || "";
}
return item;
};
// ══════════════════════════════════════════════════════════════
// MULTI-ITEM PARSER
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Parse multiple anime list items from a Cheerio instance ----
/**
* Parses multiple anime list items from a Cheerio instance.
* Uses multiple CSS selectors to handle different page layouts,
* returning only items with valid slugs.
*
* @param {CheerioAPI} $ - Cheerio instance with loaded HTML
* @param {string} [selector] - CSS selector for list items (default: auto-detect all layouts)
* @param {object} [options] - Parsing options (passed to parseListItem)
* @returns {Array<object>} Array of parsed anime objects (empty if none found)
*
* @example
* const $ = await extractPages("/most-popular");
* const items = parseListItems($);
* console.log(items.length); // Number of parsed items
*/
const parseListItems = ($, selector = "#list-items > .item, .film_list-wrap .flw-item, .film-detail, .item", options = {}) => {
const results = [];
$(selector).each((i, el) => {
const item = parseListItem($, el, options);
if (item) results.push(item);
});
return results;
};
export { parseListItems, parseListItem };
// ══════════════════════════════════════════════════════════════ END: parseListItem.helper.js