mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 19:05:11 +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.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: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user