mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
fix: add cookie-aware session establishment for stream AJAX endpoints
Root cause: upstream /ajax/server and /ajax/server/list endpoints require session cookies set by visiting the watch page. fetchWithMirror() didn't persist cookies between requests, causing upstream to return 500. - Extract cookies from set-cookie headers when fetching watch/home page - Pass cookies with AJAX requests for stream info and server list - Accept optional slug query param in /api/stream for targeted session - Both extractors now use cookie-aware axios for session establishment
This commit is contained in:
+5
-5
@@ -8,12 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [2.2.2] - 2026-08-14
|
## [2.2.2] - 2026-08-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **CRITICAL**: Fixed `/api/stream` returning `null` URL when upstream API returns error responses (e.g. `{status: 500, result: "Bad request"}`) — extractor now detects upstream error status codes and throws meaningful errors instead of silently returning null
|
- **CRITICAL**: Fixed `/api/stream` returning `null` URL — root cause was AJAX endpoints (`/ajax/server?get=`, `/ajax/server/list?servers=`) require session cookies from the watch page; `fetchWithMirror` didn't persist cookies between requests, causing upstream to return `{status:500, result:"Bad request"}`
|
||||||
- Fixed `streamInfo.extractor.js` `extractStreamInfo` to detect upstream API error responses (`data.status >= 400`) and throw descriptive errors
|
- Fixed `streamInfo.extractor.js` `extractStreamInfo` to establish session with cookies before AJAX requests — fetches watch page or home page to capture `set-cookie` headers and passes them with the AJAX request
|
||||||
- Fixed `streamInfo.extractor.js` `extractStreamInfo` to detect non-JSON error strings from upstream API (e.g. "Bad request") and throw errors instead of silently swallowing them
|
- Fixed `streamInfo.extractor.js` `extractServerList` to establish session with cookies before AJAX requests (same cookie-aware pattern)
|
||||||
- Fixed `streamInfo.extractor.js` `extractServerList` to detect upstream API error responses and throw descriptive errors
|
- Fixed `streamInfo.controller.js` `getStreamInfo` to accept optional `slug` query parameter and pass it to extractor for session establishment
|
||||||
- Fixed `streamInfo.controller.js` `getStreamInfo` to return HTTP 502 (Bad Gateway) with error message for upstream API failures instead of silently returning null URL
|
- Fixed `streamInfo.controller.js` `getStreamInfo` to return HTTP 502 (Bad Gateway) with error message for upstream API failures instead of silently returning null URL
|
||||||
- Fixed `streamInfo.controller.js` `getServerList` to properly propagate upstream errors via error handler
|
- Fixed upstream error detection in both extractors — detects `status >= 400` and non-JSON error strings from upstream API
|
||||||
|
|
||||||
## [2.2.1] - 2026-08-12
|
## [2.2.1] - 2026-08-12
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
|||||||
*/
|
*/
|
||||||
const getStreamInfo = async (req, res, next) => {
|
const getStreamInfo = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.query;
|
const { id, slug } = req.query;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return res.status(400).json({ success: false, message: "Link ID is required" });
|
return res.status(400).json({ success: false, message: "Link ID is required" });
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ const getStreamInfo = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await extractStreamInfo(id);
|
data = await extractStreamInfo(id, slug);
|
||||||
} catch (upstreamError) {
|
} catch (upstreamError) {
|
||||||
return res.status(502).json({ success: false, message: `Upstream API error: ${upstreamError.message}` });
|
return res.status(502).json({ success: false, message: `Upstream API error: ${upstreamError.message}` });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,11 @@
|
|||||||
* ======= • ======= • ======= • ======= • =======• =======
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
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";
|
||||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
import { fetchWithMirror, getWorkingMirror } from "../helper/mirror.helper.js";
|
||||||
import { normalizeServerName } from "./streamResolver.extractor.js";
|
import { normalizeServerName } from "./streamResolver.extractor.js";
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -43,14 +44,48 @@ import { normalizeServerName } from "./streamResolver.extractor.js";
|
|||||||
*/
|
*/
|
||||||
const extractStreamInfo = async (linkId, watchSlug = null) => {
|
const extractStreamInfo = async (linkId, watchSlug = null) => {
|
||||||
try {
|
try {
|
||||||
// NOTE: Establish session by visiting watch page first (required for AJAX endpoints)
|
// NOTE: Establish session with cookies — AJAX endpoints require session cookies
|
||||||
|
const mirror = await getWorkingMirror();
|
||||||
|
let cookieHeader = "";
|
||||||
|
|
||||||
if (watchSlug) {
|
if (watchSlug) {
|
||||||
await fetchWithMirror(`/watch/${watchSlug}`);
|
try {
|
||||||
|
const sessionRes = await axios.get(`${mirror}/watch/${watchSlug}`, {
|
||||||
|
headers,
|
||||||
|
timeout: 10000,
|
||||||
|
maxRedirects: 5,
|
||||||
|
});
|
||||||
|
const setCookies = sessionRes.headers["set-cookie"];
|
||||||
|
if (Array.isArray(setCookies)) {
|
||||||
|
cookieHeader = setCookies.map(c => c.split(";")[0]).join("; ");
|
||||||
|
}
|
||||||
|
} catch { /* session establishment is best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Fallback — if no slug provided, try fetching any page to get session cookies
|
||||||
|
if (!cookieHeader) {
|
||||||
|
try {
|
||||||
|
const fallbackRes = await axios.get(`${mirror}/home`, {
|
||||||
|
headers,
|
||||||
|
timeout: 10000,
|
||||||
|
maxRedirects: 5,
|
||||||
|
});
|
||||||
|
const setCookies = fallbackRes.headers["set-cookie"];
|
||||||
|
if (Array.isArray(setCookies)) {
|
||||||
|
cookieHeader = setCookies.map(c => c.split(";")[0]).join("; ");
|
||||||
|
}
|
||||||
|
} catch { /* fallback is best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = `/ajax/server?get=${linkId}`;
|
const path = `/ajax/server?get=${linkId}`;
|
||||||
|
const requestHeaders = {
|
||||||
|
...headers,
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
...(cookieHeader ? { "Cookie": cookieHeader } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
const { data: raw } = await fetchWithMirror(path, {
|
const { data: raw } = await fetchWithMirror(path, {
|
||||||
headers: { "X-Requested-With": "XMLHttpRequest" }
|
headers: requestHeaders
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: Handle both string JSON and parsed object responses
|
// NOTE: Handle both string JSON and parsed object responses
|
||||||
@@ -116,14 +151,47 @@ const extractStreamInfo = async (linkId, watchSlug = null) => {
|
|||||||
*/
|
*/
|
||||||
const extractServerList = async (episodeIds, watchSlug = null) => {
|
const extractServerList = async (episodeIds, watchSlug = null) => {
|
||||||
try {
|
try {
|
||||||
// NOTE: Establish session by visiting watch page first (required for AJAX endpoints)
|
// NOTE: Establish session with cookies — AJAX endpoints require session cookies
|
||||||
|
const mirror = await getWorkingMirror();
|
||||||
|
let cookieHeader = "";
|
||||||
|
|
||||||
if (watchSlug) {
|
if (watchSlug) {
|
||||||
await fetchWithMirror(`/watch/${watchSlug}`);
|
try {
|
||||||
|
const sessionRes = await axios.get(`${mirror}/watch/${watchSlug}`, {
|
||||||
|
headers,
|
||||||
|
timeout: 10000,
|
||||||
|
maxRedirects: 5,
|
||||||
|
});
|
||||||
|
const setCookies = sessionRes.headers["set-cookie"];
|
||||||
|
if (Array.isArray(setCookies)) {
|
||||||
|
cookieHeader = setCookies.map(c => c.split(";")[0]).join("; ");
|
||||||
|
}
|
||||||
|
} catch { /* session establishment is best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cookieHeader) {
|
||||||
|
try {
|
||||||
|
const fallbackRes = await axios.get(`${mirror}/home`, {
|
||||||
|
headers,
|
||||||
|
timeout: 10000,
|
||||||
|
maxRedirects: 5,
|
||||||
|
});
|
||||||
|
const setCookies = fallbackRes.headers["set-cookie"];
|
||||||
|
if (Array.isArray(setCookies)) {
|
||||||
|
cookieHeader = setCookies.map(c => c.split(";")[0]).join("; ");
|
||||||
|
}
|
||||||
|
} catch { /* fallback is best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = `/ajax/server/list?servers=${episodeIds}`;
|
const path = `/ajax/server/list?servers=${episodeIds}`;
|
||||||
|
const requestHeaders = {
|
||||||
|
...headers,
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
...(cookieHeader ? { "Cookie": cookieHeader } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
const { data: raw } = await fetchWithMirror(path, {
|
const { data: raw } = await fetchWithMirror(path, {
|
||||||
headers: { "X-Requested-With": "XMLHttpRequest" }
|
headers: requestHeaders
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: Parse JSON response first — fetchWithMirror returns raw text by default
|
// NOTE: Parse JSON response first — fetchWithMirror returns raw text by default
|
||||||
|
|||||||
Reference in New Issue
Block a user