mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 02:45:10 +00:00
feat: upgrade to v2.0.0 with multi-mirror fallback, new endpoints, and LRU cache
- Add multi-mirror fallback system with 5 mirror domains - Add LRU cache with configurable TTL per endpoint - Add response compression (gzip) - Add Seasons endpoint (/api/seasons/:id) - Add Watch Order endpoint (/api/watch-order/:id) - Add Latest Updated endpoint (/api/latest-updated) - Add Download endpoint (/api/download) - Add cache stats endpoint (/api/cache/stats) - Add mirror status/reset endpoints (/api/mirrors, /api/mirrors/reset) - Add configurable rate limiting with X-RateLimit headers - Add request timeouts and entity too large handling - Update documentation with all new endpoints - Update CHANGELOG.md with v2.0.0 entry - Update package.json version to 2.0.0 - Add 6 AI agent prompts for development
This commit is contained in:
@@ -19,11 +19,13 @@
|
||||
import dotenv from "dotenv";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import compression from "compression";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
import { createApiRoutes } from "./src/routes/apiRoutes.js";
|
||||
import { getCacheStats } from "./src/helper/cache.helper.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -39,6 +41,16 @@ const publicDir = path.join(process.cwd(), "public");
|
||||
const has404File = fs.existsSync(path.join(publicDir, "404.html"));
|
||||
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
|
||||
|
||||
// ---- FEATURE: Response compression ----
|
||||
app.use(compression({
|
||||
filter: (req, res) => {
|
||||
if (req.headers["x-no-compression"]) return false;
|
||||
return compression.filter(req, res);
|
||||
},
|
||||
level: 6,
|
||||
threshold: 1024
|
||||
}));
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CORS MIDDLEWARE
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
@@ -117,35 +129,62 @@ const jsonError = (res, message = "Internal server error", status = 500) =>
|
||||
// API ROUTES
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Rate limiting (100 requests per minute per IP) ----
|
||||
// ---- FEATURE: Rate limiting (configurable, default 100 requests per minute per IP) ----
|
||||
const requestCounts = new Map();
|
||||
const RATE_LIMIT = parseInt(process.env.RATE_LIMIT) || 100;
|
||||
const RATE_WINDOW = parseInt(process.env.RATE_WINDOW) || 60000;
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const now = Date.now();
|
||||
const windowMs = 60000;
|
||||
const maxRequests = 100;
|
||||
if (!requestCounts.has(ip)) {
|
||||
requestCounts.set(ip, []);
|
||||
}
|
||||
const timestamps = requestCounts.get(ip).filter(t => now - t < windowMs);
|
||||
const timestamps = requestCounts.get(ip).filter(t => now - t < RATE_WINDOW);
|
||||
requestCounts.set(ip, timestamps);
|
||||
if (timestamps.length >= maxRequests) {
|
||||
return res.status(429).json({ success: false, message: "Rate limit exceeded. Try again later." });
|
||||
if (timestamps.length >= RATE_LIMIT) {
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
message: "Rate limit exceeded. Try again later.",
|
||||
retryAfter: Math.ceil((timestamps[0] + RATE_WINDOW - now) / 1000)
|
||||
});
|
||||
}
|
||||
timestamps.push(now);
|
||||
res.setHeader("X-RateLimit-Limit", RATE_LIMIT);
|
||||
res.setHeader("X-RateLimit-Remaining", RATE_LIMIT - timestamps.length);
|
||||
next();
|
||||
});
|
||||
|
||||
// ---- FEATURE: Cache stats endpoint ----
|
||||
app.get("/api/cache/stats", (req, res) => {
|
||||
res.json({ success: true, results: getCacheStats() });
|
||||
});
|
||||
|
||||
createApiRoutes(app, jsonResponse, jsonError);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// 404 HANDLER
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Request timeout middleware (30 seconds) ----
|
||||
app.use((req, res, next) => {
|
||||
const timeout = parseInt(process.env.REQUEST_TIMEOUT) || 30000;
|
||||
req.setTimeout(timeout, () => {
|
||||
res.status(408).json({ success: false, message: "Request timeout" });
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// ---- FEATURE: Global error handler ----
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(err.status || 500).json({ success: false, message: err.message || "Internal server error" });
|
||||
console.error(`[ERROR] ${err.message}`, err.stack);
|
||||
if (err.type === 'entity.too.large') {
|
||||
return res.status(413).json({ success: false, message: "Request entity too large" });
|
||||
}
|
||||
res.status(err.status || 500).json({
|
||||
success: false,
|
||||
message: process.env.NODE_ENV === 'production' ? "Internal server error" : err.message
|
||||
});
|
||||
});
|
||||
|
||||
// ---- FEATURE: Catch-all 404 handler for undefined routes ----
|
||||
@@ -154,7 +193,16 @@ app.use((req, res) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
res.status(404).sendFile(filePath);
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: "Endpoint not found" });
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "Endpoint not found",
|
||||
availableEndpoints: [
|
||||
"/", "/search", "/info", "/episodes", "/servers", "/stream",
|
||||
"/spotlight", "/trending", "/top-ten", "/random", "/most-popular",
|
||||
"/new-release", "/schedule", "/genre/:genre", "/type/:type",
|
||||
"/status/:status", "/health", "/stats", "/openapi"
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user