mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 10:15:10 +00:00
feat: MiruroAPI v1.0.0 - Free anime streaming API
- 16 API endpoints powered by AniList GraphQL - Miruro pipe integration for streaming sources - 12 streaming providers (kiwi, pewe, bee, bonk, etc.) - M3U8 streaming URLs with subtitles and skip timestamps - In-memory caching with TTL - CORS support and health endpoint
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
const axios = require("axios");
|
||||
|
||||
const ANILIST_URL = "https://graphql.anilist.co";
|
||||
|
||||
const MEDIA_LIST_FIELDS = `
|
||||
id
|
||||
title { romaji english native }
|
||||
coverImage { large extraLarge }
|
||||
bannerImage
|
||||
format
|
||||
season
|
||||
seasonYear
|
||||
episodes
|
||||
duration
|
||||
status
|
||||
averageScore
|
||||
meanScore
|
||||
popularity
|
||||
favourites
|
||||
genres
|
||||
source
|
||||
countryOfOrigin
|
||||
isAdult
|
||||
studios(isMain: true) { nodes { name isAnimationStudio } }
|
||||
nextAiringEpisode { episode airingAt timeUntilAiring }
|
||||
startDate { year month day }
|
||||
endDate { year month day }
|
||||
`;
|
||||
|
||||
const MEDIA_FULL_FIELDS = `
|
||||
id
|
||||
idMal
|
||||
title { romaji english native }
|
||||
description(asHtml: false)
|
||||
coverImage { large extraLarge color }
|
||||
bannerImage
|
||||
format
|
||||
season
|
||||
seasonYear
|
||||
episodes
|
||||
duration
|
||||
status
|
||||
averageScore
|
||||
meanScore
|
||||
popularity
|
||||
favourites
|
||||
trending
|
||||
genres
|
||||
tags { name rank isMediaSpoiler }
|
||||
source
|
||||
countryOfOrigin
|
||||
isAdult
|
||||
hashtag
|
||||
synonyms
|
||||
siteUrl
|
||||
trailer { id site thumbnail }
|
||||
studios { nodes { id name isAnimationStudio siteUrl } }
|
||||
nextAiringEpisode { episode airingAt timeUntilAiring }
|
||||
startDate { year month day }
|
||||
endDate { year month day }
|
||||
characters(sort: [ROLE, RELEVANCE], perPage: 25) {
|
||||
edges {
|
||||
role
|
||||
node { id name { full native } image { large } }
|
||||
voiceActors(language: JAPANESE) { id name { full native } image { large } languageV2 }
|
||||
}
|
||||
}
|
||||
staff(sort: RELEVANCE, perPage: 25) {
|
||||
edges {
|
||||
role
|
||||
node { id name { full native } image { large } }
|
||||
}
|
||||
}
|
||||
relations {
|
||||
edges {
|
||||
relationType(version: 2)
|
||||
node {
|
||||
id
|
||||
title { romaji english native }
|
||||
coverImage { large }
|
||||
format
|
||||
type
|
||||
status
|
||||
episodes
|
||||
meanScore
|
||||
}
|
||||
}
|
||||
}
|
||||
recommendations(sort: RATING_DESC, perPage: 10) {
|
||||
nodes {
|
||||
rating
|
||||
mediaRecommendation {
|
||||
id
|
||||
title { romaji english native }
|
||||
coverImage { large }
|
||||
format
|
||||
episodes
|
||||
status
|
||||
meanScore
|
||||
averageScore
|
||||
}
|
||||
}
|
||||
}
|
||||
externalLinks { url site type }
|
||||
streamingEpisodes { title thumbnail url site }
|
||||
stats {
|
||||
scoreDistribution { score amount }
|
||||
statusDistribution { status amount }
|
||||
}
|
||||
`;
|
||||
|
||||
async function anilistQuery(query, variables = {}) {
|
||||
const body = { query, variables };
|
||||
const res = await axios.post(ANILIST_URL, body, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 15000,
|
||||
});
|
||||
if (res.status !== 200) throw new Error("AniList query failed");
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
async function searchAnime(query, page = 1, perPage = 20) {
|
||||
const gql = `
|
||||
query ($search: String, $page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
media(search: $search, type: ANIME, sort: SEARCH_MATCH) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { search: query, page, perPage });
|
||||
const pageData = data.Page;
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.media,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSuggestions(query) {
|
||||
const gql = `
|
||||
query ($search: String) {
|
||||
Page(page: 1, perPage: 8) {
|
||||
media(search: $search, type: ANIME, sort: SEARCH_MATCH) {
|
||||
id
|
||||
title { romaji english }
|
||||
coverImage { large }
|
||||
format
|
||||
status
|
||||
startDate { year }
|
||||
episodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { search: query });
|
||||
return data.Page.media.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title.english || item.title.romaji,
|
||||
title_romaji: item.title.romaji,
|
||||
poster: item.coverImage.large,
|
||||
format: item.format,
|
||||
status: item.status,
|
||||
year: item.startDate?.year,
|
||||
episodes: item.episodes,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getAnimeInfo(id) {
|
||||
const gql = `
|
||||
query ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
${MEDIA_FULL_FIELDS}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { id });
|
||||
return data.Media;
|
||||
}
|
||||
|
||||
async function getAnimeCharacters(id, page = 1, perPage = 25) {
|
||||
const gql = `
|
||||
query ($id: Int, $page: Int, $perPage: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
id
|
||||
title { romaji english }
|
||||
characters(sort: [ROLE, RELEVANCE], page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
edges {
|
||||
role
|
||||
node { id name { full native userPreferred } image { large medium } description gender favourites siteUrl }
|
||||
voiceActors { id name { full native } image { large } languageV2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { id, page, perPage });
|
||||
if (!data.Media) throw new Error("Anime not found");
|
||||
const chars = data.Media.characters;
|
||||
return {
|
||||
page: chars.pageInfo.currentPage,
|
||||
perPage: chars.pageInfo.perPage,
|
||||
total: chars.pageInfo.total,
|
||||
hasNextPage: chars.pageInfo.hasNextPage,
|
||||
characters: chars.edges,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAnimeRelations(id) {
|
||||
const gql = `
|
||||
query ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
id
|
||||
title { romaji english }
|
||||
relations {
|
||||
edges {
|
||||
relationType(version: 2)
|
||||
node {
|
||||
id title { romaji english native } coverImage { large }
|
||||
format type status episodes meanScore averageScore popularity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { id });
|
||||
if (!data.Media) throw new Error("Anime not found");
|
||||
return {
|
||||
id: data.Media.id,
|
||||
title: data.Media.title,
|
||||
relations: data.Media.relations.edges,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAnimeRecommendations(id, page = 1, perPage = 10) {
|
||||
const gql = `
|
||||
query ($id: Int, $page: Int, $perPage: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
id
|
||||
title { romaji english }
|
||||
recommendations(sort: RATING_DESC, page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
nodes {
|
||||
rating
|
||||
mediaRecommendation {
|
||||
id title { romaji english native } coverImage { large extraLarge }
|
||||
format episodes status meanScore averageScore popularity genres
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { id, page, perPage });
|
||||
if (!data.Media) throw new Error("Anime not found");
|
||||
const recs = data.Media.recommendations;
|
||||
return {
|
||||
page: recs.pageInfo.currentPage,
|
||||
perPage: recs.pageInfo.perPage,
|
||||
total: recs.pageInfo.total,
|
||||
hasNextPage: recs.pageInfo.hasNextPage,
|
||||
recommendations: recs.nodes,
|
||||
};
|
||||
}
|
||||
|
||||
async function getCollection(sortType, status = null, page = 1, perPage = 20) {
|
||||
const statusFilter = status ? `, status: ${status}` : "";
|
||||
const gql = `
|
||||
query ($page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
media(type: ANIME, sort: [${sortType}]${statusFilter}) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { page, perPage });
|
||||
const pageData = data.Page;
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.media,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSpotlight() {
|
||||
const gql = `
|
||||
query {
|
||||
Page(page: 1, perPage: 10) {
|
||||
media(sort: [TRENDING_DESC, POPULARITY_DESC], type: ANIME) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql);
|
||||
return data.Page.media;
|
||||
}
|
||||
|
||||
async function getSchedule(page = 1, perPage = 20) {
|
||||
const gql = `
|
||||
query ($page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
airingSchedules(notYetAired: true, sort: TIME) {
|
||||
episode
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
media { ${MEDIA_LIST_FIELDS} }
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { page, perPage });
|
||||
const pageData = data.Page;
|
||||
const results = pageData.airingSchedules.map((item) => ({
|
||||
...item.media,
|
||||
next_episode: item.episode,
|
||||
airingAt: item.airingAt,
|
||||
timeUntilAiring: item.timeUntilAiring,
|
||||
}));
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
async function filterAnime({ genre, tag, year, season, format, status, sort = "POPULARITY_DESC", page = 1, perPage = 20 }) {
|
||||
const SORT_MAP = {
|
||||
SCORE_DESC: "SCORE_DESC",
|
||||
POPULARITY_DESC: "POPULARITY_DESC",
|
||||
TRENDING_DESC: "TRENDING_DESC",
|
||||
START_DATE_DESC: "START_DATE_DESC",
|
||||
FAVOURITES_DESC: "FAVOURITES_DESC",
|
||||
UPDATED_AT_DESC: "UPDATED_AT_DESC",
|
||||
};
|
||||
|
||||
const args = ["type: ANIME", `sort: [${SORT_MAP[sort] || "POPULARITY_DESC"}]`];
|
||||
const variables = { page, perPage };
|
||||
const varTypes = ["$page: Int", "$perPage: Int"];
|
||||
|
||||
if (genre) { args.push("genre: $genre"); variables.genre = genre; varTypes.push("$genre: String"); }
|
||||
if (tag) { args.push("tag: $tag"); variables.tag = tag; varTypes.push("$tag: String"); }
|
||||
if (year) { args.push("seasonYear: $seasonYear"); variables.seasonYear = year; varTypes.push("$seasonYear: Int"); }
|
||||
if (season) { args.push("season: $season"); variables.season = season.toUpperCase(); varTypes.push("$season: MediaSeason"); }
|
||||
if (format) { args.push("format: $format"); variables.format = format.toUpperCase(); varTypes.push("$format: MediaFormat"); }
|
||||
if (status) { args.push("status: $status"); variables.status = status.toUpperCase(); varTypes.push("$status: MediaStatus"); }
|
||||
|
||||
const gql = `
|
||||
query (${varTypes.join(", ")}) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
media(${args.join(", ")}) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, variables);
|
||||
const pageData = data.Page;
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.media,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
anilistQuery,
|
||||
searchAnime,
|
||||
getSuggestions,
|
||||
getAnimeInfo,
|
||||
getAnimeCharacters,
|
||||
getAnimeRelations,
|
||||
getAnimeRecommendations,
|
||||
getCollection,
|
||||
getSpotlight,
|
||||
getSchedule,
|
||||
filterAnime,
|
||||
MEDIA_LIST_FIELDS,
|
||||
MEDIA_FULL_FIELDS,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
const cache = new Map();
|
||||
const MAX_CACHE_SIZE = 100;
|
||||
const DEFAULT_TTL = 60 * 1000;
|
||||
|
||||
function getCached(key) {
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return null;
|
||||
if (Date.now() > entry.expiry) {
|
||||
cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
function setCache(key, data, ttl = DEFAULT_TTL) {
|
||||
if (cache.size >= MAX_CACHE_SIZE) {
|
||||
const firstKey = cache.keys().next().value;
|
||||
cache.delete(firstKey);
|
||||
}
|
||||
cache.set(key, { data, expiry: Date.now() + ttl });
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
module.exports = { getCached, setCache, clearCache };
|
||||
@@ -0,0 +1,144 @@
|
||||
const axios = require("axios");
|
||||
const { Buffer } = require("buffer");
|
||||
const zlib = require("zlib");
|
||||
|
||||
const MIRURO_PIPE_URL = "https://www.miruro.tv/api/secure/pipe";
|
||||
const HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
||||
Referer: "https://www.miruro.tv/",
|
||||
};
|
||||
|
||||
function encodePipeRequest(payload) {
|
||||
const json = JSON.stringify(payload);
|
||||
return Buffer.from(json).toString("base64url");
|
||||
}
|
||||
|
||||
function decodePipeResponse(encodedStr) {
|
||||
try {
|
||||
const padded = encodedStr + "=".repeat((4 - (encodedStr.length % 4)) % 4);
|
||||
const compressed = Buffer.from(padded, "base64url");
|
||||
const decompressed = zlib.gunzipSync(compressed);
|
||||
return JSON.parse(decompressed.toString("utf-8"));
|
||||
} catch (e) {
|
||||
throw new Error("Failed to decode pipe response: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function translateId(encodedId) {
|
||||
try {
|
||||
const padded = encodedId + "=".repeat((4 - (encodedId.length % 4)) % 4);
|
||||
const decoded = Buffer.from(padded, "base64url").toString("utf-8");
|
||||
if (decoded.includes(":")) return decoded;
|
||||
return encodedId;
|
||||
} catch {
|
||||
return encodedId;
|
||||
}
|
||||
}
|
||||
|
||||
function deepTranslate(obj) {
|
||||
if (obj && typeof obj === "object") {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (key === "id" && typeof obj[key] === "string") {
|
||||
obj[key] = translateId(obj[key]);
|
||||
} else if (typeof obj[key] === "object") {
|
||||
deepTranslate(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function injectSourceSlugs(data, anilistId) {
|
||||
const providers = data.providers || {};
|
||||
for (const [provName, provData] of Object.entries(providers)) {
|
||||
if (!provData || typeof provData !== "object") continue;
|
||||
let episodes = provData.episodes;
|
||||
if (!episodes) continue;
|
||||
if (Array.isArray(episodes)) {
|
||||
provData.episodes = { sub: episodes };
|
||||
episodes = provData.episodes;
|
||||
}
|
||||
for (const [category, epList] of Object.entries(episodes)) {
|
||||
if (!Array.isArray(epList)) continue;
|
||||
for (const ep of epList) {
|
||||
if (ep.id && ep.number) {
|
||||
const origId = ep.id;
|
||||
const prefix = origId.includes(":") ? origId.split(":")[0] : origId;
|
||||
ep.id = `watch/${provName}/${anilistId}/${category}/${prefix}-${ep.number}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchRawEpisodes(anilistId) {
|
||||
const payload = {
|
||||
path: "episodes",
|
||||
method: "GET",
|
||||
query: { anilistId },
|
||||
body: null,
|
||||
version: "0.1.0",
|
||||
};
|
||||
const encodedReq = encodePipeRequest(payload);
|
||||
const res = await axios.get(`${MIRURO_PIPE_URL}?e=${encodedReq}`, {
|
||||
headers: HEADERS,
|
||||
timeout: 15000,
|
||||
});
|
||||
if (res.status !== 200) throw new Error(`Pipe request failed: ${res.status}`);
|
||||
const data = decodePipeResponse(res.text || res.data);
|
||||
return deepTranslate(data);
|
||||
}
|
||||
|
||||
async function getEpisodes(anilistId) {
|
||||
const data = await fetchRawEpisodes(anilistId);
|
||||
return injectSourceSlugs(data, anilistId);
|
||||
}
|
||||
|
||||
async function getSources(episodeId, provider, anilistId, category = "sub") {
|
||||
const encId = Buffer.from(episodeId).toString("base64url");
|
||||
const payload = {
|
||||
path: "sources",
|
||||
method: "GET",
|
||||
query: { episodeId: encId, provider, category, anilistId },
|
||||
body: null,
|
||||
version: "0.1.0",
|
||||
};
|
||||
const encodedReq = encodePipeRequest(payload);
|
||||
const res = await axios.get(`${MIRURO_PIPE_URL}?e=${encodedReq}`, {
|
||||
headers: HEADERS,
|
||||
timeout: 15000,
|
||||
});
|
||||
if (res.status !== 200) throw new Error(`Pipe sources request failed: ${res.status}`);
|
||||
return deepTranslate(decodePipeResponse(res.text || res.data));
|
||||
}
|
||||
|
||||
async function getWatchSources(provider, anilistId, category, slug) {
|
||||
const data = await fetchRawEpisodes(anilistId);
|
||||
const provData = (data.providers || {})[provider];
|
||||
if (!provData) throw new Error(`Provider ${provider} not found`);
|
||||
const episodes = provData.episodes?.[category] || [];
|
||||
let targetId = null;
|
||||
for (const ep of episodes) {
|
||||
const origId = ep.id || "";
|
||||
const prefix = origId.includes(":") ? origId.split(":")[0] : origId;
|
||||
const generated = `${prefix}-${ep.number}`;
|
||||
if (generated === slug) {
|
||||
targetId = origId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!targetId) throw new Error(`Episode slug '${slug}' not found for provider ${provider}`);
|
||||
return getSources(targetId, provider, anilistId, category);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEpisodes,
|
||||
getSources,
|
||||
getWatchSources,
|
||||
encodePipeRequest,
|
||||
decodePipeResponse,
|
||||
translateId,
|
||||
deepTranslate,
|
||||
injectSourceSlugs,
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const anilist = require("../helpers/anilist");
|
||||
const pipe = require("../helpers/pipe");
|
||||
const { getCached, setCache } = require("../helpers/cache");
|
||||
|
||||
function handleErrors(res, err) {
|
||||
console.error(err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
|
||||
// Search
|
||||
router.get("/search", async (req, res) => {
|
||||
try {
|
||||
const { query, page = 1, per_page = 20 } = req.query;
|
||||
if (!query) return res.status(400).json({ success: false, message: "query parameter is required" });
|
||||
const cacheKey = `search:${query}:${page}:${per_page}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.searchAnime(query, parseInt(page), parseInt(per_page));
|
||||
setCache(cacheKey, result);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Suggestions
|
||||
router.get("/suggestions", async (req, res) => {
|
||||
try {
|
||||
const { query } = req.query;
|
||||
if (!query) return res.status(400).json({ success: false, message: "query parameter is required" });
|
||||
const cacheKey = `suggestions:${query}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getSuggestions(query);
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Trending
|
||||
router.get("/trending", async (req, res) => {
|
||||
try {
|
||||
const { page = 1, per_page = 20 } = req.query;
|
||||
const cacheKey = `trending:${page}:${per_page}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getCollection("TRENDING_DESC", null, parseInt(page), parseInt(per_page));
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Popular
|
||||
router.get("/popular", async (req, res) => {
|
||||
try {
|
||||
const { page = 1, per_page = 20 } = req.query;
|
||||
const cacheKey = `popular:${page}:${per_page}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getCollection("POPULARITY_DESC", null, parseInt(page), parseInt(per_page));
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Upcoming
|
||||
router.get("/upcoming", async (req, res) => {
|
||||
try {
|
||||
const { page = 1, per_page = 20 } = req.query;
|
||||
const cacheKey = `upcoming:${page}:${per_page}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getCollection("POPULARITY_DESC", "NOT_YET_RELEASED", parseInt(page), parseInt(per_page));
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Recent
|
||||
router.get("/recent", async (req, res) => {
|
||||
try {
|
||||
const { page = 1, per_page = 20 } = req.query;
|
||||
const cacheKey = `recent:${page}:${per_page}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getCollection("START_DATE_DESC", "RELEASING", parseInt(page), parseInt(per_page));
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Spotlight
|
||||
router.get("/spotlight", async (req, res) => {
|
||||
try {
|
||||
const cacheKey = "spotlight";
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getSpotlight();
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Schedule
|
||||
router.get("/schedule", async (req, res) => {
|
||||
try {
|
||||
const { page = 1, per_page = 20 } = req.query;
|
||||
const cacheKey = `schedule:${page}:${per_page}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getSchedule(parseInt(page), parseInt(per_page));
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Filter
|
||||
router.get("/filter", async (req, res) => {
|
||||
try {
|
||||
const { genre, tag, year, season, format, status, sort = "POPULARITY_DESC", page = 1, per_page = 20 } = req.query;
|
||||
const cacheKey = `filter:${genre}:${tag}:${year}:${season}:${format}:${status}:${sort}:${page}:${per_page}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.filterAnime({
|
||||
genre, tag, year: year ? parseInt(year) : null, season, format, status, sort,
|
||||
page: parseInt(page), perPage: parseInt(per_page),
|
||||
});
|
||||
setCache(cacheKey, result);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Info
|
||||
router.get("/info/:id", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).json({ success: false, message: "Invalid AniList ID" });
|
||||
const cacheKey = `info:${id}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getAnimeInfo(id);
|
||||
setCache(cacheKey, result, 300 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Characters
|
||||
router.get("/anime/:id/characters", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { page = 1, per_page = 25 } = req.query;
|
||||
const result = await anilist.getAnimeCharacters(id, parseInt(page), parseInt(per_page));
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Relations
|
||||
router.get("/anime/:id/relations", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const result = await anilist.getAnimeRelations(id);
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Recommendations
|
||||
router.get("/anime/:id/recommendations", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { page = 1, per_page = 10 } = req.query;
|
||||
const result = await anilist.getAnimeRecommendations(id, parseInt(page), parseInt(per_page));
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Episodes
|
||||
router.get("/episodes/:id", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).json({ success: false, message: "Invalid AniList ID" });
|
||||
const cacheKey = `episodes:${id}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await pipe.getEpisodes(id);
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Sources (manual)
|
||||
router.get("/sources", async (req, res) => {
|
||||
try {
|
||||
const { episodeId, provider, anilistId, category = "sub" } = req.query;
|
||||
if (!episodeId || !provider || !anilistId) {
|
||||
return res.status(400).json({ success: false, message: "episodeId, provider, and anilistId are required" });
|
||||
}
|
||||
const result = await pipe.getSources(episodeId, provider, parseInt(anilistId), category);
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
// Watch (simple slug-based sources)
|
||||
router.get("/watch/:provider/:anilistId/:category/:slug", async (req, res) => {
|
||||
try {
|
||||
const { provider, anilistId, category, slug } = req.params;
|
||||
const result = await pipe.getWatchSources(provider, parseInt(anilistId), category, slug);
|
||||
res.json({ success: true, results: result });
|
||||
} catch (err) { handleErrors(res, err); }
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user