diff --git a/README.md b/README.md
index 260a1d6..d36bf6d 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,7 @@ flowchart TD
### ⚡ Core
- **Real-time scraping** from anikototv.to
- **Smart caching** with LRU and configurable TTL per endpoint
-- **33 RESTful endpoints** covering all data
+- **38 RESTful endpoints** covering all data
- **AJAX episode loading** for accurate data
- **Mapper API integration** for extra servers
- **Graceful error handling** per endpoint
@@ -320,7 +320,7 @@ AniKotoAPI/
│ │ ├── 📄 header.config.js # 📋 Request headers
│ │ └── 📄 ids.config.js # 🏷️ Genre/Type/Status/Source/Season ID mappings
│ │
-│ ├── 📂 controllers/ # 🎮 Route handlers (24 files)
+│ ├── 📂 controllers/ # 🎮 Route handlers (28 files)
│ │ ├── 📄 homeInfo.controller.js
│ │ ├── 📄 animeInfo.controller.js
│ │ ├── 📄 search.controller.js
@@ -332,7 +332,7 @@ AniKotoAPI/
│ │ ├── 📄 watchPage.controller.js
│ │ └── 📄 ... (15 more)
│ │
-│ ├── 📂 extractors/ # 🔍 HTML scrapers (26 files)
+│ ├── 📂 extractors/ # 🔍 HTML scrapers (28 files)
│ │ ├── 📄 homeInfo.extractor.js # 🏠 Home page data
│ │ ├── 📄 search.extractor.js # 🔍 Search results
│ │ ├── 📄 animeInfo.extractor.js # 📺 Anime details
@@ -2298,8 +2298,8 @@ CMD ["node", "server.js"]
|:---|:---|
| ⚡ Cached response | ~200ms |
| 🔄 Fresh fetch | ~1-3s |
-| 💾 Cache TTL | 5 minutes |
-| ⏱️ Timeout per request | 15 seconds |
+| 💾 Cache TTL | 3 min–1 hour (per-endpoint) |
+| ⏱️ Timeout per request | 30 seconds |
| 🎬 Total anime indexed | 10,000+ |
| 📦 Total codebase | ~120KB |
@@ -2390,7 +2390,7 @@ Yes! CORS is enabled for all origins (*). Just make fetch requests
🔄 How often does the data refresh?
-The cache TTL is 5 minutes. After that, the next request triggers a fresh scrape from anikototv.to. This keeps data relatively fresh while reducing load on the source site.
+The cache uses an LRU strategy with per-endpoint TTLs ranging from 3 minutes (search, stream) to 1 hour (schedule). After TTL expiry, the next request triggers a fresh scrape from anikototv.to.
diff --git a/agents/backend-architect.md b/agents/backend-architect.md
index 2344ece..d613eac 100644
--- a/agents/backend-architect.md
+++ b/agents/backend-architect.md
@@ -12,7 +12,7 @@ You are **Backend Architect** for AniKotoAPI, a free REST API scraping anime dat
## Your Identity
- **Project**: AniKotoAPI v2.2.0 — https://github.com/Shineii86/AniKotoAPI
-- **Stack**: Node.js (ESM modules), Express 4.21, Cheerio 1.0, Axios 1.8, compression 1.8
+- **Stack**: Node.js (ESM modules), Express 4.21, Cheerio 1.0, Axios 1.8, compression 1.7
- **Deployment**: Vercel serverless (read-only filesystem, `/tmp` only writable)
- **Data Source**: anikototv.to (5 mirror domains: anikototv.to, anikoto.cz, anikoto.me, anikoto.net, anikototv.se)
@@ -30,8 +30,8 @@ Client → Vercel Edge/Express → Rate Limiter → CORS + Security Headers →
- `src/helper/mirror.helper.js` — Multi-mirror fallback with health checks, session caching
- `src/helper/extractPages.helper.js` — Page fetching using `fetchWithMirror()`
- `src/helper/parseListItem.helper.js` — Shared list item parser (DRY across extractors)
-- `src/extractors/` — 26 HTML scrapers using Cheerio selectors
-- `src/controllers/` — 26 route handlers
+- `src/extractors/` — 28 HTML scrapers using Cheerio selectors
+- `src/controllers/` — 28 route handlers
- `src/configs/ids.config.js` — Genre/Type/Status/Rating/Sort/Source/Season ID mappings
### Endpoints (38 total)
diff --git a/agents/devops-automator.md b/agents/devops-automator.md
index a444d43..7945d25 100644
--- a/agents/devops-automator.md
+++ b/agents/devops-automator.md
@@ -91,7 +91,7 @@ git remote set-url origin https://github.com/Shineii86/AniKotoAPI.git
### Version Bump Checklist
1. [ ] Update `package.json` version
-2. [ ] Update README.md badge (`Version-2.0.0`)
+2. [ ] Update README.md badge (`Version-2.2.0`)
3. [ ] Add CHANGELOG.md entry (prepend at top)
4. [ ] Update OpenAPI spec version in `apiRoutes.js`
5. [ ] Update endpoint count if changed
diff --git a/agents/security-architect.md b/agents/security-architect.md
index 1f144c3..ce655b1 100644
--- a/agents/security-architect.md
+++ b/agents/security-architect.md
@@ -38,7 +38,7 @@ You are **Security Architect** for AniKotoAPI, responsible for securing a public
```javascript
// ALLOWED_ORIGINS env var (comma-separated)
// Default: allow all origins
-// Only GET method allowed
+// GET and POST methods allowed
// Handles OPTIONS preflight
```
@@ -99,8 +99,7 @@ You are **Security Architect** for AniKotoAPI, responsible for securing a public
| express | 4.21 | Check for known CVEs |
| axios | 1.8 | Check for SSRF bugs |
| cheerio | 1.0-rc.12 | Check for prototype pollution |
-| cors | 2.8 | Check for bypass issues |
-| compression | 1.8 | Check for BREACH attacks |
+| compression | 1.7 | Check for BREACH attacks |
| dotenv | 16.4 | Check for env injection |
## What You Do
diff --git a/agents/technical-writer.md b/agents/technical-writer.md
index 5514933..a31bee3 100644
--- a/agents/technical-writer.md
+++ b/agents/technical-writer.md
@@ -19,7 +19,7 @@ You are **Technical Writer** for AniKotoAPI, responsible for maintaining all doc
```
AniKotoAPI/
-├── README.md # Main docs (2200+ lines)
+├── README.md # Main docs (2600+ lines)
├── CHANGELOG.md # Version history (append only)
├── CONTRIBUTING.md # Contribution guidelines
├── docs/
@@ -27,7 +27,8 @@ AniKotoAPI/
│ ├── endpoints.md # Full API reference (38 endpoints)
│ ├── streaming.md # 3-step streaming flow guide
│ ├── examples.md # cURL, JS, Python examples
-│ └── architecture.md # Project structure, tech stack
+│ ├── architecture.md # Project structure, tech stack
+│ └── testing.md # Integration test suite & benchmarks
└── public/
├── index.html # Landing page
├── tos.html # Terms of Service
diff --git a/docs/architecture.md b/docs/architecture.md
index 2b34994..dc06226 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -178,8 +178,8 @@ Client Response
## Caching Strategy
-- **Type:** In-memory Map
-- **TTL:** 5 minutes (300,000ms)
+- **Type:** LRU Cache (Map-based with TTL expiration)
+- **TTL:** Per-endpoint (3 min for search/stream, 5 min for home/trending, 10 min for info, 1 hour for schedule)
- **Key:** Full request URL
- **Behavior:** First request fetches from source, subsequent requests served from cache
- **Reset:** TTL resets on each access
@@ -252,6 +252,5 @@ Error responses:
| express | ^4.21.0 | Web framework |
| cheerio | ^1.0.0-rc.12 | HTML parsing |
| axios | ^1.8.0 | HTTP requests |
-| cookie-parser | ^1.4.7 | Cookie parsing |
| dotenv | ^16.4.0 | Environment variables |
| compression | ^1.7.4 | Response compression |
diff --git a/docs/readme.md b/docs/readme.md
index f005a6a..6c9dff7 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -57,7 +57,7 @@ curl "https://anikototvapi.vercel.app/api/info?id=naruto-eybxz"
## Features
-- **33 Endpoints** — Home, search, info, episodes, streaming, schedule, seasons, watch order, download, and more
+- **38 Endpoints** — Home, search, info, episodes, streaming, schedule, seasons, watch order, download, and more
- **No API Key** — Just make requests, no registration needed
- **LRU Cache** — Configurable TTL per endpoint type (3min to 60min)
- **Multi-Mirror Fallback** — Automatic failover across 5 mirror domains
diff --git a/server.js b/server.js
index cb54cd8..38b07f9 100644
--- a/server.js
+++ b/server.js
@@ -25,7 +25,6 @@ import crypto from "crypto";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { createApiRoutes } from "./src/routes/apiRoutes.js";
-import { getCacheStats } from "./src/helper/cache.helper.js";
import { addCreatorInfo } from "./src/middleware/creatorInfo.js";
dotenv.config();
@@ -39,7 +38,6 @@ const PORT = process.env.PORT || 4444;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
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 ----
@@ -188,11 +186,6 @@ app.use((req, res, next) => {
next();
});
-// ---- FEATURE: Cache stats endpoint ----
-app.get("/api/cache/stats", (req, res) => {
- res.json({ success: true, results: getCacheStats() });
-});
-
// ---- FEATURE: Creator info middleware (injects attribution into all responses) ----
app.use(addCreatorInfo);
diff --git a/src/extractors/animeInfo.extractor.js b/src/extractors/animeInfo.extractor.js
index f201d19..78ccdb7 100644
--- a/src/extractors/animeInfo.extractor.js
+++ b/src/extractors/animeInfo.extractor.js
@@ -17,7 +17,6 @@
*/
import * as cheerio from "cheerio";
-import { URLS } from "../configs/dataUrl.js";
import { fetchWithMirror } from "../helper/mirror.helper.js";
// ══════════════════════════════════════════════════════════════
diff --git a/src/extractors/episodeList.extractor.js b/src/extractors/episodeList.extractor.js
index e4da435..8422441 100644
--- a/src/extractors/episodeList.extractor.js
+++ b/src/extractors/episodeList.extractor.js
@@ -17,8 +17,6 @@
*/
import * as cheerio from "cheerio";
-import { headers } from "../configs/header.config.js";
-import { URLS, BASE_URL } from "../configs/dataUrl.js";
import { fetchWithMirror } from "../helper/mirror.helper.js";
// ══════════════════════════════════════════════════════════════
diff --git a/src/extractors/episodeListAjax.extractor.js b/src/extractors/episodeListAjax.extractor.js
index ad5570b..f45c50e 100644
--- a/src/extractors/episodeListAjax.extractor.js
+++ b/src/extractors/episodeListAjax.extractor.js
@@ -17,7 +17,6 @@
*/
import * as cheerio from "cheerio";
-import axios from "axios";
import { headers } from "../configs/header.config.js";
import { fetchWithMirror } from "../helper/mirror.helper.js";
diff --git a/src/extractors/seasons.extractor.js b/src/extractors/seasons.extractor.js
index 5f5451b..7bb3864 100644
--- a/src/extractors/seasons.extractor.js
+++ b/src/extractors/seasons.extractor.js
@@ -40,7 +40,7 @@ import { fetchWithMirror } from "../helper/mirror.helper.js";
*/
const extractSeasons = async (slugOrId) => {
try {
- const path = /^\d+$/.test(slugOrId) ? `/watch/${slugOrId}` : `/watch/${slugOrId}`;
+ const path = `/watch/${slugOrId}`;
const { data } = await fetchWithMirror(path);
const html = typeof data === "string" ? data : String(data);
diff --git a/src/extractors/topAnimeRankings.extractor.js b/src/extractors/topAnimeRankings.extractor.js
index 8f478dd..93876a7 100644
--- a/src/extractors/topAnimeRankings.extractor.js
+++ b/src/extractors/topAnimeRankings.extractor.js
@@ -28,7 +28,7 @@ import { fetchWithMirror } from "../helper/mirror.helper.js";
* Fetches and parses the top anime rankings from the homepage.
* Supports switching between "top" (all-time) and "newest" rankings.
*
- * @param {string} sort - Ranking sort mode: "top" or "newest" (default: "top")
+ * @param {string} sort - Ranking sort mode: "day", "week", or "month" (default: "day")
* @returns {Promise>} Array of ranked anime objects
* @returns {number} return[].rank - Ranking position (1-9)
* @returns {string} return[].slug - URL slug for the anime
diff --git a/src/extractors/trending.extractor.js b/src/extractors/trending.extractor.js
index 71eee9d..64ce1a4 100644
--- a/src/extractors/trending.extractor.js
+++ b/src/extractors/trending.extractor.js
@@ -16,7 +16,6 @@
*/
import * as cheerio from "cheerio";
-import { URLS } from "../configs/dataUrl.js";
import { fetchWithMirror } from "../helper/mirror.helper.js";
// ══════════════════════════════════════════════════════════════
diff --git a/src/extractors/trendingSidebar.extractor.js b/src/extractors/trendingSidebar.extractor.js
index 6bfccf3..c89925a 100644
--- a/src/extractors/trendingSidebar.extractor.js
+++ b/src/extractors/trendingSidebar.extractor.js
@@ -16,7 +16,6 @@
*/
import * as cheerio from "cheerio";
-import { URLS } from "../configs/dataUrl.js";
import { fetchWithMirror } from "../helper/mirror.helper.js";
// ══════════════════════════════════════════════════════════════
diff --git a/src/extractors/upcomingAnime.extractor.js b/src/extractors/upcomingAnime.extractor.js
index 16d5b37..c8a6c5b 100644
--- a/src/extractors/upcomingAnime.extractor.js
+++ b/src/extractors/upcomingAnime.extractor.js
@@ -17,7 +17,6 @@
import * as cheerio from "cheerio";
import { fetchWithMirror } from "../helper/mirror.helper.js";
-import { parseListItems } from "../helper/parseListItem.helper.js";
// ══════════════════════════════════════════════════════════════
// UPCOMING ANIME EXTRACTION
diff --git a/src/extractors/watchOrder.extractor.js b/src/extractors/watchOrder.extractor.js
index d7e52e1..a9ad0a4 100644
--- a/src/extractors/watchOrder.extractor.js
+++ b/src/extractors/watchOrder.extractor.js
@@ -40,7 +40,7 @@ import { fetchWithMirror } from "../helper/mirror.helper.js";
*/
const extractWatchOrder = async (slugOrId) => {
try {
- const path = /^\d+$/.test(slugOrId) ? `/watch/${slugOrId}` : `/watch/${slugOrId}`;
+ const path = `/watch/${slugOrId}`;
const { data } = await fetchWithMirror(path);
const html = typeof data === "string" ? data : String(data);
diff --git a/src/extractors/watchPage.extractor.js b/src/extractors/watchPage.extractor.js
index 23e7a2a..3c6258b 100644
--- a/src/extractors/watchPage.extractor.js
+++ b/src/extractors/watchPage.extractor.js
@@ -16,7 +16,6 @@
*/
import * as cheerio from "cheerio";
-import { URLS, BASE_URL } from "../configs/dataUrl.js";
import { fetchWithMirror } from "../helper/mirror.helper.js";
// ══════════════════════════════════════════════════════════════
diff --git a/src/helper/mirror.helper.js b/src/helper/mirror.helper.js
index f6ff594..d45c2db 100644
--- a/src/helper/mirror.helper.js
+++ b/src/helper/mirror.helper.js
@@ -201,7 +201,7 @@ async function fetchWithMirror(path, options = {}) {
console.log(`[MIRROR] Switched to: ${mirror}`);
}
return {
- data: returnType === "text" ? response.data : response.data,
+ data: response.data,
mirror
};
}
diff --git a/src/helper/pagination.helper.js b/src/helper/pagination.helper.js
index 7b6d3fa..35cf0ff 100644
--- a/src/helper/pagination.helper.js
+++ b/src/helper/pagination.helper.js
@@ -45,7 +45,7 @@
const addPaginationMeta = (data, currentPage, itemsPerPage = 30) => {
const totalPages = data.totalPages || 1;
const page = parseInt(currentPage) || 1;
- const totalItems = data.data ? data.data.length * totalPages : 0;
+ const totalItems = data.data ? data.data.length + (totalPages - 1) * itemsPerPage : 0;
return {
...data,
diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js
index 0bb99f4..ecae657 100644
--- a/src/routes/apiRoutes.js
+++ b/src/routes/apiRoutes.js
@@ -238,7 +238,7 @@ app.use((req, res, next) => {
// NOTE: Validate URL is from known streaming domains
const allowedDomains = ["vidtube.site", "vidplay.site", "megaplay.buzz", "megaplay-1.buzz", "cdn.anipixcdn.co"];
const urlObj = new URL(url);
- const isAllowed = allowedDomains.some(d => urlObj.hostname.includes(d)) || urlObj.hostname.endsWith(".m3u8");
+ const isAllowed = allowedDomains.some(d => urlObj.hostname === d || urlObj.hostname.endsWith(`.${d}`)) || urlObj.pathname.endsWith(".m3u8");
if (!isAllowed) {
return res.status(403).json({ success: false, message: "Domain not allowed for proxy" });