diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31b54b8..15f2800 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,12 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- **CRITICAL**: Fixed `streamInfo.extractor.js` `extractServerList` not parsing JSON response — `fetchWithMirror` returns raw text by default, causing Cheerio to parse a JSON string as HTML, resulting in empty server arrays (null values)
- **CRITICAL**: Fixed `streamInfo.extractor.js` `extractMapperServers` not parsing JSON response — same raw text issue caused the data processing loop to be skipped entirely, returning empty arrays
+- **CRITICAL**: Fixed `creatorInfo.js` middleware overwriting error messages — the `message` field from creatorInfo was replacing actual error messages, making debugging impossible (all 500 errors showed "Built with ❤️ by Shinei Nouzen" instead of the real error)
- Fixed `streamInfo.controller.js` `getStreamInfo` to return 404 with error message when stream URL is null instead of silently returning null values
- Fixed `streamInfo.controller.js` `getServerList` to return 404 with error message when server list is empty instead of returning empty array with no indication
- Fixed `streamInfo.controller.js` to use `TTL.stream` instead of `TTL.default` for stream info caching (stream URLs rotate frequently)
- Fixed `streamInfo.controller.js` to use `TTL.servers` instead of `TTL.default` for server list caching
- Fixed `episodeList.extractor.js` JSON.parse error handling — added try/catch around JSON parsing to prevent uncaught exceptions
- Fixed `episodeListAjax.extractor.js` JSON.parse error handling — added try/catch around JSON parsing to prevent uncaught exceptions
+- Fixed `schedule.extractor.js` — changed from incorrect `/?date=` URL to proper `/ajax/schedule?date=` AJAX endpoint with JSON parsing
+- Fixed `seasons.extractor.js` — updated CSS selectors to handle new HTML structure where items use `data-tip` attribute instead of `` tags
+- Fixed `watchOrder.extractor.js` — updated CSS selectors to handle new HTML structure, added fallback to `data-tip` attribute for slugs, fixed type/episodes field extraction from `.meta .dot` elements
## [2.2.0] - 2026-07-28
diff --git a/src/extractors/schedule.extractor.js b/src/extractors/schedule.extractor.js
index 8eb5b0c..2a7b6ab 100644
--- a/src/extractors/schedule.extractor.js
+++ b/src/extractors/schedule.extractor.js
@@ -42,24 +42,37 @@ import { fetchWithMirror } from "../helper/mirror.helper.js";
*/
const extractSchedule = async (date) => {
try {
- const path = `/?date=${date}`;
- const { data } = await fetchWithMirror(path);
- const $ = cheerio.load(data);
+ // NOTE: The schedule data is loaded via AJAX endpoint, not a static page
+ const path = `/ajax/schedule?date=${date}`;
+ const { data: raw } = await fetchWithMirror(path, {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ });
+
+ // NOTE: Parse JSON response first — fetchWithMirror returns raw text by default
+ let parsed = raw;
+ if (typeof raw === "string") {
+ try { parsed = JSON.parse(raw); } catch { parsed = {}; }
+ }
+ const html = parsed?.result || "";
+ const $ = cheerio.load(html);
const schedule = [];
- // NOTE: Dual selector pattern — different mirrors use different HTML structures
- // .schedule-item = new layout, .anime-schedule .item = old layout
- $(".schedule-item, .anime-schedule .item").each((i, el) => {
- const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
- const title = $(el).find(".film-name a, .name").text().trim() || "";
- const time = $(el).find(".time, .schedule-time").text().trim() || "";
- const episodeNo = parseInt($(el).find(".episode-no, .ep-num").text().trim()) || 0;
+ // NOTE: Schedule items live inside .item with .time, .ep, .title structure
+ $(".item").each((i, el) => {
+ const title = $(el).find(".title").text().trim() || "";
+ const jpTitle = $(el).find(".title").attr("data-jp") || "";
+ const time = $(el).find(".time").text().trim() || "";
+ const episodeText = $(el).find(".ep span").text().trim() || "";
+ const episodeNo = parseInt(episodeText.replace(/\D/g, "")) || 0;
+ // NOTE: Items may not have direct links — use title as identifier
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "";
- if (slug) {
+ if (title) {
schedule.push({
slug,
title,
+ japaneseTitle: jpTitle,
time,
episode_no: episodeNo
});
diff --git a/src/extractors/seasons.extractor.js b/src/extractors/seasons.extractor.js
index 7bb3864..183d6a4 100644
--- a/src/extractors/seasons.extractor.js
+++ b/src/extractors/seasons.extractor.js
@@ -55,7 +55,10 @@ const extractSeasons = async (slugOrId) => {
const href = link.attr("href") || "";
const poster = $(el).find("img").attr("src") || "";
const title = $(el).find(".name, .title").text().trim() || link.text().trim() || "";
- const slug = href.split("/watch/").pop() || "";
+ let slug = href.split("/watch/").pop() || "";
+ if (!slug) {
+ slug = $(el).find(".poster").attr("data-tip") || "";
+ }
if (slug || title) {
seasons.push({ title, slug, poster, url: href });
@@ -69,7 +72,11 @@ const extractSeasons = async (slugOrId) => {
const href = link.attr("href") || "";
const poster = $(el).find("img").attr("src") || "";
const title = $(el).find(".name").text().trim() || "";
- const slug = href.split("/watch/").pop() || "";
+ let slug = href.split("/watch/").pop() || "";
+ // NOTE: Items may not have tags — fall back to data-tip attribute
+ if (!slug) {
+ slug = $(el).find(".poster").attr("data-tip") || "";
+ }
const score = $(el).find(".score").text().trim() || "";
const episodes = $(el).find(".meta .dot:last-child").text().trim() || "";
diff --git a/src/extractors/watchOrder.extractor.js b/src/extractors/watchOrder.extractor.js
index a9ad0a4..4a68f0c 100644
--- a/src/extractors/watchOrder.extractor.js
+++ b/src/extractors/watchOrder.extractor.js
@@ -55,7 +55,7 @@ const extractWatchOrder = async (slugOrId) => {
const href = link.attr("href") || "";
const title = $(el).find(".name").text().trim() || "";
const poster = $(el).find("img").attr("src") || "";
- const slug = href.split("/watch/").pop() || "";
+ const slug = href.split("/watch/").pop() || $(el).find(".poster").attr("data-tip") || "";
const relation = $(el).find(".relation, .serieslabelitem").text().trim() || "related";
if (slug) {
@@ -63,17 +63,24 @@ const extractWatchOrder = async (slugOrId) => {
}
});
- // NOTE: Fallback — extract trending sidebar items as suggested watch order
+ // NOTE: Fallback — extract from #watch-order section (trending sidebar items as suggested watch order)
if (related.length === 0) {
- $(".w-side-section:has(.title:contains('Trending')) .item, .w-side-section:first .item").each((_, el) => {
+ $("#watch-order .item, .w-side-section:has(.title:contains('Trending')) .item, .w-side-section:first .item").each((_, el) => {
const link = $(el).find("a").first();
const href = link.attr("href") || "";
const title = $(el).find(".name").text().trim() || "";
const poster = $(el).find("img").attr("src") || "";
- const slug = href.split("/watch/").pop() || "";
+ // NOTE: Items may not have tags — fall back to data-tip attribute for slug
+ let slug = href.split("/watch/").pop() || "";
+ if (!slug) {
+ const dataTip = $(el).find(".poster").attr("data-tip") || "";
+ if (dataTip) slug = dataTip;
+ }
const score = $(el).find(".score").text().trim() || "";
- const type = $(el).find(".meta .dot:last-child").text().trim() || "";
- const episodes = $(el).find(".meta .dot:first-child").text().trim() || "";
+ // NOTE: .meta .dot elements: [0]=score, [1]=type, [2]=episodes
+ const metaDots = $(el).find(".meta .dot");
+ const type = metaDots.eq(1).text().trim() || "";
+ const episodes = metaDots.eq(2).text().trim() || "";
if (slug && slug !== slugOrId) {
related.push({ title, slug, poster, url: href, relation: "trending", score, type, episodes });
diff --git a/src/middleware/creatorInfo.js b/src/middleware/creatorInfo.js
index 62d1099..7fc0154 100644
--- a/src/middleware/creatorInfo.js
+++ b/src/middleware/creatorInfo.js
@@ -58,14 +58,21 @@ const addCreatorInfo = (req, res, next) => {
res.json = (data) => {
// NOTE: Only inject if data is a plain object and missing creator field
if (typeof data === "object" && data !== null && !data.creator) {
+ // NOTE: Preserve existing 'message' field (e.g., error messages) — only add creator metadata
+ const preservedMessage = data.message;
data = {
...data,
creator: creatorInfo.creator,
github: creatorInfo.github,
telegram: creatorInfo.telegram,
- message: creatorInfo.message,
- timestamp: new Date().toLocaleString("en-IN", { timeZone: "Asia/Kolkata", hour12: true }),
};
+ // NOTE: Restore original message if it existed (error handler sets this)
+ if (preservedMessage) {
+ data.message = preservedMessage;
+ } else {
+ data.message = creatorInfo.message;
+ }
+ data.timestamp = new Date().toLocaleString("en-IN", { timeZone: "Asia/Kolkata", hour12: true });
}
return originalJson(data);
};