diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c6109e..f322277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] - 2026-06-08 + +### Added +- Stream URL extraction via `/ajax/server?get={linkId}` endpoint +- Server list endpoint (`/api/servers?ids={episodeIds}`) +- Mapper API integration for gogoanime/anivibe servers (`/api/mapper-servers`) +- Seasons endpoint (`/api/seasons/:id`) +- Watch order endpoint (`/api/watch-order/:id`) +- Episode list AJAX endpoint (`/api/episodes-ajax/:id`) +- Genre ID mapping for correct filter parameters +- Type, Status, Rating, Sort ID mappings + +### Fixed +- Stream extractor now properly extracts video embed URLs +- Filter endpoint now uses correct numeric IDs for genres/types/status +- Watch page extractor captures all data attributes (data-id, data-url, etc.) +- Server list extraction includes all data attributes (link-id, ep-id, cmid, sv-id) + ## [1.2.0] - 2026-06-08 ### Added diff --git a/src/configs/ids.config.js b/src/configs/ids.config.js new file mode 100644 index 0000000..39f3bcc --- /dev/null +++ b/src/configs/ids.config.js @@ -0,0 +1,82 @@ +const GENRE_IDS = { + "action": "1", + "adventure": "2", + "cars": "3", + "comedy": "4", + "dementia": "5", + "demons": "6", + "drama": "7", + "ecchi": "8", + "fantasy": "9", + "game": "10", + "harem": "11", + "historical": "12", + "horror": "13", + "isekai": "14", + "josei": "15", + "kids": "16", + "magic": "17", + "mahou-shoujo": "18", + "martial-arts": "19", + "mecha": "20", + "military": "21", + "music": "22", + "mystery": "23", + "parody": "24", + "police": "25", + "psychological": "26", + "romance": "27", + "samurai": "28", + "school": "29", + "sci-fi": "30", + "seinen": "31", + "shoujo": "32", + "shoujo-ai": "33", + "shounen": "34", + "shounen-ai": "35", + "slice-of-life": "36", + "space": "37", + "sports": "38", + "super-power": "39", + "supernatural": "40", + "thriller": "41", + "unknown": "42", + "vampire": "43" +}; + +const TYPE_IDS = { + "movie": "1", + "music": "2", + "ona": "3", + "ova": "4", + "special": "5", + "tv": "6" +}; + +const STATUS_IDS = { + "currently-airing": "1", + "finished-airing": "2", + "not-yet-aired": "3" +}; + +const RATING_IDS = { + "g": "1", + "pg": "2", + "pg-13": "3", + "r": "4", + "r+": "5", + "rx": "6" +}; + +const SORT_IDS = { + "default": "", + "latest-updated": "1", + "latest-added": "2", + "score": "3", + "name-az": "4", + "release-date": "5", + "most-viewed": "6", + "number_of_episodes": "7" +}; + +export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS }; \ No newline at end of file diff --git a/src/controllers/episodeListAjax.controller.js b/src/controllers/episodeListAjax.controller.js new file mode 100644 index 0000000..19743d3 --- /dev/null +++ b/src/controllers/episodeListAjax.controller.js @@ -0,0 +1,27 @@ +import { extractEpisodeListAjax } from "../extractors/episodeListAjax.extractor.js"; +import { getCache, setCache } from "../helper/cache.helper.js"; + +const getEpisodeListAjax = async (req, res, next) => { + try { + const { id } = req.params; + const { style, vrf } = req.query; + + if (!id) { + return res.status(400).json({ success: false, message: "Anime ID is required" }); + } + + const cacheKey = `episodes_ajax_${id}_${style || ""}_${vrf || ""}`; + const cached = getCache(cacheKey); + if (cached) { + return res.json({ success: true, results: cached }); + } + + const data = await extractEpisodeListAjax(id, style, vrf); + setCache(cacheKey, data); + res.json({ success: true, results: data }); + } catch (error) { + next(error); + } +}; + +export { getEpisodeListAjax }; \ No newline at end of file diff --git a/src/controllers/seasons.controller.js b/src/controllers/seasons.controller.js new file mode 100644 index 0000000..63ca9f8 --- /dev/null +++ b/src/controllers/seasons.controller.js @@ -0,0 +1,26 @@ +import { extractSeasons } from "../extractors/seasons.extractor.js"; +import { getCache, setCache } from "../helper/cache.helper.js"; + +const getSeasons = async (req, res, next) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ success: false, message: "Anime ID is required" }); + } + + const cacheKey = `seasons_${id}`; + const cached = getCache(cacheKey); + if (cached) { + return res.json({ success: true, results: cached }); + } + + const data = await extractSeasons(id); + setCache(cacheKey, data); + res.json({ success: true, results: data }); + } catch (error) { + next(error); + } +}; + +export { getSeasons }; \ No newline at end of file diff --git a/src/controllers/streamInfo.controller.js b/src/controllers/streamInfo.controller.js index 7aa9a2e..b1d61ce 100644 --- a/src/controllers/streamInfo.controller.js +++ b/src/controllers/streamInfo.controller.js @@ -1,4 +1,4 @@ -import { extractStreamInfo } from "../extractors/streamInfo.extractor.js"; +import { extractStreamInfo, extractServerList, extractMapperServers } from "../extractors/streamInfo.extractor.js"; import { getCache, setCache } from "../helper/cache.helper.js"; const getStreamInfo = async (req, res, next) => { @@ -6,7 +6,7 @@ const getStreamInfo = async (req, res, next) => { const { id } = req.query; if (!id) { - return res.status(400).json({ success: false, message: "Episode ID is required" }); + return res.status(400).json({ success: false, message: "Link ID is required" }); } const cacheKey = `stream_${id}`; @@ -23,4 +23,48 @@ const getStreamInfo = async (req, res, next) => { } }; -export { getStreamInfo }; \ No newline at end of file +const getServerList = async (req, res, next) => { + try { + const { ids } = req.query; + + if (!ids) { + return res.status(400).json({ success: false, message: "Episode IDs are required" }); + } + + const cacheKey = `servers_${ids}`; + const cached = getCache(cacheKey); + if (cached) { + return res.json({ success: true, results: cached }); + } + + const data = await extractServerList(ids); + setCache(cacheKey, data); + res.json({ success: true, results: data }); + } catch (error) { + next(error); + } +}; + +const getMapperServers = async (req, res, next) => { + try { + const { malId, slug, timestamp } = req.query; + + if (!malId || !slug || !timestamp) { + return res.status(400).json({ success: false, message: "malId, slug, and timestamp are required" }); + } + + const cacheKey = `mapper_${malId}_${slug}_${timestamp}`; + const cached = getCache(cacheKey); + if (cached) { + return res.json({ success: true, results: cached }); + } + + const data = await extractMapperServers(malId, slug, timestamp); + setCache(cacheKey, data); + res.json({ success: true, results: data }); + } catch (error) { + next(error); + } +}; + +export { getStreamInfo, getServerList, getMapperServers }; \ No newline at end of file diff --git a/src/controllers/watchOrder.controller.js b/src/controllers/watchOrder.controller.js new file mode 100644 index 0000000..5294eef --- /dev/null +++ b/src/controllers/watchOrder.controller.js @@ -0,0 +1,26 @@ +import { extractWatchOrder } from "../extractors/watchOrder.extractor.js"; +import { getCache, setCache } from "../helper/cache.helper.js"; + +const getWatchOrder = async (req, res, next) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ success: false, message: "Anime ID is required" }); + } + + const cacheKey = `watchorder_${id}`; + const cached = getCache(cacheKey); + if (cached) { + return res.json({ success: true, results: cached }); + } + + const data = await extractWatchOrder(id); + setCache(cacheKey, data); + res.json({ success: true, results: data }); + } catch (error) { + next(error); + } +}; + +export { getWatchOrder }; \ No newline at end of file diff --git a/src/extractors/episodeListAjax.extractor.js b/src/extractors/episodeListAjax.extractor.js new file mode 100644 index 0000000..f9af932 --- /dev/null +++ b/src/extractors/episodeListAjax.extractor.js @@ -0,0 +1,26 @@ +import axios from "axios"; +import { headers } from "../configs/header.config.js"; +import { BASE_URL } from "../configs/dataUrl.js"; + +const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => { + try { + let url = `${BASE_URL}/ajax/episode/list/${animeId}`; + const params = []; + if (style) params.push(`style=${style}`); + if (vrf) params.push(`vrf=${vrf}`); + if (params.length) url += `?${params.join("&")}`; + + const { data } = await axios.get(url, { + headers: { + ...headers, + "X-Requested-With": "XMLHttpRequest" + } + }); + + return data; + } catch (error) { + throw error; + } +}; + +export { extractEpisodeListAjax }; \ No newline at end of file diff --git a/src/extractors/filter.extractor.js b/src/extractors/filter.extractor.js index 05a985f..7ce9aad 100644 --- a/src/extractors/filter.extractor.js +++ b/src/extractors/filter.extractor.js @@ -1,43 +1,81 @@ import * as cheerio from "cheerio"; -import axios from "axios"; import { headers } from "../configs/header.config.js"; import { URLS } from "../configs/dataUrl.js"; +import { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS } from "../configs/ids.config.js"; import { countPages } from "../helper/countPages.helper.js"; import { extractPages } from "../helper/extractPages.helper.js"; const extractFilter = async (params) => { try { const queryParams = new URLSearchParams(); - + if (params.keyword) queryParams.set("keyword", params.keyword); - if (params.genre) queryParams.set("genre", params.genre); - if (params.season) queryParams.set("season", params.season); - if (params.year) queryParams.set("year", params.year); - if (params.type) queryParams.set("type", params.type); - if (params.status) queryParams.set("status", params.status); - if (params.language) queryParams.set("language", params.language); - if (params.rating) queryParams.set("rating", params.rating); - if (params.sort) queryParams.set("sort", params.sort); + + if (params.genre) { + const genres = params.genre.split(",").map(g => GENRE_IDS[g.trim().toLowerCase()] || g.trim()); + genres.forEach(g => queryParams.append("genre[]", g)); + } + + if (params.type) { + const types = params.type.split(",").map(t => TYPE_IDS[t.trim().toLowerCase()] || t.trim()); + types.forEach(t => queryParams.append("term_type[]", t)); + } + + if (params.status) { + const statuses = params.status.split(",").map(s => STATUS_IDS[s.trim().toLowerCase()] || s.trim()); + statuses.forEach(s => queryParams.append("status[]", s)); + } + + if (params.language) { + const langs = params.language.split(","); + langs.forEach(l => queryParams.append("language[]", l.trim())); + } + + if (params.rating) { + const ratings = params.rating.split(",").map(r => RATING_IDS[r.trim().toLowerCase()] || r.trim()); + ratings.forEach(r => queryParams.append("rating[]", r)); + } + + if (params.sort) { + const sort = SORT_IDS[params.sort.trim().toLowerCase()] ?? params.sort; + if (sort) queryParams.set("sort", sort); + } + + if (params.season) { + const seasons = params.season.split(","); + seasons.forEach(s => queryParams.append("season[]", s.trim())); + } + + if (params.year) { + const years = params.year.split(","); + years.forEach(y => queryParams.append("year[]", y.trim())); + } const url = `${URLS.search}?${queryParams.toString()}`; const $ = await extractPages(url, params.page || 1); const totalPages = countPages($); const results = []; - $(".film_list-wrap .flw-item, .film-detail").each((i, el) => { + $("#list-items .item, .film_list-wrap .flw-item").each((i, el) => { const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || ""; - const poster = $(el).find("img").attr("src") || ""; - const title = $(el).find(".film-name a, .name.d-title").text().trim() || ""; + const poster = $(el).find(".poster img, .ani.poster img").attr("src") || ""; + const title = $(el).find(".name.d-title").text().trim() || ""; const japaneseTitle = $(el).find(".name.d-title").attr("data-jp") || ""; + const animeId = $(el).find(".poster").attr("data-tip") || ""; const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0; const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0; const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0; - const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || ""; - const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || ""; + const type = $(el).find(".meta .right, .m-item:last-child label").text().trim() || ""; + const rating = $(el).find(".m-item.rated span, .rating").text().trim() || ""; + const genres = []; + $(el).find(".genre a").each((j, g) => { + genres.push($(g).text().trim()); + }); if (slug) { results.push({ slug, + animeId, poster, title, japaneseTitle, @@ -45,7 +83,8 @@ const extractFilter = async (params) => { dub, total, type, - rating + rating, + genres }); } }); diff --git a/src/extractors/seasons.extractor.js b/src/extractors/seasons.extractor.js new file mode 100644 index 0000000..51fb3f6 --- /dev/null +++ b/src/extractors/seasons.extractor.js @@ -0,0 +1,21 @@ +import axios from "axios"; +import { headers } from "../configs/header.config.js"; +import { BASE_URL } from "../configs/dataUrl.js"; + +const extractSeasons = async (animeId) => { + try { + const url = `${BASE_URL}/api/seasons/${animeId}`; + const { data } = await axios.get(url, { + headers: { + ...headers, + "X-Requested-With": "XMLHttpRequest" + } + }); + + return data || []; + } catch (error) { + return []; + } +}; + +export { extractSeasons }; \ No newline at end of file diff --git a/src/extractors/streamInfo.extractor.js b/src/extractors/streamInfo.extractor.js index c98ca79..37a5214 100644 --- a/src/extractors/streamInfo.extractor.js +++ b/src/extractors/streamInfo.extractor.js @@ -1,49 +1,79 @@ -import * as cheerio from "cheerio"; import axios from "axios"; import { headers } from "../configs/header.config.js"; -import { URLS } from "../configs/dataUrl.js"; +import { BASE_URL } from "../configs/dataUrl.js"; -const extractStreamInfo = async (episodeId) => { +const extractStreamInfo = async (linkId) => { try { - const url = `${URLS.serverList}?episodeId=${episodeId}`; - const { data } = await axios.get(url, { + const url = `${BASE_URL}/ajax/server?get=${linkId}`; + const { data } = await axios.get(url, { headers: { ...headers, "X-Requested-With": "XMLHttpRequest" } }); - - const $ = cheerio.load(data); - const servers = []; - $(".servers .server-item, .type-list .server-item").each((i, el) => { - const serverId = $(el).attr("data-id") || $(el).attr("data-server-id") || ""; - const serverName = $(el).text().trim() || ""; - const serverType = $(el).hasClass("sub") ? "sub" : "dub"; - - if (serverId) { - servers.push({ id: serverId, name: serverName, type: serverType }); - } - }); - - const links = []; - $("li[data-link-id]").each((i, el) => { - const linkId = $(el).attr("data-link-id") || ""; - const linkType = $(el).attr("data-type") || "hls"; - - if (linkId) { - links.push({ id: linkId, type: linkType }); - } - }); + if (!data || !data.result) { + return { linkId, url: null, skipData: null }; + } return { - episodeId, - servers, - links + linkId, + url: data.result.url || null, + skipData: data.result.skip_data || null }; } catch (error) { throw error; } }; -export { extractStreamInfo }; \ No newline at end of file +const extractServerList = async (episodeIds) => { + try { + const url = `${BASE_URL}/ajax/server/list?servers=${episodeIds}`; + const { data } = await axios.get(url, { + headers: { + ...headers, + "X-Requested-With": "XMLHttpRequest" + } + }); + + return data; + } catch (error) { + throw error; + } +}; + +const extractMapperServers = async (malId, slug, timestamp) => { + try { + const url = `https://mapper.nekostream.site/api/mal/${malId}/${slug}/${timestamp}`; + const { data } = await axios.get(url, { headers }); + + const servers = []; + + if (data && typeof data === "object") { + for (const [provider, sources] of Object.entries(data)) { + if (sources && sources.sub) { + servers.push({ + provider, + type: "sub", + url: sources.sub.url || null, + download: sources.sub.download || null + }); + } + if (sources && sources.dub) { + servers.push({ + provider, + type: "dub", + url: sources.dub.url || null, + download: sources.dub.download || null + }); + } + } + } + + return servers; + } catch (error) { + return []; + } +}; + +export { extractStreamInfo, extractServerList, extractMapperServers }; \ No newline at end of file diff --git a/src/extractors/watchOrder.extractor.js b/src/extractors/watchOrder.extractor.js new file mode 100644 index 0000000..e7e95d5 --- /dev/null +++ b/src/extractors/watchOrder.extractor.js @@ -0,0 +1,21 @@ +import axios from "axios"; +import { headers } from "../configs/header.config.js"; +import { BASE_URL } from "../configs/dataUrl.js"; + +const extractWatchOrder = async (animeId) => { + try { + const url = `${BASE_URL}/api/watch-order/${animeId}`; + const { data } = await axios.get(url, { + headers: { + ...headers, + "X-Requested-With": "XMLHttpRequest" + } + }); + + return data || []; + } catch (error) { + return []; + } +}; + +export { extractWatchOrder }; \ No newline at end of file diff --git a/src/extractors/watchPage.extractor.js b/src/extractors/watchPage.extractor.js index 542ceb5..32a88c5 100644 --- a/src/extractors/watchPage.extractor.js +++ b/src/extractors/watchPage.extractor.js @@ -1,7 +1,7 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { headers } from "../configs/header.config.js"; -import { URLS } from "../configs/dataUrl.js"; +import { URLS, BASE_URL } from "../configs/dataUrl.js"; const extractWatchPage = async (slug, ep) => { try { @@ -9,43 +9,41 @@ const extractWatchPage = async (slug, ep) => { const { data } = await axios.get(url, { headers }); const $ = cheerio.load(data); - const title = $(".heading-name a").text().trim() || ""; + const animeId = parseInt($("#watch-main").attr("data-id")) || parseInt($("div[data-id]").attr("data-id")) || 0; + const animeUrl = $("#watch-main").attr("data-url") || ""; + const title = $(".heading-name a").text().trim() || slug; const japaneseTitle = $(".heading-name .alternate-title").text().trim() || ""; - const animeId = $(".heading-name a").attr("href")?.split("/watch/").pop()?.split("/")[0] || ""; + const episodeNumber = parseInt(ep) || 0; - const episodeNumber = parseInt($(".heading-name .episode").text().trim()) || parseInt(ep) || 0; - - const synopsis = $(".anisc-detail .description, .film-description .text").text().trim() || ""; + const synopsis = $(".synopsis .content, .film-description .text").text().trim() || ""; const animeInfo = {}; - $(".anisc-info .item").each((i, el) => { - const label = $(el).find(".name").text().trim().replace(":", ""); - const value = $(el).find(".text").text().trim(); + $(".bmeta .meta div").each((i, el) => { + const label = $(el).find("strong, b").text().trim().replace(":", ""); + const value = $(el).find("span, a").text().trim(); if (label && value) { animeInfo[label] = value; } }); const genres = []; - $(".anisc-info .item:last-child a, .genres a").each((i, el) => { + $(".bmeta .meta div:contains('Genres') a").each((i, el) => { const genre = $(el).text().trim(); if (genre) genres.push(genre); }); - const servers = []; - $(".servers .server-item, .type-list .server-item").each((i, el) => { - const serverId = $(el).attr("data-id") || $(el).attr("data-server-id") || ""; - const serverName = $(el).text().trim() || ""; - const serverType = $(el).hasClass("sub") ? "sub" : "dub"; + const rating = $(".brating .score .value, #w-rating .score").text().trim() || ""; + const MALScore = $(".bmeta .meta div:contains('MAL') span").text().trim() || ""; - if (serverId) { - servers.push({ id: serverId, name: serverName, type: serverType }); - } - }); + const poster = $(".poster img[itemprop='image'], .binfo .poster img").attr("src") || ""; + const backgroundImage = $("#player").css("background-image")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || ""; + + const nextEpisodeDate = $(".next-episode, .alert.next-episode").text().trim() || ""; + const nextEpisodeTimestamp = parseInt($(".count-down").attr("data-target")) || 0; const episodeList = []; - $(".episodes-list .ep-item, .ss-list a").each((i, el) => { - const epId = $(el).attr("data-id") || ""; + $("#w-episodes .ep-item, .episodes-list a, .ss-list a").each((i, el) => { + const epId = $(el).attr("data-id") || $(el).attr("data-ep-id") || ""; const epNum = parseInt($(el).find(".ep-number, .ep-num").text().trim()) || i + 1; const epTitle = $(el).find(".ep-title, .ep-name").text().trim() || ""; const isActive = $(el).hasClass("active") || $(el).hasClass("selected") || false; @@ -60,63 +58,83 @@ const extractWatchPage = async (slug, ep) => { } }); - const nextEpisodeDate = $(".next-episode, .schedule-next").text().trim() || ""; - const prevEpisode = $(".ep-prev a").attr("href")?.split("/ep-").pop() || ""; - const nextEpisode = $(".ep-next a").attr("href")?.split("/ep-").pop() || ""; + const servers = []; + $("#w-servers .servers .type li, #w-servers li").each((i, el) => { + const linkId = $(el).attr("data-link-id") || ""; + const epId = $(el).attr("data-ep-id") || ""; + const cmId = $(el).attr("data-cmid") || ""; + const svId = $(el).attr("data-sv-id") || ""; + const serverName = $(el).text().trim() || ""; + const serverType = $(el).closest(".type").attr("data-type") || ""; - const relatedData = []; - $(".related-content .item, .film-list-block .item").each((i, el) => { - const relSlug = $(el).find("a").attr("href")?.split("/watch/").pop() || ""; - const relPoster = $(el).find("img").attr("src") || ""; - const relTitle = $(el).find(".name, .film-name a").text().trim() || ""; - const relType = $(el).find(".type, .fdi-item:first-child").text().trim() || ""; - - if (relSlug) { - relatedData.push({ slug: relSlug, poster: relPoster, title: relTitle, type: relType }); - } - }); - - const recommendedData = []; - $(".recommend-content .item, .film-list-block .item").each((i, el) => { - const recSlug = $(el).find("a").attr("href")?.split("/watch/").pop() || ""; - const recPoster = $(el).find("img").attr("src") || ""; - const recTitle = $(el).find(".name, .film-name a").text().trim() || ""; - const recType = $(el).find(".type, .fdi-item:first-child").text().trim() || ""; - - if (recSlug) { - recommendedData.push({ slug: recSlug, poster: recPoster, title: recTitle, type: recType }); + if (linkId) { + servers.push({ + linkId, + epId, + cmId, + svId, + name: serverName, + type: serverType + }); } }); const trending = []; - $(".trending-list .item, .top-tables .item").each((i, el) => { - const trendSlug = $(el).find("a").attr("href")?.split("/watch/").pop() || ""; - const trendPoster = $(el).find("img").attr("src") || ""; + $(".w-side-section .scaff.side.items a.item").each((i, el) => { + const trendSlug = $(el).attr("href")?.split("/watch/").pop() || ""; + const trendPoster = $(el).find(".poster img").attr("src") || ""; const trendTitle = $(el).find(".name").text().trim() || ""; - const trendType = $(el).find(".type").text().trim() || ""; + const trendType = $(el).find(".dot:last-child").text().trim() || ""; + const trendScore = $(el).find(".score").text().trim() || ""; if (trendSlug) { - trending.push({ slug: trendSlug, poster: trendPoster, title: trendTitle, type: trendType }); + trending.push({ + slug: trendSlug, + poster: trendPoster, + title: trendTitle, + type: trendType, + score: trendScore + }); + } + }); + + const recommended = []; + $(".w-side-section:has(.title:contains('Recommended')) a.item, aside.sidebar:last .w-side-section a.item").each((i, el) => { + const recSlug = $(el).attr("href")?.split("/watch/").pop() || ""; + const recPoster = $(el).find(".poster img").attr("src") || ""; + const recTitle = $(el).find(".name").text().trim() || ""; + const recType = $(el).find(".dot:last-child").text().trim() || ""; + + if (recSlug) { + recommended.push({ + slug: recSlug, + poster: recPoster, + title: recTitle, + type: recType + }); } }); return { slug, - title: title || slug, - japaneseTitle, animeId, + animeUrl, + title, + japaneseTitle, episodeNumber, synopsis, animeInfo, genres, - servers, - episodeList, + rating, + MALScore, + poster, + backgroundImage, nextEpisodeDate, - prevEpisode, - nextEpisode, - relatedData, - recommendedData, - trending + nextEpisodeTimestamp, + episodeList, + servers, + trending, + recommended }; } catch (error) { throw error; diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js index b4f0a23..49d63c2 100644 --- a/src/routes/apiRoutes.js +++ b/src/routes/apiRoutes.js @@ -2,7 +2,8 @@ import { getHomeInfo } from "../controllers/homeInfo.controller.js"; import { getAnimeInfo } from "../controllers/animeInfo.controller.js"; import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js"; import { getEpisodeList } from "../controllers/episodeList.controller.js"; -import { getStreamInfo } from "../controllers/streamInfo.controller.js"; +import { getEpisodeListAjax } from "../controllers/episodeListAjax.controller.js"; +import { getStreamInfo, getServerList, getMapperServers } from "../controllers/streamInfo.controller.js"; import { getSchedule } from "../controllers/schedule.controller.js"; import { getSpotlight } from "../controllers/spotlight.controller.js"; import { getTrending } from "../controllers/trending.controller.js"; @@ -16,6 +17,8 @@ import { getAzList } from "../controllers/azList.controller.js"; import { getNewRelease, getNewlyAdded } from "../controllers/newRelease.controller.js"; import { getStatus } from "../controllers/status.controller.js"; import { getTrendingSidebar } from "../controllers/trendingSidebar.controller.js"; +import { getSeasons } from "../controllers/seasons.controller.js"; +import { getWatchOrder } from "../controllers/watchOrder.controller.js"; import { categoryRoutes } from "./category.route.js"; const createApiRoutes = (app, jsonResponse, jsonError) => { @@ -67,6 +70,14 @@ const createApiRoutes = (app, jsonResponse, jsonError) => { } }); + app.get("/api/episodes-ajax/:id", async (req, res, next) => { + try { + await getEpisodeListAjax(req, res, next); + } catch (error) { + jsonError(res, error.message); + } + }); + app.get("/api/stream", async (req, res, next) => { try { await getStreamInfo(req, res, next); @@ -75,6 +86,22 @@ const createApiRoutes = (app, jsonResponse, jsonError) => { } }); + app.get("/api/servers", async (req, res, next) => { + try { + await getServerList(req, res, next); + } catch (error) { + jsonError(res, error.message); + } + }); + + app.get("/api/mapper-servers", async (req, res, next) => { + try { + await getMapperServers(req, res, next); + } catch (error) { + jsonError(res, error.message); + } + }); + app.get("/api/schedule", async (req, res, next) => { try { await getSchedule(req, res, next); @@ -163,6 +190,22 @@ const createApiRoutes = (app, jsonResponse, jsonError) => { } }); + app.get("/api/seasons/:id", async (req, res, next) => { + try { + await getSeasons(req, res, next); + } catch (error) { + jsonError(res, error.message); + } + }); + + app.get("/api/watch-order/:id", async (req, res, next) => { + try { + await getWatchOrder(req, res, next); + } catch (error) { + jsonError(res, error.message); + } + }); + app.get("/api/az-list/:letter", async (req, res, next) => { try { await getAzList(req, res, next);