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); };