mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 19:05:11 +00:00
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
This commit is contained in:
@@ -60,7 +60,17 @@ const extractCategory = async (category, page = 1) => {
|
||||
} else if (category.startsWith("type/")) {
|
||||
url = URLS.type(category.replace("type/", ""));
|
||||
} else if (category.startsWith("status/")) {
|
||||
url = URLS.status(category.replace("status/", ""));
|
||||
// NOTE: Map short status names to full slugs used by the site
|
||||
const statusSlug = category.replace("status/", "");
|
||||
const statusMap = {
|
||||
ongoing: "currently-airing",
|
||||
airing: "currently-airing",
|
||||
completed: "finished-airing",
|
||||
finished: "finished-airing",
|
||||
upcoming: "not-yet-aired"
|
||||
};
|
||||
const mapped = statusMap[statusSlug] || statusSlug;
|
||||
url = URLS.status(mapped);
|
||||
} else {
|
||||
url = `${URLS.home}/${category}`;
|
||||
}
|
||||
|
||||
@@ -49,18 +49,19 @@ const extractCompletedAnime = async () => {
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: completed section contains finished anime series
|
||||
$(".top-table[data-name='completed'] .item, #completed .item, .completed-list .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
// NOTE: completed section is inside .top-tables with data-name="completed"
|
||||
// Items are <a class="item"> inside .scaff.items
|
||||
$("section.top-table[data-name='completed'] .scaff.items a.item, .top-table[data-name='completed'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
if (!slug) return;
|
||||
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
const title = $(el).find(".film-name a, .name.d-title, a.name").text().trim() || "";
|
||||
const japaneseTitle = $(el).find(".name.d-title, a.name").attr("data-jp") || "";
|
||||
const title = $(el).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 .inner .right, .type, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
const type = $(el).find(".meta .dot:last-child").text().trim() || "";
|
||||
|
||||
results.push({ slug, poster, title, japaneseTitle, sub, dub, total, type });
|
||||
});
|
||||
|
||||
@@ -53,28 +53,34 @@ const extractRecentlyUpdatedTabs = async (tab = "all") => {
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Homepage has tabbed sections: updated-all, updated-dub, updated-sub
|
||||
// Each section has data-name attribute matching the tab name
|
||||
const selector = tab === "dub"
|
||||
? ".top-table[data-name='updated-dub'] .item, #updated-dub .item"
|
||||
: tab === "sub"
|
||||
? ".top-table[data-name='updated-sub'] .item, #updated-sub .item"
|
||||
: ".top-table[data-name='updated-all'] .item, #recent-update .item, .section-updated .item";
|
||||
// NOTE: #recent-update has tabs (all/dub/sub/trending/random) but filtering is client-side
|
||||
// Items are <div class="item"> inside #recent-update .ani.items
|
||||
// Each item has .ani.poster with <a> link and .info .name.d-title
|
||||
const allItems = $("#recent-update .ani.items .item");
|
||||
|
||||
$(selector).each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
allItems.each((i, el) => {
|
||||
// NOTE: The item itself is a div, the link is inside .ani.poster a or .info a.name
|
||||
const link = $(el).find(".ani.poster a").attr("href") || $(el).find(".info a.name").attr("href") || "";
|
||||
const slug = link.split("/watch/").pop()?.split("/ep-")[0] || "";
|
||||
if (!slug) return;
|
||||
|
||||
// NOTE: Check sub/dub status for tab filtering
|
||||
const hasSub = $(el).find(".ep-status.sub").length > 0;
|
||||
const hasDub = $(el).find(".ep-status.dub").length > 0;
|
||||
|
||||
// NOTE: Filter by tab if not "all" — since server-side rendering shows all items
|
||||
if (tab === "dub" && !hasDub) return;
|
||||
if (tab === "sub" && !hasSub) return;
|
||||
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
const title = $(el).find(".film-name a, .name.d-title, a.name").text().trim() || "";
|
||||
const japaneseTitle = $(el).find(".name.d-title, a.name").attr("data-jp") || "";
|
||||
const title = $(el).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 .inner .right, .type, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
const episodeInfo = $(el).find(".episode-info, .ep-info").text().trim() || "";
|
||||
const type = $(el).find(".meta .right, .type").text().trim() || "";
|
||||
|
||||
results.push({ slug, poster, title, japaneseTitle, sub, dub, total, type, episodeInfo });
|
||||
results.push({ slug, poster, title, japaneseTitle, sub, dub, total, type });
|
||||
});
|
||||
|
||||
return results;
|
||||
|
||||
@@ -56,25 +56,29 @@ const extractTopAnimeRankings = async (sort = "top") => {
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Ranking items have rank1 through rank9 classes for position
|
||||
// The data-sort attribute on tabs controls which ranking is displayed
|
||||
// We parse all ranked items and return them with position
|
||||
$("#top-anime .item, .top-anime .item, .top-table[data-name='top'] .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
// NOTE: Ranking items live inside #top-anime .tab-content .scaff.side.items
|
||||
// Each item is an <a class="item rankN"> with rank classes rank1..rank9
|
||||
// The data-name on .tab-content controls which tab: day/week/month
|
||||
const tabSelector = sort === "week" ? "[data-name='week']"
|
||||
: sort === "month" ? "[data-name='month']"
|
||||
: "[data-name='day']";
|
||||
|
||||
$(`#top-anime .tab-content${tabSelector} a.item, #top-anime .scaff.side.items a.item`).each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
if (!slug) return;
|
||||
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
const title = $(el).find(".name, .film-name a").text().trim() || "";
|
||||
const title = $(el).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 type = $(el).find(".type").text().trim() || "";
|
||||
const views = $(el).find(".views, .view-count").text().trim() || "";
|
||||
const type = $(el).find(".meta .dot:last-child").text().trim() || "";
|
||||
|
||||
// NOTE: Rank is determined by position in the list (1-indexed)
|
||||
const rank = i + 1;
|
||||
// NOTE: Rank is extracted from the rankN class on the <a> element
|
||||
const rankClass = [...$(el).attr("class") || ""].join(" ").match(/rank(\d+)/);
|
||||
const rank = rankClass ? parseInt(rankClass[1]) : i + 1;
|
||||
|
||||
results.push({ rank, slug, poster, title, japaneseTitle, sub, dub, type, views });
|
||||
results.push({ rank, slug, poster, title, japaneseTitle, sub, dub, type });
|
||||
});
|
||||
|
||||
return results;
|
||||
|
||||
@@ -40,7 +40,7 @@ import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
*/
|
||||
const extractWatchPage = async (slug, ep) => {
|
||||
try {
|
||||
const path = `/watch/${slug}/${ep}`;
|
||||
const path = `/watch/${slug}/ep-${ep}`;
|
||||
const { data } = await fetchWithMirror(path);
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user