fix: episodes endpoint accepts numeric ID, episodes-ajax returns parsed JSON

This commit is contained in:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-09 04:55:26 +05:30
parent e203761789
commit 49243ccca1
2 changed files with 80 additions and 25 deletions
+16 -10
View File
@@ -39,18 +39,24 @@ import { URLS, BASE_URL } from "../configs/dataUrl.js";
* console.log(episodeData.totalEpisodes); // e.g. 1100+ * console.log(episodeData.totalEpisodes); // e.g. 1100+
* console.log(episodeData.episodes[0].episode_no); // 1 * console.log(episodeData.episodes[0].episode_no); // 1
*/ */
const extractEpisodeList = async (slug) => { const extractEpisodeList = async (slugOrId) => {
try { try {
// NOTE: First fetch the watch page to extract the internal anime ID let animeId = 0;
const infoUrl = URLS.watch(slug); let resolvedSlug = slugOrId;
const { data: infoData } = await axios.get(infoUrl, { headers });
const $ = cheerio.load(infoData);
// NOTE: data-id on #watch-main is the anime ID used for the AJAX episode list call // NOTE: Accept either numeric anime ID or slug — if numeric, skip watch page fetch
const animeId = parseInt($("#watch-main").attr("data-id")) || 0; if (/^\d+$/.test(slugOrId)) {
animeId = parseInt(slugOrId);
} else {
// NOTE: Fetch the watch page to extract the internal anime ID from the slug
const infoUrl = URLS.watch(slugOrId);
const { data: infoData } = await axios.get(infoUrl, { headers });
const $ = cheerio.load(infoData);
animeId = parseInt($("#watch-main").attr("data-id")) || 0;
}
if (!animeId) { if (!animeId) {
return { animeId: 0, slug, totalEpisodes: 0, episodes: [] }; return { animeId: 0, slug: resolvedSlug, totalEpisodes: 0, episodes: [] };
} }
try { try {
@@ -98,7 +104,7 @@ const extractEpisodeList = async (slug) => {
return { return {
animeId, animeId,
slug, slug: resolvedSlug,
totalEpisodes: episodes.length, totalEpisodes: episodes.length,
episodes episodes
}; };
@@ -106,7 +112,7 @@ const extractEpisodeList = async (slug) => {
// NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue) // NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue)
return { return {
animeId, animeId,
slug, slug: resolvedSlug,
totalEpisodes: 0, totalEpisodes: 0,
episodes: [] episodes: []
}; };
+64 -15
View File
@@ -16,6 +16,7 @@
* ======= • ======= • ======= • ======= • =======• ======= * ======= • ======= • ======= • ======= • =======• =======
*/ */
import * as cheerio from "cheerio";
import axios from "axios"; import axios from "axios";
import { headers } from "../configs/header.config.js"; import { headers } from "../configs/header.config.js";
import { BASE_URL } from "../configs/dataUrl.js"; import { BASE_URL } from "../configs/dataUrl.js";
@@ -24,43 +25,91 @@ import { BASE_URL } from "../configs/dataUrl.js";
// EPISODE LIST AJAX EXTRACTOR // EPISODE LIST AJAX EXTRACTOR
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
// ---- FEATURE: Fetch raw episode list HTML via AJAX with optional params ---- // ---- FEATURE: Fetch and parse episode list HTML via AJAX ----
/** /**
* Directly queries the AJAX episode list endpoint for a given anime ID. * Queries the AJAX episode list endpoint for a given anime ID.
* Returns the raw HTML response from the server — does not parse or * Parses the HTML response and returns structured episode data
* transform the data. Use this when you need the raw response or want * with server IDs, timestamps, and MAL IDs.
* to apply custom style/VRF parameters.
* *
* @param {number|string} animeId - The internal anime ID from anikototv.to * @param {number|string} animeId - The internal anime ID from anikototv.to
* @param {string} [style=""] - Optional style parameter for episode list rendering * @param {string} [style=""] - Optional style parameter for episode list rendering
* @param {string} [vrf=""] - Optional VRF parameter (verification/decryption key) * @param {string} [vrf=""] - Optional VRF parameter (verification/decryption key)
* @returns {Promise<string>} Raw HTML response from the AJAX endpoint * @returns {Promise<Object>} Parsed episode data with filters, ranges, and episodes array
* *
* @example * @example
* const rawHtml = await extractEpisodeListAjax(12345); * const data = await extractEpisodeListAjax(7457);
* console.log(rawHtml); // HTML fragment with episode links * console.log(data.episodes[0].server_ids); // base64-encoded server IDs
*
* @example
* const rawHtml = await extractEpisodeListAjax(12345, "modern", "abc123");
*/ */
const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => { const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
try { try {
let url = `${BASE_URL}/ajax/episode/list/${animeId}`; let url = `${BASE_URL}/ajax/episode/list/${animeId}`;
// NOTE: Optional params are appended only if non-empty to avoid trailing ?/&
const params = []; const params = [];
if (style) params.push(`style=${style}`); if (style) params.push(`style=${style}`);
if (vrf) params.push(`vrf=${vrf}`); if (vrf) params.push(`vrf=${vrf}`);
if (params.length) url += `?${params.join("&")}`; if (params.length) url += `?${params.join("&")}`;
// NOTE: X-Requested-With header is required — without it the server returns the full page const { data: raw } = await axios.get(url, {
const { data } = await axios.get(url, {
headers: { headers: {
...headers, ...headers,
"X-Requested-With": "XMLHttpRequest" "X-Requested-With": "XMLHttpRequest"
} }
}); });
return data; // NOTE: Response is JSON {status, result} where result is HTML
const html = typeof raw === "string" ? raw : (raw?.result || "");
const $ = cheerio.load(html);
// NOTE: Extract filter options (Sub/Dub, episode ranges)
const filters = [];
$(".dropdown.filter").each((_, el) => {
const filterName = $(el).find("button").text().trim();
const options = [];
$(el).find(".dropdown-item").each((__, item) => {
options.push({
value: $(item).attr("data-value") || "",
label: $(item).text().trim()
});
});
filters.push({ name: filterName, options });
});
// NOTE: Extract episode ranges
const ranges = [];
$(".ep-range").each((_, el) => {
ranges.push($(el).attr("data-range") || "");
});
// NOTE: Parse episodes from list items
const episodes = [];
$("li[data-html]").each((_, li) => {
const a = $(li).find("a").first();
if (!a.length) return;
const title = $(li).attr("title") || "";
const jpTitle = a.find(".d-title").attr("data-jp") || "";
episodes.push({
id: a.attr("data-id") || "",
episode_no: parseInt(a.attr("data-num")) || 0,
slug: a.attr("data-slug") || "",
title,
jp_title: jpTitle,
mal_id: a.attr("data-mal") || "",
timestamp: a.attr("data-timestamp") || "",
has_sub: a.attr("data-sub") === "1",
has_dub: a.attr("data-dub") === "1",
server_ids: a.attr("data-ids") || "",
active: a.hasClass("active") || false
});
});
return {
animeId,
filters,
ranges,
totalEpisodes: episodes.length,
episodes
};
} catch (error) { } catch (error) {
throw error; throw error;
} }