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 });
});
diff --git a/src/extractors/recentlyUpdatedTabs.extractor.js b/src/extractors/recentlyUpdatedTabs.extractor.js
index 4831aa4..54bc149 100644
--- a/src/extractors/recentlyUpdatedTabs.extractor.js
+++ b/src/extractors/recentlyUpdatedTabs.extractor.js
@@ -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 inside #recent-update .ani.items
+ // Each item has .ani.poster with
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;
diff --git a/src/extractors/topAnimeRankings.extractor.js b/src/extractors/topAnimeRankings.extractor.js
index 30aff9b..8f478dd 100644
--- a/src/extractors/topAnimeRankings.extractor.js
+++ b/src/extractors/topAnimeRankings.extractor.js
@@ -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 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 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;
diff --git a/src/extractors/watchPage.extractor.js b/src/extractors/watchPage.extractor.js
index 802fb0c..23e7a2a 100644
--- a/src/extractors/watchPage.extractor.js
+++ b/src/extractors/watchPage.extractor.js
@@ -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);
diff --git a/src/helper/parseListItem.helper.js b/src/helper/parseListItem.helper.js
index fc88e7e..d87fe50 100644
--- a/src/helper/parseListItem.helper.js
+++ b/src/helper/parseListItem.helper.js
@@ -41,17 +41,24 @@ 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() || "";
+ // NOTE: item may BE the 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(".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 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(".info .meta .m-item:nth-child(2) label, .meta .inner .right, .fdi-item:nth-child(2), .type").text().trim() || "";
+ 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 };
@@ -62,7 +69,7 @@ const parseListItem = ($, el, options = {}) => {
// 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() || "";
+ item.rating = $(el).find(".rated span, .rating, .fdi-item:nth-child(3)").text().trim() || "";
}
return item;
diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js
index a20eb1c..e073964 100644
--- a/src/routes/apiRoutes.js
+++ b/src/routes/apiRoutes.js
@@ -48,6 +48,7 @@ import { getRecentlyUpdatedTabs } from "../controllers/recentlyUpdatedTabs.contr
import { getCompletedAnime } from "../controllers/completedAnime.controller.js";
import { categoryRoutes } from "./category.route.js";
import { getMirrorStatus, resetMirrorCache } from "../helper/mirror.helper.js";
+import { getCacheStats } from "../helper/cache.helper.js";
// ══════════════════════════════════════════════════════════════
// ROUTE REGISTRATION
@@ -490,6 +491,12 @@ app.get("/api/mirrors", async (req, res) => {
}
});
+// ---- FEATURE: Cache statistics ----
+app.get("/api/cache/stats", (req, res) => {
+ const stats = getCacheStats();
+ res.json({ success: true, results: stats });
+});
+
// ---- FEATURE: Reset mirror cache ----
app.post("/api/mirrors/reset", (req, res) => {
resetMirrorCache();