mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
fix: episodes endpoint accepts numeric ID, episodes-ajax returns parsed JSON
This commit is contained in:
@@ -39,18 +39,24 @@ import { URLS, BASE_URL } from "../configs/dataUrl.js";
|
||||
* console.log(episodeData.totalEpisodes); // e.g. 1100+
|
||||
* console.log(episodeData.episodes[0].episode_no); // 1
|
||||
*/
|
||||
const extractEpisodeList = async (slug) => {
|
||||
const extractEpisodeList = async (slugOrId) => {
|
||||
try {
|
||||
// NOTE: First fetch the watch page to extract the internal anime ID
|
||||
const infoUrl = URLS.watch(slug);
|
||||
const { data: infoData } = await axios.get(infoUrl, { headers });
|
||||
const $ = cheerio.load(infoData);
|
||||
let animeId = 0;
|
||||
let resolvedSlug = slugOrId;
|
||||
|
||||
// NOTE: data-id on #watch-main is the anime ID used for the AJAX episode list call
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
// NOTE: Accept either numeric anime ID or slug — if numeric, skip watch page fetch
|
||||
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) {
|
||||
return { animeId: 0, slug, totalEpisodes: 0, episodes: [] };
|
||||
return { animeId: 0, slug: resolvedSlug, totalEpisodes: 0, episodes: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -98,7 +104,7 @@ const extractEpisodeList = async (slug) => {
|
||||
|
||||
return {
|
||||
animeId,
|
||||
slug,
|
||||
slug: resolvedSlug,
|
||||
totalEpisodes: episodes.length,
|
||||
episodes
|
||||
};
|
||||
@@ -106,7 +112,7 @@ const extractEpisodeList = async (slug) => {
|
||||
// NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue)
|
||||
return {
|
||||
animeId,
|
||||
slug,
|
||||
slug: resolvedSlug,
|
||||
totalEpisodes: 0,
|
||||
episodes: []
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { BASE_URL } from "../configs/dataUrl.js";
|
||||
@@ -24,43 +25,91 @@ import { BASE_URL } from "../configs/dataUrl.js";
|
||||
// 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.
|
||||
* Returns the raw HTML response from the server — does not parse or
|
||||
* transform the data. Use this when you need the raw response or want
|
||||
* to apply custom style/VRF parameters.
|
||||
* Queries the AJAX episode list endpoint for a given anime ID.
|
||||
* Parses the HTML response and returns structured episode data
|
||||
* with server IDs, timestamps, and MAL IDs.
|
||||
*
|
||||
* @param {number|string} animeId - The internal anime ID from anikototv.to
|
||||
* @param {string} [style=""] - Optional style parameter for episode list rendering
|
||||
* @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
|
||||
* const rawHtml = await extractEpisodeListAjax(12345);
|
||||
* console.log(rawHtml); // HTML fragment with episode links
|
||||
*
|
||||
* @example
|
||||
* const rawHtml = await extractEpisodeListAjax(12345, "modern", "abc123");
|
||||
* const data = await extractEpisodeListAjax(7457);
|
||||
* console.log(data.episodes[0].server_ids); // base64-encoded server IDs
|
||||
*/
|
||||
const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
|
||||
try {
|
||||
let url = `${BASE_URL}/ajax/episode/list/${animeId}`;
|
||||
// NOTE: Optional params are appended only if non-empty to avoid trailing ?/&
|
||||
const params = [];
|
||||
if (style) params.push(`style=${style}`);
|
||||
if (vrf) params.push(`vrf=${vrf}`);
|
||||
if (params.length) url += `?${params.join("&")}`;
|
||||
|
||||
// NOTE: X-Requested-With header is required — without it the server returns the full page
|
||||
const { data } = await axios.get(url, {
|
||||
const { data: raw } = await axios.get(url, {
|
||||
headers: {
|
||||
...headers,
|
||||
"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) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user