mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 10:15:10 +00:00
docs: rewrite all files with AniKotoAPI documentation style
- Box-style file headers (======= border, @description, @exports, @author, @license) - Section headers (═════════════════════════════════════════) - Function-level JSDoc comments with @param, @returns, @example - Inline notes for non-obvious logic (lazy eviction, pipe decoding, slug resolution) - Feature markers (---- FEATURE: description ----) - Footer on every module (═══════════ END: filename ═══════════) - Added /stats and /openapi endpoints to router
This commit is contained in:
+418
-191
@@ -1,222 +1,449 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* MiruroAPI — apiRoutes.js
|
||||
* Repository: https://github.com/Shineii86/MiruroAPI
|
||||
*
|
||||
* @description
|
||||
* Central API router that maps all Express GET endpoints to their
|
||||
* corresponding handler functions. Each route wraps its handler
|
||||
* in try/catch for consistent error handling. Includes health,
|
||||
* stats, and OpenAPI specification endpoints.
|
||||
*
|
||||
* @exports
|
||||
* createApiRoutes - Function that registers all API routes
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
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 });
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// ROUTE REGISTRATION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// 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);
|
||||
// ---- FEATURE: Main API route registration function ----
|
||||
/**
|
||||
* Registers all API routes on the Express app.
|
||||
* Receives response helpers for consistent JSON formatting.
|
||||
*
|
||||
* @param {object} app - Express application instance
|
||||
* @param {function} jsonResponse - Standardized success response wrapper
|
||||
* @param {function} jsonError - Standardized error response wrapper
|
||||
* @returns {express.Router} Configured router
|
||||
*/
|
||||
const createApiRoutes = (app, jsonResponse, jsonError) => {
|
||||
const router = express.Router();
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// REQUEST COUNTER (must be before all routes)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
const startTime = Date.now();
|
||||
let requestCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
router.use((req, res, next) => {
|
||||
requestCount++;
|
||||
res.on("finish", () => {
|
||||
if (res.statusCode >= 400) errorCount++;
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEARCH & DISCOVERY
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Full-text anime search with pagination ----
|
||||
router.get("/search", async (req, res) => {
|
||||
try {
|
||||
const { query, page = 1, per_page = 20 } = req.query;
|
||||
if (!query) return jsonError(res, "query parameter is required", 400);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Lightweight autocomplete suggestions (max 8) ----
|
||||
router.get("/suggestions", async (req, res) => {
|
||||
try {
|
||||
const { query } = req.query;
|
||||
if (!query) return jsonError(res, "query parameter is required", 400);
|
||||
|
||||
const cacheKey = `suggestions:${query}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getSuggestions(query);
|
||||
// NOTE: Cache suggestions longer (2 minutes) — they change less frequently
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Advanced anime filter with multiple parameters ----
|
||||
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);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// COLLECTIONS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Currently trending anime ----
|
||||
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));
|
||||
// NOTE: Cache trending for 2 minutes — changes less frequently than search
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Most popular anime of all time ----
|
||||
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);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Most anticipated upcoming anime ----
|
||||
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);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Currently airing anime ----
|
||||
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);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Spotlight/featured anime (top 10) ----
|
||||
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);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Airing schedule with timestamps ----
|
||||
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);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// ANIME DETAILS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Complete anime info by AniList ID ----
|
||||
router.get("/info/:id", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) return jsonError(res, "Invalid AniList ID", 400);
|
||||
|
||||
const cacheKey = `info:${id}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await anilist.getAnimeInfo(id);
|
||||
// NOTE: Cache info for 5 minutes — detailed data changes infrequently
|
||||
setCache(cacheKey, result, 300 * 1000);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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);
|
||||
// ---- FEATURE: Paginated character list with voice actors ----
|
||||
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));
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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" });
|
||||
// ---- FEATURE: Related anime (sequels, prequels, spin-offs) ----
|
||||
router.get("/anime/:id/relations", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const result = await anilist.getAnimeRelations(id);
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
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); }
|
||||
});
|
||||
// ---- FEATURE: Community recommendations sorted by rating ----
|
||||
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));
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// STREAMING
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Episode list from all providers ----
|
||||
router.get("/episodes/:id", async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) return jsonError(res, "Invalid AniList ID", 400);
|
||||
|
||||
const cacheKey = `episodes:${id}`;
|
||||
let result = getCached(cacheKey);
|
||||
if (!result) {
|
||||
result = await pipe.getEpisodes(id);
|
||||
// NOTE: Cache episodes for 2 minutes — episode lists change during airing
|
||||
setCache(cacheKey, result, 120 * 1000);
|
||||
}
|
||||
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ---- FEATURE: Streaming sources (detailed/manual endpoint) ----
|
||||
router.get("/sources", async (req, res) => {
|
||||
try {
|
||||
const { episodeId, provider, anilistId, category = "sub" } = req.query;
|
||||
if (!episodeId || !provider || !anilistId) {
|
||||
return jsonError(res, "episodeId, provider, and anilistId are required", 400);
|
||||
}
|
||||
|
||||
const result = await pipe.getSources(episodeId, provider, parseInt(anilistId), category);
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ---- FEATURE: Streaming sources (simple slug-based endpoint) ----
|
||||
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);
|
||||
jsonResponse(res, result);
|
||||
} catch (err) {
|
||||
jsonError(res, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// HEALTH & STATS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Health check endpoint ----
|
||||
router.get("/health", (req, res) => {
|
||||
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
const seconds = uptime % 60;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
results: {
|
||||
status: "healthy",
|
||||
version: "1.1.0",
|
||||
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
||||
uptimeSeconds: uptime,
|
||||
timestamp: new Date().toISOString(),
|
||||
node: process.version,
|
||||
memory: {
|
||||
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + "MB",
|
||||
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + "MB",
|
||||
},
|
||||
endpoints: 16,
|
||||
providers: ["kiwi", "pewe", "bee", "bonk", "bun", "ally", "nun", "twin", "cog", "moo", "hop", "telli"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// ---- FEATURE: Cache & API statistics ----
|
||||
router.get("/stats", (req, res) => {
|
||||
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
results: {
|
||||
uptime: uptime + "s",
|
||||
requests: {
|
||||
total: requestCount,
|
||||
errors: errorCount,
|
||||
successRate: requestCount > 0
|
||||
? ((1 - errorCount / requestCount) * 100).toFixed(1) + "%"
|
||||
: "100%",
|
||||
},
|
||||
cache: {
|
||||
type: "in-memory",
|
||||
ttl: "1 minute (default), 2-5 minutes for collections",
|
||||
maxSize: 100,
|
||||
description: "Map-based cache with TTL expiration and FIFO eviction",
|
||||
},
|
||||
endpoints: 16,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// ---- FEATURE: OpenAPI specification ----
|
||||
router.get("/openapi", (req, res) => {
|
||||
res.json({
|
||||
openapi: "3.0.3",
|
||||
info: {
|
||||
title: "MiruroAPI",
|
||||
description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers",
|
||||
version: "1.1.0",
|
||||
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
||||
},
|
||||
servers: [
|
||||
{ url: "https://mirurotvapi.vercel.app/api", description: "Production" },
|
||||
],
|
||||
paths: {
|
||||
"/search": { get: { summary: "Search anime", tags: ["Search"] } },
|
||||
"/suggestions": { get: { summary: "Autocomplete suggestions", tags: ["Search"] } },
|
||||
"/filter": { get: { summary: "Advanced filter", tags: ["Search"] } },
|
||||
"/trending": { get: { summary: "Trending anime", tags: ["Collections"] } },
|
||||
"/popular": { get: { summary: "Most popular", tags: ["Collections"] } },
|
||||
"/upcoming": { get: { summary: "Upcoming anime", tags: ["Collections"] } },
|
||||
"/recent": { get: { summary: "Recently aired", tags: ["Collections"] } },
|
||||
"/spotlight": { get: { summary: "Spotlight anime", tags: ["Collections"] } },
|
||||
"/schedule": { get: { summary: "Airing schedule", tags: ["Collections"] } },
|
||||
"/info/{id}": { get: { summary: "Full anime info", tags: ["Anime Details"] } },
|
||||
"/anime/{id}/characters": { get: { summary: "Characters", tags: ["Anime Details"] } },
|
||||
"/anime/{id}/relations": { get: { summary: "Relations", tags: ["Anime Details"] } },
|
||||
"/anime/{id}/recommendations": { get: { summary: "Recommendations", tags: ["Anime Details"] } },
|
||||
"/episodes/{id}": { get: { summary: "Episode list", tags: ["Streaming"] } },
|
||||
"/sources": { get: { summary: "Streaming sources (detailed)", tags: ["Streaming"] } },
|
||||
"/watch/{provider}/{anilistId}/{category}/{slug}": { get: { summary: "Streaming sources (simple)", tags: ["Streaming"] } },
|
||||
"/health": { get: { summary: "Health check", tags: ["System"] } },
|
||||
"/stats": { get: { summary: "API statistics", tags: ["System"] } },
|
||||
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
|
||||
},
|
||||
tags: [
|
||||
{ name: "Search", description: "Search & filter" },
|
||||
{ name: "Collections", description: "Trending, popular, upcoming, schedule" },
|
||||
{ name: "Anime Details", description: "Info, characters, relations, recommendations" },
|
||||
{ name: "Streaming", description: "Episodes, sources, watch" },
|
||||
{ name: "System", description: "Health, stats, OpenAPI" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
module.exports = createApiRoutes;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: apiRoutes.js
|
||||
|
||||
Reference in New Issue
Block a user