mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
feat: complete streaming and AJAX endpoint support
- Add stream URL extraction via /ajax/server?get={linkId}
- Add mapper API integration for gogoanime/anivibe servers
- Add /api/seasons/:id endpoint
- Add /api/watch-order/:id endpoint
- Add /api/episodes-ajax/:id endpoint
- Add /api/servers endpoint
- Add genre/type/status/rating ID mappings
- Fix filter to use correct numeric IDs
- Fix watch page data attribute extraction
This commit is contained in:
@@ -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/),
|
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).
|
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
|
## [1.2.0] - 2026-06-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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";
|
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
const getStreamInfo = async (req, res, next) => {
|
const getStreamInfo = async (req, res, next) => {
|
||||||
@@ -6,7 +6,7 @@ const getStreamInfo = async (req, res, next) => {
|
|||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
|
|
||||||
if (!id) {
|
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}`;
|
const cacheKey = `stream_${id}`;
|
||||||
@@ -23,4 +23,48 @@ const getStreamInfo = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getStreamInfo };
|
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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.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 { countPages } from "../helper/countPages.helper.js";
|
||||||
import { extractPages } from "../helper/extractPages.helper.js";
|
import { extractPages } from "../helper/extractPages.helper.js";
|
||||||
|
|
||||||
@@ -10,34 +10,72 @@ const extractFilter = async (params) => {
|
|||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
if (params.keyword) queryParams.set("keyword", params.keyword);
|
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.genre) {
|
||||||
if (params.year) queryParams.set("year", params.year);
|
const genres = params.genre.split(",").map(g => GENRE_IDS[g.trim().toLowerCase()] || g.trim());
|
||||||
if (params.type) queryParams.set("type", params.type);
|
genres.forEach(g => queryParams.append("genre[]", g));
|
||||||
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.type) {
|
||||||
if (params.sort) queryParams.set("sort", params.sort);
|
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 url = `${URLS.search}?${queryParams.toString()}`;
|
||||||
const $ = await extractPages(url, params.page || 1);
|
const $ = await extractPages(url, params.page || 1);
|
||||||
const totalPages = countPages($);
|
const totalPages = countPages($);
|
||||||
|
|
||||||
const results = [];
|
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 slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||||
const poster = $(el).find("img").attr("src") || "";
|
const poster = $(el).find(".poster img, .ani.poster img").attr("src") || "";
|
||||||
const title = $(el).find(".film-name a, .name.d-title").text().trim() || "";
|
const title = $(el).find(".name.d-title").text().trim() || "";
|
||||||
const japaneseTitle = $(el).find(".name.d-title").attr("data-jp") || "";
|
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 sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
||||||
const dub = parseInt($(el).find(".ep-status.dub 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 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 type = $(el).find(".meta .right, .m-item:last-child label").text().trim() || "";
|
||||||
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").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) {
|
if (slug) {
|
||||||
results.push({
|
results.push({
|
||||||
slug,
|
slug,
|
||||||
|
animeId,
|
||||||
poster,
|
poster,
|
||||||
title,
|
title,
|
||||||
japaneseTitle,
|
japaneseTitle,
|
||||||
@@ -45,7 +83,8 @@ const extractFilter = async (params) => {
|
|||||||
dub,
|
dub,
|
||||||
total,
|
total,
|
||||||
type,
|
type,
|
||||||
rating
|
rating,
|
||||||
|
genres
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import * as cheerio from "cheerio";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
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 {
|
try {
|
||||||
const url = `${URLS.serverList}?episodeId=${episodeId}`;
|
const url = `${BASE_URL}/ajax/server?get=${linkId}`;
|
||||||
const { data } = await axios.get(url, {
|
const { data } = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
@@ -13,37 +12,68 @@ const extractStreamInfo = async (episodeId) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const $ = cheerio.load(data);
|
if (!data || !data.result) {
|
||||||
|
return { linkId, url: null, skipData: null };
|
||||||
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 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
episodeId,
|
linkId,
|
||||||
servers,
|
url: data.result.url || null,
|
||||||
links
|
skipData: data.result.skip_data || null
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { extractStreamInfo };
|
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 };
|
||||||
@@ -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 };
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { headers } from "../configs/header.config.js";
|
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) => {
|
const extractWatchPage = async (slug, ep) => {
|
||||||
try {
|
try {
|
||||||
@@ -9,43 +9,41 @@ const extractWatchPage = async (slug, ep) => {
|
|||||||
const { data } = await axios.get(url, { headers });
|
const { data } = await axios.get(url, { headers });
|
||||||
const $ = cheerio.load(data);
|
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 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 = $(".synopsis .content, .film-description .text").text().trim() || "";
|
||||||
|
|
||||||
const synopsis = $(".anisc-detail .description, .film-description .text").text().trim() || "";
|
|
||||||
|
|
||||||
const animeInfo = {};
|
const animeInfo = {};
|
||||||
$(".anisc-info .item").each((i, el) => {
|
$(".bmeta .meta div").each((i, el) => {
|
||||||
const label = $(el).find(".name").text().trim().replace(":", "");
|
const label = $(el).find("strong, b").text().trim().replace(":", "");
|
||||||
const value = $(el).find(".text").text().trim();
|
const value = $(el).find("span, a").text().trim();
|
||||||
if (label && value) {
|
if (label && value) {
|
||||||
animeInfo[label] = value;
|
animeInfo[label] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const genres = [];
|
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();
|
const genre = $(el).text().trim();
|
||||||
if (genre) genres.push(genre);
|
if (genre) genres.push(genre);
|
||||||
});
|
});
|
||||||
|
|
||||||
const servers = [];
|
const rating = $(".brating .score .value, #w-rating .score").text().trim() || "";
|
||||||
$(".servers .server-item, .type-list .server-item").each((i, el) => {
|
const MALScore = $(".bmeta .meta div:contains('MAL') span").text().trim() || "";
|
||||||
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) {
|
const poster = $(".poster img[itemprop='image'], .binfo .poster img").attr("src") || "";
|
||||||
servers.push({ id: serverId, name: serverName, type: serverType });
|
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 = [];
|
const episodeList = [];
|
||||||
$(".episodes-list .ep-item, .ss-list a").each((i, el) => {
|
$("#w-episodes .ep-item, .episodes-list a, .ss-list a").each((i, el) => {
|
||||||
const epId = $(el).attr("data-id") || "";
|
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 epNum = parseInt($(el).find(".ep-number, .ep-num").text().trim()) || i + 1;
|
||||||
const epTitle = $(el).find(".ep-title, .ep-name").text().trim() || "";
|
const epTitle = $(el).find(".ep-title, .ep-name").text().trim() || "";
|
||||||
const isActive = $(el).hasClass("active") || $(el).hasClass("selected") || false;
|
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 servers = [];
|
||||||
const prevEpisode = $(".ep-prev a").attr("href")?.split("/ep-").pop() || "";
|
$("#w-servers .servers .type li, #w-servers li").each((i, el) => {
|
||||||
const nextEpisode = $(".ep-next a").attr("href")?.split("/ep-").pop() || "";
|
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 = [];
|
if (linkId) {
|
||||||
$(".related-content .item, .film-list-block .item").each((i, el) => {
|
servers.push({
|
||||||
const relSlug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
linkId,
|
||||||
const relPoster = $(el).find("img").attr("src") || "";
|
epId,
|
||||||
const relTitle = $(el).find(".name, .film-name a").text().trim() || "";
|
cmId,
|
||||||
const relType = $(el).find(".type, .fdi-item:first-child").text().trim() || "";
|
svId,
|
||||||
|
name: serverName,
|
||||||
if (relSlug) {
|
type: serverType
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const trending = [];
|
const trending = [];
|
||||||
$(".trending-list .item, .top-tables .item").each((i, el) => {
|
$(".w-side-section .scaff.side.items a.item").each((i, el) => {
|
||||||
const trendSlug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
const trendSlug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||||
const trendPoster = $(el).find("img").attr("src") || "";
|
const trendPoster = $(el).find(".poster img").attr("src") || "";
|
||||||
const trendTitle = $(el).find(".name").text().trim() || "";
|
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) {
|
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 {
|
return {
|
||||||
slug,
|
slug,
|
||||||
title: title || slug,
|
|
||||||
japaneseTitle,
|
|
||||||
animeId,
|
animeId,
|
||||||
|
animeUrl,
|
||||||
|
title,
|
||||||
|
japaneseTitle,
|
||||||
episodeNumber,
|
episodeNumber,
|
||||||
synopsis,
|
synopsis,
|
||||||
animeInfo,
|
animeInfo,
|
||||||
genres,
|
genres,
|
||||||
servers,
|
rating,
|
||||||
episodeList,
|
MALScore,
|
||||||
|
poster,
|
||||||
|
backgroundImage,
|
||||||
nextEpisodeDate,
|
nextEpisodeDate,
|
||||||
prevEpisode,
|
nextEpisodeTimestamp,
|
||||||
nextEpisode,
|
episodeList,
|
||||||
relatedData,
|
servers,
|
||||||
recommendedData,
|
trending,
|
||||||
trending
|
recommended
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
+44
-1
@@ -2,7 +2,8 @@ import { getHomeInfo } from "../controllers/homeInfo.controller.js";
|
|||||||
import { getAnimeInfo } from "../controllers/animeInfo.controller.js";
|
import { getAnimeInfo } from "../controllers/animeInfo.controller.js";
|
||||||
import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
|
import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
|
||||||
import { getEpisodeList } from "../controllers/episodeList.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 { getSchedule } from "../controllers/schedule.controller.js";
|
||||||
import { getSpotlight } from "../controllers/spotlight.controller.js";
|
import { getSpotlight } from "../controllers/spotlight.controller.js";
|
||||||
import { getTrending } from "../controllers/trending.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 { getNewRelease, getNewlyAdded } from "../controllers/newRelease.controller.js";
|
||||||
import { getStatus } from "../controllers/status.controller.js";
|
import { getStatus } from "../controllers/status.controller.js";
|
||||||
import { getTrendingSidebar } from "../controllers/trendingSidebar.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";
|
import { categoryRoutes } from "./category.route.js";
|
||||||
|
|
||||||
const createApiRoutes = (app, jsonResponse, jsonError) => {
|
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) => {
|
app.get("/api/stream", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getStreamInfo(req, res, next);
|
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) => {
|
app.get("/api/schedule", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getSchedule(req, res, next);
|
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) => {
|
app.get("/api/az-list/:letter", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await getAzList(req, res, next);
|
await getAzList(req, res, next);
|
||||||
|
|||||||
Reference in New Issue
Block a user