From e0e348682f097f7ffe5388c1d4903bb1375d981a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=C9=B4=E1=B4=87=C9=AA=20N=E1=B4=8F=E1=B4=9C?= =?UTF-8?q?=E1=B4=A2=E1=B4=87=C9=B4?= <157171073+Shineii86@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:51:05 +0530 Subject: [PATCH] fix: streaming endpoints - parse JSON responses, extract server HTML - episodeList: Parse AJAX JSON response, extract server_ids/timestamp/mal_id - streamInfo: Parse server list HTML into structured server objects - streamInfo: Parse stream URL from JSON response properly - Added cheerio import to streamInfo extractor - Servers now return: type, link_id, cmid, sv_id, ep_id, name --- src/extractors/episodeList.extractor.js | 20 ++++++++++----- src/extractors/streamInfo.extractor.js | 33 +++++++++++++++++++++---- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/extractors/episodeList.extractor.js b/src/extractors/episodeList.extractor.js index 0e2a0a2..8fee0e7 100644 --- a/src/extractors/episodeList.extractor.js +++ b/src/extractors/episodeList.extractor.js @@ -56,27 +56,32 @@ const extractEpisodeList = async (slug) => { try { // NOTE: AJAX endpoint requires X-Requested-With header to return HTML fragment const ajaxUrl = `${BASE_URL}/ajax/episode/list/${animeId}`; - const { data: ajaxData } = await axios.get(ajaxUrl, { + const { data: ajaxRaw } = await axios.get(ajaxUrl, { headers: { ...headers, "X-Requested-With": "XMLHttpRequest" } }); - const $ep = cheerio.load(ajaxData); + // NOTE: AJAX response is JSON {status, result} where result is HTML + const ajaxHtml = typeof ajaxRaw === "string" ? ajaxRaw : (ajaxRaw?.result || ""); + const $ep = cheerio.load(ajaxHtml); // ---- FEATURE: Parse episode list from AJAX response HTML ---- // NOTE: Episode links have either data-num or data-ep-id attributes const episodes = []; - $ep("a[data-num], a[data-ep-id]").each((i, el) => { + $ep("a[data-num], a[data-ep-id], a[data-id]").each((i, el) => { const epId = $ep(el).attr("data-ep-id") || $ep(el).attr("data-id") || ""; const epNum = parseInt($ep(el).attr("data-num")) || i + 1; const epSlug = $ep(el).attr("data-slug") || ""; const href = $ep(el).attr("href") || ""; - // NOTE: Episode title is in .ep-title or .ep-name span const epTitle = $ep(el).find(".ep-title, .ep-name").text().trim() || ""; - // NOTE: Active class indicates the currently-playing episode const isActive = $ep(el).hasClass("active") || false; + // NOTE: data-ids is the base64-encoded server ID string needed for /api/servers + const serverIds = $ep(el).attr("data-ids") || ""; + // NOTE: data-timestamp used for mapper API + const timestamp = $ep(el).attr("data-timestamp") || ""; + const malId = $ep(el).attr("data-mal") || ""; episodes.push({ id: epId, @@ -84,7 +89,10 @@ const extractEpisodeList = async (slug) => { slug: epSlug, title: epTitle, active: isActive, - href + href, + server_ids: serverIds, + timestamp, + mal_id: malId }); }); diff --git a/src/extractors/streamInfo.extractor.js b/src/extractors/streamInfo.extractor.js index 86a104f..ba7d585 100644 --- a/src/extractors/streamInfo.extractor.js +++ b/src/extractors/streamInfo.extractor.js @@ -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"; @@ -40,7 +41,6 @@ import { BASE_URL } from "../configs/dataUrl.js"; const extractStreamInfo = async (linkId) => { try { const url = `${BASE_URL}/ajax/server?get=${linkId}`; - // NOTE: X-Requested-With header is required for AJAX endpoints on anikototv.to const { data } = await axios.get(url, { headers: { ...headers, @@ -48,14 +48,17 @@ const extractStreamInfo = async (linkId) => { } }); + // NOTE: Response is JSON {status, result: {url, skip_data}} if (!data || !data.result) { return { linkId, url: null, skipData: null }; } + const result = typeof data.result === "string" ? (() => { try { return JSON.parse(data.result); } catch { return {}; } })() : data.result; + return { linkId, - url: data.result.url || null, - skipData: data.result.skip_data || null + url: result.url || null, + skipData: result.skip_data || null }; } catch (error) { throw error; @@ -81,14 +84,34 @@ const extractStreamInfo = async (linkId) => { const extractServerList = async (episodeIds) => { try { const url = `${BASE_URL}/ajax/server/list?servers=${episodeIds}`; - 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); + + const servers = []; + // NOTE: Servers are in .servers div, grouped by type (sub/dub) + $(".servers .type").each((_, typeEl) => { + const type = $(typeEl).attr("data-type") || "sub"; + $(typeEl).find("li[data-link-id]").each((__, li) => { + servers.push({ + type, + ep_id: $(li).attr("data-ep-id") || "", + link_id: $(li).attr("data-link-id") || "", + cmid: $(li).attr("data-cmid") || "", + sv_id: $(li).attr("data-sv-id") || "", + name: $(li).text().trim() || "" + }); + }); + }); + + return servers; } catch (error) { throw error; }