Files
AniKotoAPI/src/helper/parseListItem.helper.js
T
Shinei Nouzen d0432e1654 fix: Bug fixes from full API diagnostic sweep — all 33 endpoints now 200 OK
- Fix /api/watch 500: correct URL path format (slug/ep-N -> slug/ep-N)
- Fix /api/info: accept both ?id= and ?slug= query params
- Fix /api/status/airing 500: map short status names (ongoing/airing/completed) to correct slugs
- Fix /api/top-rankings: correct selectors (#top-anime .scaff.side.items a.item) + extract rank from rankN class
- Fix /api/completed: correct selectors (section.top-table[data-name='completed'] a.item)
- Fix /api/recently-updated: correct selectors (#recent-update .ani.items .item) + client-side tab filtering
- Fix parseListItem: detect self-as-anchor (<a> elements) + fallback slug extraction
- Fix category extractor: add status name mapping (ongoing->currently-airing, etc.)
- Add /api/cache/stats endpoint for LRU cache diagnostics
- 33 endpoints verified: 0 errors, 0 timeouts
2026-07-28 20:17:58 +05:30

112 lines
5.0 KiB
JavaScript

/*
* ======= • ======= • ======= • ======= • =======• =======
* 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
// NOTE: item may BE the <a> tag itself, so check both self and children
let slug = "";
if ($(el).is("a")) {
slug = $(el).attr("href")?.split("/watch/").pop() || "";
}
if (!slug) {
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("img").attr("src") || "";
const title = $(el).find(".name").text().trim() || $(el).children(".info").find(".name").text().trim() || "";
const japaneseTitle = $(el).find(".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(".meta .dot:last-child, .fdi-item:nth-child(2)").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(".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