mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 19:05:11 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c854d763c6 | ||
|
|
0f2258a8d5 |
@@ -2,3 +2,4 @@ node_modules/
|
|||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
|
*.pid
|
||||||
|
|||||||
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.5.1] - 2026-10-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Direct Embed Providers**: New `GET /api/stream/embeds` endpoint returning iframe-ready player URLs for **Megavid** (megavid.buzz), **AniXo** (anixo.buzz) and **MegaPlay** (megaplay.buzz) — bypasses Cloudflare entirely since playback happens in the provider's own iframe, no source-site AJAX scraping needed at watch time
|
||||||
|
- **Provider Config**: New `src/configs/embedProviders.config.js` — provider registry with URL builders (patterns verified against aluukill/AniCult's provider registry)
|
||||||
|
- **Embed Extractor**: New `src/extractors/embedProviders.extractor.js` — resolves MAL ID from the site's episode list (`data-mal`), looks up AniList ID via the public AniList GraphQL API (cached 60min), builds provider URLs
|
||||||
|
- **Embed Controller**: New `src/controllers/embedProviders.controller.js` — supports `?slug=`, `?mal=`, `?anilist=` (direct IDs skip slug scraping entirely), `?ep=`, `?lang=sub|dub`, `?provider=` comma filter, with 400 errors for unknown providers
|
||||||
|
- **IDs Cache TTL**: New `TTL.ids` (60min) for MAL/AniList ID mappings — these never change per anime
|
||||||
|
- **OpenAPI**: Registered `/stream/embeds` in the spec
|
||||||
|
- **Info External IDs**: `GET /api/info` responses now include `externalIds: { animeId, malId, anilistId }` — resolved server-side via the episode-list + AniList lookups (60min cache), best-effort (failed resolution returns nulls, never breaks the response); lets clients feed IDs straight into `/api/stream/embeds`
|
||||||
|
- **Latest-Episode Checker**: New `GET /api/episodes/latest?slug=` endpoint (new `episodeLatest.controller.js`) — returns only `latest: { episode_no, title, id, server_ids, mal_id, ... }` plus `animeId`/`totalEpisodes`, built for bot polling (10min cache, registered before `/episodes/:id` so the literal path wins)
|
||||||
|
- **Tests**: Added `Embed Providers`, `Embed (direct MAL)`, `Info External IDs` and `Latest Episode` cases to test.js
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Spotlight Carousel Selectors (BREAKING drift)**: Source site redesigned the `#hotest` carousel — updated `homeInfo.extractor.js` and `spotlight.extractor.js` for the new markup (`swiper-slide` without `.item`, `a.btn.btn-primary` instead of `a.play`, `.cover img` instead of `.image div[style]`, `h2.ak-title` instead of `h2.title`, `.ak-desc` instead of `.desc`, type/date/rating packed into `.ak-detail span.it`). Old-layout selectors kept as fallbacks (dual-selector). Spotlight was returning 0 items before this fix; now returns 9.
|
||||||
|
- **Spotlight Rating**: New layout packs rating into the `.ak-detail` list alongside year/date — numeric-with-decimal matching prevents the year (e.g. 2026) from being mistaken for the rating (e.g. 7.84)
|
||||||
|
- **Diagnostic Sweep Fixes**: Full-repo deep scan caught stale counts — README endpoints badge, docs/index.md badge/table/tree/sample counts (38→39 endpoints, 28→29 scrapers/handlers), docs/endpoints.md sample `/stats` response, public/index.html animated stat counter, docs/architecture.md extractor count and Episode & Stream category now list `embedProviders` (39 reached at that sweep, then 40 with the info-ID and latest-episode additions)
|
||||||
|
- **render.yaml**: Added missing trailing newline (EOF)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Version**: Bumped to 2.5.1 (single release consolidating domain sync, embed providers, docs pass and diagnostic sweep)
|
||||||
|
- **Mirror Domains Synced**: Verified all mirror domains against the official AniKoto domain hub (https://anikoto.site/#domains) — live check confirmed all 5 official domains return HTTP 200; replaced dead `anikoto.se` with official `anikototv.se` in `dataUrl.js` ALT_DOMAINS, `render.yaml` and README domain tables
|
||||||
|
- **Documentation Style**: Full repo-style pass on embed modules — inline `NOTE`/`TIP` markers for cache-first lookups, MAL-ID dedup, failure-tolerant AniList resolution, direct-ID fast path, input sanitization, cache-key composition; corrected JSDoc examples to current AniList ID values
|
||||||
|
- **Docs Sync**: `/api/stream/embeds` in docs/streaming.md (Direct Embed Providers section + TOC) and README streaming status table; project trees in README.md and docs/architecture.md list the new modules; `/episodes/latest` documented in docs/endpoints.md; all endpoint counts synced to 40 (README badge/highlights/checklist, landing-page metas and stat counter, docs/index.md badge/tables, docs/architecture.md controller counts, `/api/stats`); top-rankings `?sort=day|week|month` confirmed already supported — docs matched, no code change needed
|
||||||
|
- **Dockerfile**: Base image `node:18-alpine` → `node:22-alpine` — Node 18 is EOL (Sept 2025); aligns with README's Node 20+ guidance and current LTS
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```
|
||||||
|
GET /api/stream/embeds?slug=one-piece-odmau&ep=1150
|
||||||
|
GET /api/stream/embeds?mal=21&ep=1&lang=dub&provider=megavid,megaplay
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- All 77 JS files pass `node --check`; all relative imports resolve
|
||||||
|
- Official domains (all HTTP 200): `anikototv.to` (primary), `anikoto.cz`, `anikoto.me`, `anikoto.net`, `anikototv.se`; dead `anikoto.se` confirmed offline
|
||||||
|
- All 3 embed provider URLs live-verified HTTP 200 (Megavid serves embed-only page by design)
|
||||||
|
- Live smoke test 9/9 extractors passed; full API suite 38 passed / 0 failed / 2 optional skips (upstream AJAX blocks, documented)
|
||||||
|
- No secrets tracked in git
|
||||||
|
|
||||||
## [2.4.1] - 2026-08-27
|
## [2.4.1] - 2026-08-27
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
<img src="https://img.shields.io/badge/Cheerio-1.0-3572A5?style=flat-square&logoColor=white" alt="Cheerio"/>
|
<img src="https://img.shields.io/badge/Cheerio-1.0-3572A5?style=flat-square&logoColor=white" alt="Cheerio"/>
|
||||||
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
||||||
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
||||||
<img src="https://img.shields.io/badge/Version-2.4.1-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
<img src="https://img.shields.io/badge/Version-2.5.1-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
||||||
<img src="https://img.shields.io/badge/Endpoints-38-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
<img src="https://img.shields.io/badge/Endpoints-40-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
||||||
<img src="https://img.shields.io/badge/Anime-10000+-a855f7?style=flat-square&logoColor=white" alt="Anime Database"/>
|
<img src="https://img.shields.io/badge/Anime-10000+-a855f7?style=flat-square&logoColor=white" alt="Anime Database"/>
|
||||||
<img src="https://img.shields.io/badge/Maintenance-Active-22c55e?style=flat-square&logo=github&logoColor=white" alt="Maintenance"/>
|
<img src="https://img.shields.io/badge/Maintenance-Active-22c55e?style=flat-square&logo=github&logoColor=white" alt="Maintenance"/>
|
||||||
</p>
|
</p>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
### Why AniKotoAPI?
|
### Why AniKotoAPI?
|
||||||
|
|
||||||
- 🎬 **38 Endpoints** — Complete coverage of anikototv.to data
|
- 🎬 **40 Endpoints** — Complete coverage of anikototv.to data
|
||||||
- 🔍 **Full-Text Search** — Search anime by keyword with suggestions
|
- 🔍 **Full-Text Search** — Search anime by keyword with suggestions
|
||||||
- 📺 **Episode Lists** — AJAX-loaded episode data with server info
|
- 📺 **Episode Lists** — AJAX-loaded episode data with server info
|
||||||
- 🎯 **Smart Filtering** — Genre, type, status, rating, sort, season, year
|
- 🎯 **Smart Filtering** — Genre, type, status, rating, sort, season, year
|
||||||
@@ -133,6 +133,8 @@ flowchart TD
|
|||||||
> **Streaming endpoints (`/api/servers`, `/api/stream`, `/api/stream/resolve`) return errors when anikototv.to blocks AJAX requests from server-side IPs.**
|
> **Streaming endpoints (`/api/servers`, `/api/stream`, `/api/stream/resolve`) return errors when anikototv.to blocks AJAX requests from server-side IPs.**
|
||||||
>
|
>
|
||||||
> This is expected behavior when scraping from datacenter IPs (including Vercel's). AniKoto's bot protection blocks non-browser requests to their AJAX endpoints. Metadata endpoints (home, search, info, trending, etc.) work perfectly — only streaming is affected.
|
> This is expected behavior when scraping from datacenter IPs (including Vercel's). AniKoto's bot protection blocks non-browser requests to their AJAX endpoints. Metadata endpoints (home, search, info, trending, etc.) work perfectly — only streaming is affected.
|
||||||
|
>
|
||||||
|
> **✅ Alternative: `/api/stream/embeds` never touches AniKoto's AJAX layer** — it returns iframe-ready player URLs for Megavid/AniXo/MegaPlay built from MAL/AniList IDs, so playback works from any environment with zero Cloudflare friction.
|
||||||
|
|
||||||
| What Works | What's Blocked |
|
| What Works | What's Blocked |
|
||||||
|:---|:---|
|
|:---|:---|
|
||||||
@@ -164,7 +166,8 @@ FLARESOLVERR_URL=http://your-flaresolverr-host:8191
|
|||||||
### ⚡ Core
|
### ⚡ Core
|
||||||
- **Real-time scraping** from anikototv.to
|
- **Real-time scraping** from anikototv.to
|
||||||
- **Smart caching** with LRU and configurable TTL per endpoint
|
- **Smart caching** with LRU and configurable TTL per endpoint
|
||||||
- **38 RESTful endpoints** covering all data
|
- **40 RESTful endpoints** covering all data
|
||||||
|
- **Direct embed providers** (Megavid/AniXo/MegaPlay) — Cloudflare-free iframe playback
|
||||||
- **AJAX episode loading** for accurate data
|
- **AJAX episode loading** for accurate data
|
||||||
- **Mapper API integration** for extra servers
|
- **Mapper API integration** for extra servers
|
||||||
- **Graceful error handling** per endpoint
|
- **Graceful error handling** per endpoint
|
||||||
@@ -215,7 +218,7 @@ FLARESOLVERR_URL=http://your-flaresolverr-host:8191
|
|||||||
|
|
||||||
| Feature | Description | Status |
|
| Feature | Description | Status |
|
||||||
|:---|:---|:---:|
|
|:---|:---|:---:|
|
||||||
| 🎬 38 API Endpoints | Complete coverage of anime data | ✅ |
|
| 🎬 40 API Endpoints | Complete coverage of anime data | ✅ |
|
||||||
| 🔍 Full-Text Search | Keyword search with pagination | ✅ |
|
| 🔍 Full-Text Search | Keyword search with pagination | ✅ |
|
||||||
| 📺 Episode Lists | AJAX-loaded episode data | ✅ |
|
| 📺 Episode Lists | AJAX-loaded episode data | ✅ |
|
||||||
| 🎯 Advanced Filtering | Genre, type, status, rating, sort, season | ✅ |
|
| 🎯 Advanced Filtering | Genre, type, status, rating, sort, season | ✅ |
|
||||||
@@ -240,7 +243,7 @@ FLARESOLVERR_URL=http://your-flaresolverr-host:8191
|
|||||||
| **AniKoto** | `anikoto.cz` | ✅ Active | Mirror domain |
|
| **AniKoto** | `anikoto.cz` | ✅ Active | Mirror domain |
|
||||||
| **AniKoto** | `anikoto.me` | ✅ Active | Mirror domain |
|
| **AniKoto** | `anikoto.me` | ✅ Active | Mirror domain |
|
||||||
| **AniKoto** | `anikoto.net` | ✅ Active | Mirror domain |
|
| **AniKoto** | `anikoto.net` | ✅ Active | Mirror domain |
|
||||||
| **AniKoto** | `anikoto.se` | ✅ Active | Mirror domain |
|
| **AniKoto** | `anikototv.se` | ✅ Active | Mirror domain |
|
||||||
|
|
||||||
### Data Available
|
### Data Available
|
||||||
|
|
||||||
@@ -333,7 +336,7 @@ AniKotoAPI/
|
|||||||
│ │
|
│ │
|
||||||
│ ├── 📂 docs/ # 📚 API documentation
|
│ ├── 📂 docs/ # 📚 API documentation
|
||||||
│ │ ├── 📄 index.md # Overview, quick start
|
│ │ ├── 📄 index.md # Overview, quick start
|
||||||
│ │ ├── 📄 endpoints.md # Full API reference (38 endpoints)
|
│ │ ├── 📄 endpoints.md # Full API reference (40 endpoints)
|
||||||
│ │ ├── 📄 streaming.md # Streaming flow guide + proxy
|
│ │ ├── 📄 streaming.md # Streaming flow guide + proxy
|
||||||
│ │ ├── 📄 examples.md # Code examples (cURL, JS, Python)
|
│ │ ├── 📄 examples.md # Code examples (cURL, JS, Python)
|
||||||
│ │ ├── 📄 architecture.md # Project structure, tech stack
|
│ │ ├── 📄 architecture.md # Project structure, tech stack
|
||||||
@@ -344,32 +347,36 @@ AniKotoAPI/
|
|||||||
│ ├── 📂 configs/ # 🔧 Configuration files
|
│ ├── 📂 configs/ # 🔧 Configuration files
|
||||||
│ │ ├── 📄 dataUrl.js # 🌐 URL patterns for anikototv.to
|
│ │ ├── 📄 dataUrl.js # 🌐 URL patterns for anikototv.to
|
||||||
│ │ ├── 📄 header.config.js # 📋 Request headers
|
│ │ ├── 📄 header.config.js # 📋 Request headers
|
||||||
│ │ └── 📄 ids.config.js # 🏷️ Genre/Type/Status/Source/Season ID mappings
|
│ │ ├── 📄 ids.config.js # 🏷️ Genre/Type/Status/Source/Season ID mappings
|
||||||
|
│ │ └── 📄 embedProviders.config.js # 🆕 Direct embed provider registry (Megavid/AniXo/MegaPlay)
|
||||||
│ │
|
│ │
|
||||||
│ ├── 📂 controllers/ # 🎮 Route handlers (28 files)
|
│ ├── 📂 controllers/ # 🎮 Route handlers (30 files)
|
||||||
│ │ ├── 📄 homeInfo.controller.js
|
│ │ ├── 📄 homeInfo.controller.js
|
||||||
│ │ ├── 📄 animeInfo.controller.js
|
│ │ ├── 📄 animeInfo.controller.js
|
||||||
│ │ ├── 📄 search.controller.js
|
│ │ ├── 📄 search.controller.js
|
||||||
│ │ ├── 📄 episodeList.controller.js
|
│ │ ├── 📄 episodeList.controller.js
|
||||||
|
│ │ ├── 📄 episodeLatest.controller.js # 🆕 Latest-episode checker (bots)
|
||||||
│ │ ├── 📄 episodeListAjax.controller.js
|
│ │ ├── 📄 episodeListAjax.controller.js
|
||||||
│ │ ├── 📄 streamInfo.controller.js
|
│ │ ├── 📄 streamInfo.controller.js
|
||||||
│ │ ├── 📄 streamResolver.controller.js # 🆕 Stream URL resolution + quality detection
|
│ │ ├── 📄 streamResolver.controller.js # 🆕 Stream URL resolution + quality detection
|
||||||
|
│ │ ├── 📄 embedProviders.controller.js # 🆕 Direct embed provider URLs
|
||||||
│ │ ├── 📄 filter.controller.js
|
│ │ ├── 📄 filter.controller.js
|
||||||
│ │ ├── 📄 watchPage.controller.js
|
│ │ ├── 📄 watchPage.controller.js
|
||||||
│ │ └── 📄 ... (15 more)
|
│ │ └── 📄 ... (17 more)
|
||||||
│ │
|
│ │
|
||||||
│ ├── 📂 extractors/ # 🔍 HTML scrapers (28 files)
|
│ ├── 📂 extractors/ # 🔍 HTML scrapers (29 files)
|
||||||
│ │ ├── 📄 homeInfo.extractor.js # 🏠 Home page data
|
│ │ ├── 📄 homeInfo.extractor.js # 🏠 Home page data
|
||||||
│ │ ├── 📄 search.extractor.js # 🔍 Search results
|
│ │ ├── 📄 search.extractor.js # 🔍 Search results
|
||||||
│ │ ├── 📄 animeInfo.extractor.js # 📺 Anime details
|
│ │ ├── 📄 animeInfo.extractor.js # 📺 Anime details
|
||||||
│ │ ├── 📄 streamInfo.extractor.js # 📡 Streaming servers
|
│ │ ├── 📄 streamInfo.extractor.js # 📡 Streaming servers
|
||||||
│ │ ├── 📄 streamResolver.extractor.js # 🔗 Stream URL resolution, M3U8 parser
|
│ │ ├── 📄 streamResolver.extractor.js # 🔗 Stream URL resolution, M3U8 parser
|
||||||
|
│ │ ├── 📄 embedProviders.extractor.js # 🆕 MAL/AniList ID resolution + embed URLs
|
||||||
│ │ ├── 📄 filter.extractor.js # 🎯 Filtered results
|
│ │ ├── 📄 filter.extractor.js # 🎯 Filtered results
|
||||||
│ │ ├── 📄 watchPage.extractor.js # ▶️ Watch page data
|
│ │ ├── 📄 watchPage.extractor.js # ▶️ Watch page data
|
||||||
│ │ ├── 📄 seasons.extractor.js # 📅 Season info from watch page
|
│ │ ├── 📄 seasons.extractor.js # 📅 Season info from watch page
|
||||||
│ │ ├── 📄 watchOrder.extractor.js # 📋 Watch order from sidebar
|
│ │ ├── 📄 watchOrder.extractor.js # 📋 Watch order from sidebar
|
||||||
│ │ ├── 📄 episodeList.extractor.js # 📋 Episode lists
|
│ │ ├── 📄 episodeList.extractor.js # 📋 Episode lists
|
||||||
│ │ └── 📄 ... (16 more)
|
│ │ └── 📄 ... (17 more)
|
||||||
│ │
|
│ │
|
||||||
│ ├── 📂 helper/ # 🛠️ Utility functions
|
│ ├── 📂 helper/ # 🛠️ Utility functions
|
||||||
│ │ ├── 📄 cache.helper.js # 💾 In-memory caching
|
│ │ ├── 📄 cache.helper.js # 💾 In-memory caching
|
||||||
@@ -384,7 +391,7 @@ AniKotoAPI/
|
|||||||
│ │ └── 📄 creatorInfo.js # ✍️ Creator attribution injection
|
│ │ └── 📄 creatorInfo.js # ✍️ Creator attribution injection
|
||||||
│ │
|
│ │
|
||||||
│ └── 📂 routes/ # 🛤️ Express routes
|
│ └── 📂 routes/ # 🛤️ Express routes
|
||||||
│ ├── 📄 apiRoutes.js # 🌐 Main API routes (38 endpoints)
|
│ ├── 📄 apiRoutes.js # 🌐 Main API routes (40 endpoints)
|
||||||
│ └── 📄 category.route.js # 🏷️ Category routes
|
│ └── 📄 category.route.js # 🏷️ Category routes
|
||||||
│
|
│
|
||||||
├── 📄 server.js # 🚀 Express server entry point
|
├── 📄 server.js # 🚀 Express server entry point
|
||||||
@@ -2344,6 +2351,7 @@ CMD ["node", "server.js"]
|
|||||||
|
|
||||||
| Version | Date | Key Changes |
|
| Version | Date | Key Changes |
|
||||||
|:---|:---|:---|
|
|:---|:---|:---|
|
||||||
|
| **2.5.1** | 2026-10-13 | Domain sync vs official hub, spotlight carousel fix, direct embed providers (`/api/stream/embeds`), info `externalIds` + latest-episode checker, full docs pass, diagnostic sweep, Dockerfile Node 22 |
|
||||||
| **2.4.1** | 2026-08-27 | Security: fixed SSRF in stream proxies, dotenv load order fix, SECURITY.md |
|
| **2.4.1** | 2026-08-27 | Security: fixed SSRF in stream proxies, dotenv load order fix, SECURITY.md |
|
||||||
| **2.2.0** | 2026-07-28 | Streaming resolver, M3U8/TS proxy, 5 new endpoints, bug fixes, full diagnostic sweep |
|
| **2.2.0** | 2026-07-28 | Streaming resolver, M3U8/TS proxy, 5 new endpoints, bug fixes, full diagnostic sweep |
|
||||||
| **1.7.2** | 2026-06-08 | Full rebrand AniKotoAPI → AniKotoAPI, docs folder with real data, streaming fix |
|
| **1.7.2** | 2026-06-08 | Full rebrand AniKotoAPI → AniKotoAPI, docs folder with real data, streaming fix |
|
||||||
@@ -2457,7 +2465,7 @@ The API can access 10,000+ anime titles from anikototv.to. The most-popular endp
|
|||||||
|
|
||||||
### ✅ Completed
|
### ✅ Completed
|
||||||
|
|
||||||
- [x] 🎬 38 API endpoints covering all data
|
- [x] 🎬 40 API endpoints covering all data
|
||||||
- [x] 🔍 Full-text search with pagination
|
- [x] 🔍 Full-text search with pagination
|
||||||
- [x] 📺 Episode lists via AJAX loading
|
- [x] 📺 Episode lists via AJAX loading
|
||||||
- [x] 🎯 Advanced filtering (genre, type, status, rating, sort, season, year)
|
- [x] 🎯 Advanced filtering (genre, type, status, rating, sort, season, year)
|
||||||
@@ -2560,7 +2568,8 @@ git push origin feature/amazing-feature
|
|||||||
| [AniKoto CZ](https://anikoto.cz) | Mirror domain |
|
| [AniKoto CZ](https://anikoto.cz) | Mirror domain |
|
||||||
| [AniKoto ME](https://anikoto.me) | Mirror domain |
|
| [AniKoto ME](https://anikoto.me) | Mirror domain |
|
||||||
| [AniKoto NET](https://anikoto.net) | Mirror domain |
|
| [AniKoto NET](https://anikoto.net) | Mirror domain |
|
||||||
| [AniKoto SE](https://anikoto.se) | Mirror domain |
|
| [AniKoto SE](https://anikototv.se) | Mirror domain |
|
||||||
|
| [AniKoto Hub](https://anikoto.site) | Official domain verification hub |
|
||||||
|
|
||||||
### 🛠️ Technologies
|
### 🛠️ Technologies
|
||||||
|
|
||||||
|
|||||||
+23
-17
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
* AniKotoAPI — Architecture Document
|
* AniKotoAPI — Architecture Document
|
||||||
* Version : 2.4.1
|
* Version : 2.5.1
|
||||||
* Generated: 2026-08-14
|
* Generated: 2026-10-13
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*
|
*
|
||||||
* Free REST API for scraping anime data from anikototv.to
|
* Free REST API for scraping anime data from anikototv.to
|
||||||
@@ -67,13 +67,13 @@
|
|||||||
* │ │ │ │ │
|
* │ │ │ │ │
|
||||||
* │ │ ▼ │ │
|
* │ │ ▼ │ │
|
||||||
* │ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
* │ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||||||
* │ │ │ CONTROLLER LAYER (28 controllers) │ │ │
|
* │ │ │ CONTROLLER LAYER (30 controllers) │ │ │
|
||||||
* │ │ │ Validation · Parameter parsing · Response formatting │ │ │
|
* │ │ │ Validation · Parameter parsing · Response formatting │ │ │
|
||||||
* │ │ └──────────────────────┬───────────────────────────────────┘ │ │
|
* │ │ └──────────────────────┬───────────────────────────────────┘ │ │
|
||||||
* │ │ │ │ │
|
* │ │ │ │ │
|
||||||
* │ │ ▼ │ │
|
* │ │ ▼ │ │
|
||||||
* │ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
* │ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||||||
* │ │ │ EXTRACTOR / SCRAPER LAYER (28 extractors) │ │ │
|
* │ │ │ EXTRACTOR / SCRAPER LAYER (29 extractors) │ │ │
|
||||||
* │ │ │ HTML fetching · Cheerio parsing · Data transformation │ │ │
|
* │ │ │ HTML fetching · Cheerio parsing · Data transformation │ │ │
|
||||||
* │ │ └──────────────────────┬───────────────────────────────────┘ │ │
|
* │ │ └──────────────────────┬───────────────────────────────────┘ │ │
|
||||||
* │ │ │ │ │
|
* │ │ │ │ │
|
||||||
@@ -180,15 +180,18 @@
|
|||||||
* ├── src/
|
* ├── src/
|
||||||
* │ ├── configs/
|
* │ ├── configs/
|
||||||
* │ │ ├── header.config.js # Default HTTP headers for upstream requests
|
* │ │ ├── header.config.js # Default HTTP headers for upstream requests
|
||||||
* │ │ └── ids.config.js # Genre / Type / Status numeric ID mappings
|
* │ │ ├── ids.config.js # Genre / Type / Status numeric ID mappings
|
||||||
* │ ├── controllers/ # 28 route handlers (one per endpoint)
|
* │ │ └── embedProviders.config.js # Direct embed provider registry (Megavid/AniXo/MegaPlay)
|
||||||
|
* │ ├── controllers/ # 30 route handlers (one per endpoint)
|
||||||
* │ │ ├── homeInfo.controller.js
|
* │ │ ├── homeInfo.controller.js
|
||||||
* │ │ ├── animeInfo.controller.js
|
* │ │ ├── animeInfo.controller.js
|
||||||
* │ │ ├── search.controller.js
|
* │ │ ├── search.controller.js
|
||||||
* │ │ ├── episodeList.controller.js
|
* │ │ ├── episodeList.controller.js
|
||||||
|
* │ │ ├── episodeLatest.controller.js
|
||||||
* │ │ ├── episodeListAjax.controller.js
|
* │ │ ├── episodeListAjax.controller.js
|
||||||
* │ │ ├── streamInfo.controller.js
|
* │ │ ├── streamInfo.controller.js
|
||||||
* │ │ ├── streamResolver.controller.js
|
* │ │ ├── streamResolver.controller.js
|
||||||
|
* │ │ ├── embedProviders.controller.js
|
||||||
* │ │ ├── schedule.controller.js
|
* │ │ ├── schedule.controller.js
|
||||||
* │ │ ├── spotlight.controller.js
|
* │ │ ├── spotlight.controller.js
|
||||||
* │ │ ├── trending.controller.js
|
* │ │ ├── trending.controller.js
|
||||||
@@ -210,7 +213,7 @@
|
|||||||
* │ │ ├── recentlyUpdatedTabs.controller.js
|
* │ │ ├── recentlyUpdatedTabs.controller.js
|
||||||
* │ │ ├── completedAnime.controller.js
|
* │ │ ├── completedAnime.controller.js
|
||||||
* │ │ └── category.controller.js
|
* │ │ └── category.controller.js
|
||||||
* │ ├── extractors/ # 28 HTML scrapers (one per data source)
|
* │ ├── extractors/ # 29 HTML scrapers (one per data source)
|
||||||
* │ │ ├── homeInfo.extractor.js
|
* │ │ ├── homeInfo.extractor.js
|
||||||
* │ │ ├── animeInfo.extractor.js
|
* │ │ ├── animeInfo.extractor.js
|
||||||
* │ │ ├── search.extractor.js
|
* │ │ ├── search.extractor.js
|
||||||
@@ -218,6 +221,7 @@
|
|||||||
* │ │ ├── episodeListAjax.extractor.js
|
* │ │ ├── episodeListAjax.extractor.js
|
||||||
* │ │ ├── streamInfo.extractor.js
|
* │ │ ├── streamInfo.extractor.js
|
||||||
* │ │ ├── streamResolver.extractor.js
|
* │ │ ├── streamResolver.extractor.js
|
||||||
|
* │ │ ├── embedProviders.extractor.js
|
||||||
* │ │ ├── schedule.extractor.js
|
* │ │ ├── schedule.extractor.js
|
||||||
* │ │ ├── spotlight.extractor.js
|
* │ │ ├── spotlight.extractor.js
|
||||||
* │ │ ├── trending.extractor.js
|
* │ │ ├── trending.extractor.js
|
||||||
@@ -297,7 +301,7 @@
|
|||||||
* └────────────────────────────────────────────────────────────────────────┘
|
* └────────────────────────────────────────────────────────────────────────┘
|
||||||
*
|
*
|
||||||
* ┌────────────────────────────────────────────────────────────────────────┐
|
* ┌────────────────────────────────────────────────────────────────────────┐
|
||||||
* │ Controllers (28 handlers) │
|
* │ Controllers (30 handlers) │
|
||||||
* ├────────────────────────────────────────────────────────────────────────┤
|
* ├────────────────────────────────────────────────────────────────────────┤
|
||||||
* │ One controller per API endpoint. Responsibilities: │
|
* │ One controller per API endpoint. Responsibilities: │
|
||||||
* │ • Extract and validate request parameters (query, params, body) │
|
* │ • Extract and validate request parameters (query, params, body) │
|
||||||
@@ -308,7 +312,7 @@
|
|||||||
* └────────────────────────────────────────────────────────────────────────┘
|
* └────────────────────────────────────────────────────────────────────────┘
|
||||||
*
|
*
|
||||||
* ┌────────────────────────────────────────────────────────────────────────┐
|
* ┌────────────────────────────────────────────────────────────────────────┐
|
||||||
* │ Extractors (28 scrapers) │
|
* │ Extractors (29 scrapers) │
|
||||||
* ├────────────────────────────────────────────────────────────────────────┤
|
* ├────────────────────────────────────────────────────────────────────────┤
|
||||||
* │ One extractor per data source on the target site. Responsibilities: │
|
* │ One extractor per data source on the target site. Responsibilities: │
|
||||||
* │ • Build the upstream URL (via mirror.helper) │
|
* │ • Build the upstream URL (via mirror.helper) │
|
||||||
@@ -360,7 +364,7 @@
|
|||||||
* │ Routes │
|
* │ Routes │
|
||||||
* ├────────────────────────────────────────────────────────────────────────┤
|
* ├────────────────────────────────────────────────────────────────────────┤
|
||||||
* │ apiRoutes.js │
|
* │ apiRoutes.js │
|
||||||
* │ • Defines all 28 endpoint paths with HTTP methods │
|
* │ • Defines all 40 endpoint paths with HTTP methods │
|
||||||
* │ • Maps paths to controller functions │
|
* │ • Maps paths to controller functions │
|
||||||
* │ • Embeds OpenAPI 3.0 spec for auto-documentation │
|
* │ • Embeds OpenAPI 3.0 spec for auto-documentation │
|
||||||
* │ │
|
* │ │
|
||||||
@@ -778,7 +782,7 @@
|
|||||||
* │ INTEGRATION TESTS (test.js) │
|
* │ INTEGRATION TESTS (test.js) │
|
||||||
* ├──────────────────────────────────────────────────────────────────────┤
|
* ├──────────────────────────────────────────────────────────────────────┤
|
||||||
* │ │
|
* │ │
|
||||||
* │ • 35 end-to-end tests covering all 28+ endpoints. │
|
* │ • 40 end-to-end tests covering all 40+ endpoints. │
|
||||||
* │ • Each test hits the live API and validates: │
|
* │ • Each test hits the live API and validates: │
|
||||||
* │ - HTTP status code (200, or expected error code) │
|
* │ - HTTP status code (200, or expected error code) │
|
||||||
* │ - Response shape (success: true, data present) │
|
* │ - Response shape (success: true, data present) │
|
||||||
@@ -832,12 +836,13 @@
|
|||||||
* │ VERSION HISTORY │
|
* │ VERSION HISTORY │
|
||||||
* ├──────────────────────────────────────────────────────────────────────┤
|
* ├──────────────────────────────────────────────────────────────────────┤
|
||||||
* │ │
|
* │ │
|
||||||
* │ v2.4.1 (Current) │
|
* │ v2.5.1 (Current) │
|
||||||
* │ • 28 endpoints, 28 extractors │
|
* │ • 40 endpoints, 29 extractors, 30 controllers │
|
||||||
* │ • LRU cache with per-endpoint TTL │
|
* │ • Cache TTLs per endpoint + 60min MAL/AniList ID cache │
|
||||||
* │ • 5-mirror failover system │
|
* │ • 5-mirror failover system │
|
||||||
* │ • Vercel serverless deployment │
|
* │ • Vercel serverless deployment │
|
||||||
* │ • 35 integration tests │
|
* │ • Direct embed providers (Megavid/AniXo/MegaPlay) │
|
||||||
|
* │ • 40 integration tests │
|
||||||
* │ • OpenAPI 3.0 spec embedded in apiRoutes.js │
|
* │ • OpenAPI 3.0 spec embedded in apiRoutes.js │
|
||||||
* │ │
|
* │ │
|
||||||
* └──────────────────────────────────────────────────────────────────────┘
|
* └──────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -861,13 +866,14 @@
|
|||||||
* │ Search & Filter │ search, filter, azList, category │
|
* │ Search & Filter │ search, filter, azList, category │
|
||||||
* │ Anime Detail │ animeInfo, watchPage, status │
|
* │ Anime Detail │ animeInfo, watchPage, status │
|
||||||
* │ Episode & Stream │ episodeList, episodeListAjax, │
|
* │ Episode & Stream │ episodeList, episodeListAjax, │
|
||||||
* │ │ streamInfo, streamResolver, download │
|
* │ │ streamInfo, streamResolver, download,│
|
||||||
|
* │ │ embedProviders, episodeLatest │
|
||||||
* │ Lists │ newRelease, trendingSidebar, │
|
* │ Lists │ newRelease, trendingSidebar, │
|
||||||
* │ │ topAnimeRankings, recentlyUpdated, │
|
* │ │ topAnimeRankings, recentlyUpdated, │
|
||||||
* │ │ completedAnime, upcomingAnime │
|
* │ │ completedAnime, upcomingAnime │
|
||||||
* │ Meta │ seasons, watchOrder │
|
* │ Meta │ seasons, watchOrder │
|
||||||
* ├─────────────────────────────┼────────────────────────────────────────┤
|
* ├─────────────────────────────┼────────────────────────────────────────┤
|
||||||
* │ TOTAL │ 28 endpoints │
|
* │ TOTAL │ 40 endpoints │
|
||||||
* └─────────────────────────────┴────────────────────────────────────────┘
|
* └─────────────────────────────┴────────────────────────────────────────┘
|
||||||
*
|
*
|
||||||
* ┌──────────────────────────────────────────────────────────────────────┐
|
* ┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
|||||||
+93
-4
@@ -164,7 +164,7 @@ curl "https://anikototvapi.vercel.app/api/search?keyword=naruto"
|
|||||||
|
|
||||||
## GET /info
|
## GET /info
|
||||||
|
|
||||||
Get detailed info about an anime.
|
Get detailed info about an anime. Includes `externalIds` (MAL/AniList IDs) that can be passed directly to `/stream/embeds`.
|
||||||
|
|
||||||
| Param | Type | Required | Description |
|
| Param | Type | Required | Description |
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
@@ -172,6 +172,8 @@ Get detailed info about an anime.
|
|||||||
|
|
||||||
> **Important:** Slugs now include random hashes (e.g., `one-piece-odmau`). Get valid slugs from `/search` or `/most-popular` results.
|
> **Important:** Slugs now include random hashes (e.g., `one-piece-odmau`). Get valid slugs from `/search` or `/most-popular` results.
|
||||||
|
|
||||||
|
> **Note:** `externalIds` is resolved server-side (cached 60 min) and is best-effort — if ID resolution fails it comes back as `null` values, the rest of the response is unaffected.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -203,7 +205,12 @@ curl "https://anikototvapi.vercel.app/api/info?id=naruto-shippuuden-movie-6-road
|
|||||||
"producers": ["TV Tokyo", "Shueisha"],
|
"producers": ["TV Tokyo", "Shueisha"],
|
||||||
"genres": ["Action", "Comedy", "Supernatural"],
|
"genres": ["Action", "Comedy", "Supernatural"],
|
||||||
"rating": "PG-13",
|
"rating": "PG-13",
|
||||||
"reviewCount": "0"
|
"reviewCount": "0",
|
||||||
|
"externalIds": {
|
||||||
|
"animeId": 1642,
|
||||||
|
"malId": "21",
|
||||||
|
"anilistId": "21"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -254,6 +261,46 @@ curl "https://anikototvapi.vercel.app/api/episodes/958"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## GET /episodes/latest
|
||||||
|
|
||||||
|
Latest-episode checker — returns only the newest episode plus summary counts. Built for bots and notification jobs ("is there a new episode?") that shouldn't download the full episode list.
|
||||||
|
|
||||||
|
| Param | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `slug` | string | Yes | Anime slug or animeId |
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://anikototvapi.vercel.app/api/episodes/latest?slug=one-piece-odmau"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"results": {
|
||||||
|
"slug": "one-piece-odmau",
|
||||||
|
"animeId": 1642,
|
||||||
|
"totalEpisodes": 1165,
|
||||||
|
"latest": {
|
||||||
|
"episode_no": 1165,
|
||||||
|
"title": "",
|
||||||
|
"id": "50167",
|
||||||
|
"slug": "1165",
|
||||||
|
"href": "#",
|
||||||
|
"server_ids": "dXNCT3hNQzk3THhSTW8ySnM5...",
|
||||||
|
"mal_id": "21"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Tip:** Poll this endpoint, compare `results.latest.episode_no` with your stored value, and call `/servers?ids=results.latest.server_ids` only when it changes. Responses are cached for 10 minutes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## GET /servers
|
## GET /servers
|
||||||
|
|
||||||
Get available servers for an episode.
|
Get available servers for an episode.
|
||||||
@@ -835,6 +882,48 @@ Returns array of quality objects with url, quality label, resolution, bandwidth,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## GET /stream/embeds
|
||||||
|
|
||||||
|
Direct embed provider URLs (Megavid / AniXo / MegaPlay) — iframe-ready players that bypass Cloudflare entirely. Playback happens inside the provider's own iframe; no source-site AJAX scraping at watch time.
|
||||||
|
|
||||||
|
| Param | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `slug` | string | One of* | Anime slug — MAL/AniList IDs resolved automatically |
|
||||||
|
| `mal` | string | One of* | Direct MAL ID (skips slug lookup) |
|
||||||
|
| `anilist` | string | One of* | Direct AniList ID (skips slug lookup) |
|
||||||
|
| `ep` | number | No | Episode number (default 1) |
|
||||||
|
| `lang` | string | No | `sub` or `dub` (default `sub`) |
|
||||||
|
| `provider` | string | No | Comma-separated filter: `megaplay`, `anixo`, `megavid` |
|
||||||
|
|
||||||
|
\* Provide at least one of `slug`, `mal`, or `anilist`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://anikototvapi.vercel.app/api/stream/embeds?slug=one-piece-odmau&ep=1"
|
||||||
|
curl "https://anikototvapi.vercel.app/api/stream/embeds?mal=21&ep=1&lang=dub&provider=megavid"
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns resolved IDs plus iframe-ready URLs per provider:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"results": {
|
||||||
|
"slug": "one-piece-odmau",
|
||||||
|
"episode": 1,
|
||||||
|
"lang": "sub",
|
||||||
|
"ids": { "animeId": 1642, "malId": "21", "anilistId": "21" },
|
||||||
|
"providers": [
|
||||||
|
{ "id": "megaplay", "name": "MegaPlay", "url": "https://megaplay.buzz/stream/mal/21/1/sub", "iframeReady": true },
|
||||||
|
{ "id": "anixo", "name": "AniXo", "url": "https://anixo.buzz/embed/ani/21/1/sub?color=%23e63946", "iframeReady": true },
|
||||||
|
{ "id": "megavid", "name": "Megavid", "url": "https://megavid.buzz/mal/21/1/sub?color=%23e63946&autoplay=true", "iframeReady": true }
|
||||||
|
],
|
||||||
|
"unavailable": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## GET /stream/proxy
|
## GET /stream/proxy
|
||||||
|
|
||||||
M3U8 playlist proxy — rewrites relative URLs to proxy endpoints for CORS-free playback.
|
M3U8 playlist proxy — rewrites relative URLs to proxy endpoints for CORS-free playback.
|
||||||
@@ -884,7 +973,7 @@ curl "https://anikototvapi.vercel.app/api/health"
|
|||||||
"success": true,
|
"success": true,
|
||||||
"results": {
|
"results": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "2.4.1",
|
"version": "2.5.1",
|
||||||
"uptime": "0h 1m 21s",
|
"uptime": "0h 1m 21s",
|
||||||
"uptimeSeconds": 81,
|
"uptimeSeconds": 81,
|
||||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||||
@@ -924,7 +1013,7 @@ curl "https://anikototvapi.vercel.app/api/stats"
|
|||||||
"ttl": "5 minutes",
|
"ttl": "5 minutes",
|
||||||
"description": "Map-based cache with TTL expiration"
|
"description": "Map-based cache with TTL expiration"
|
||||||
},
|
},
|
||||||
"endpoints": 38,
|
"endpoints": 40,
|
||||||
"timestamp": "2026-07-30T02:20:46.205Z"
|
"timestamp": "2026-07-30T02:20:46.205Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@ curl -s "https://anikototvapi.vercel.app/api/health" | python3 -m json.tool
|
|||||||
"success": true,
|
"success": true,
|
||||||
"results": {
|
"results": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "2.4.1",
|
"version": "2.5.1",
|
||||||
"uptime": "0h 1m 21s",
|
"uptime": "0h 1m 21s",
|
||||||
"uptimeSeconds": 81,
|
"uptimeSeconds": 81,
|
||||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||||
@@ -2036,7 +2036,7 @@ curl -s "https://anikototvapi.vercel.app/api/health" | python3 -m json.tool
|
|||||||
"success": true,
|
"success": true,
|
||||||
"results": {
|
"results": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "2.4.1",
|
"version": "2.5.1",
|
||||||
"uptime": "0h 1m 21s",
|
"uptime": "0h 1m 21s",
|
||||||
"uptimeSeconds": 81,
|
"uptimeSeconds": 81,
|
||||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||||
|
|||||||
+11
-11
@@ -1,13 +1,13 @@
|
|||||||
# // === HEADER ===
|
# // === HEADER ===
|
||||||
# AniKotoAPI v2.4.1
|
# AniKotoAPI v2.5.1
|
||||||
# Free REST API for Anime Data
|
# Free REST API for Anime Data
|
||||||
# Scraping anikototv.to with Cheerio
|
# Scraping anikototv.to with Cheerio
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
> A fast, free REST API that scrapes anime data from **anikototv.to**.
|
> A fast, free REST API that scrapes anime data from **anikototv.to**.
|
||||||
@@ -70,9 +70,9 @@ That's it. No API keys required.
|
|||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **38+ Endpoints** | Full coverage of anime data, streaming, and discovery |
|
| **40+ Endpoints** | Full coverage of anime data, streaming, and discovery |
|
||||||
| **28 HTML Scrapers** | Cheerio-based scrapers for robust data extraction |
|
| **29 HTML Scrapers** | Cheerio-based scrapers for robust data extraction |
|
||||||
| **28 Route Handlers** | Organized controllers with clean separation of concerns |
|
| **30 Route Handlers** | Organized controllers with clean separation of concerns |
|
||||||
| **5 Mirror Domains** | Automatic failover across multiple source domains |
|
| **5 Mirror Domains** | Automatic failover across multiple source domains |
|
||||||
| **LRU Cache** | Per-endpoint TTL caching for performance |
|
| **LRU Cache** | Per-endpoint TTL caching for performance |
|
||||||
| **Rate Limiting** | 100 requests per 15 minutes per IP |
|
| **Rate Limiting** | 100 requests per 15 minutes per IP |
|
||||||
@@ -194,12 +194,12 @@ AniKotoAPI/
|
|||||||
│ ├── info.js # Anime info
|
│ ├── info.js # Anime info
|
||||||
│ ├── search.js # Search
|
│ ├── search.js # Search
|
||||||
│ ├── stream.js # Streaming
|
│ ├── stream.js # Streaming
|
||||||
│ └── ... # 38+ route handlers
|
│ └── ... # 40 route handlers
|
||||||
├── scrapers/ # Cheerio HTML scrapers
|
├── scrapers/ # Cheerio HTML scrapers
|
||||||
│ ├── home.js
|
│ ├── home.js
|
||||||
│ ├── info.js
|
│ ├── info.js
|
||||||
│ ├── search.js
|
│ ├── search.js
|
||||||
│ └── ... # 28 scrapers
|
│ └── ... # 29 scrapers
|
||||||
├── utils/ # Shared utilities
|
├── utils/ # Shared utilities
|
||||||
│ ├── cache.js # LRU cache with TTL
|
│ ├── cache.js # LRU cache with TTL
|
||||||
│ ├── rateLimit.js # IP-based rate limiter
|
│ ├── rateLimit.js # IP-based rate limiter
|
||||||
@@ -218,8 +218,8 @@ AniKotoAPI/
|
|||||||
|
|
||||||
### Key Design Patterns
|
### Key Design Patterns
|
||||||
|
|
||||||
- **Scraping Layer**: 28 Cheerio-based scrapers parse HTML from anikototv.to, isolating page-specific logic from route handlers.
|
- **Scraping Layer**: 29 Cheerio-based scrapers parse HTML from anikototv.to, isolating page-specific logic from route handlers.
|
||||||
- **Controller Layer**: 28 route handlers map HTTP requests to scraper calls, handle validation, and format responses.
|
- **Controller Layer**: 30 route handlers map HTTP requests to scraper calls, handle validation, and format responses.
|
||||||
- **Cache Layer**: LRU cache with per-endpoint TTL reduces redundant scraping. Hot endpoints cache for 2–5 minutes; cold endpoints for 10+ minutes.
|
- **Cache Layer**: LRU cache with per-endpoint TTL reduces redundant scraping. Hot endpoints cache for 2–5 minutes; cold endpoints for 10+ minutes.
|
||||||
- **Mirror Layer**: 5 source domains with automatic failover. If one domain is slow or down, requests transparently fall back to the next mirror.
|
- **Mirror Layer**: 5 source domains with automatic failover. If one domain is slow or down, requests transparently fall back to the next mirror.
|
||||||
- **Rate Limiter**: Token bucket algorithm, 100 requests per 15-minute window per client IP.
|
- **Rate Limiter**: Token bucket algorithm, 100 requests per 15-minute window per client IP.
|
||||||
@@ -350,7 +350,7 @@ All errors return JSON with a consistent structure:
|
|||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| [endpoints.md](./endpoints.md) | Full API reference for all 38+ endpoints |
|
| [endpoints.md](./endpoints.md) | Full API reference for all 40+ endpoints |
|
||||||
| [streaming.md](./streaming.md) | Complete streaming workflow guide |
|
| [streaming.md](./streaming.md) | Complete streaming workflow guide |
|
||||||
| [examples.md](./examples.md) | Working code examples in cURL, JS, Python |
|
| [examples.md](./examples.md) | Working code examples in cURL, JS, Python |
|
||||||
| [architecture.md](./architecture.md) | Project structure and design patterns |
|
| [architecture.md](./architecture.md) | Project structure and design patterns |
|
||||||
|
|||||||
+47
-6
@@ -15,6 +15,8 @@ This document covers the complete streaming workflow for AniKotoAPI — from sea
|
|||||||
## ⚠️ Streaming Status
|
## ⚠️ Streaming Status
|
||||||
|
|
||||||
> **Streaming endpoints (`/api/servers`, `/api/stream`, `/api/stream/resolve`) return errors when anikototv.to blocks AJAX requests from server-side IPs.**
|
> **Streaming endpoints (`/api/servers`, `/api/stream`, `/api/stream/resolve`) return errors when anikototv.to blocks AJAX requests from server-side IPs.**
|
||||||
|
>
|
||||||
|
> **✅ Alternative: `/api/stream/embeds` never touches AniKoto's AJAX layer** — see [Direct Embed Providers](#direct-embed-providers).
|
||||||
|
|
||||||
| What Works | What's Blocked |
|
| What Works | What's Blocked |
|
||||||
|:---|:---|
|
|:---|:---|
|
||||||
@@ -24,6 +26,7 @@ This document covers the complete streaming workflow for AniKotoAPI — from sea
|
|||||||
| Top 10, most popular, new release | `/api/mapper-servers` — gogoanime servers |
|
| Top 10, most popular, new release | `/api/mapper-servers` — gogoanime servers |
|
||||||
| AZ list, random, genre list | All AJAX-dependent endpoints |
|
| AZ list, random, genre list | All AJAX-dependent endpoints |
|
||||||
| All HTML-scraped endpoints | |
|
| All HTML-scraped endpoints | |
|
||||||
|
| **`/api/stream/embeds` — direct provider iframes** | |
|
||||||
|
|
||||||
**To fix streaming**, set one of these environment variables:
|
**To fix streaming**, set one of these environment variables:
|
||||||
|
|
||||||
@@ -35,17 +38,55 @@ SCRAPER_API_KEY=your_scraperapi_key
|
|||||||
FLARESOLVERR_URL=http://your-flaresolverr-host:8191
|
FLARESOLVERR_URL=http://your-flaresolverr-host:8191
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or use the Cloudflare-free embed providers below — no proxy needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Direct Embed Providers
|
||||||
|
|
||||||
|
`GET /api/stream/embeds` builds iframe-ready player URLs for three third-party providers. Playback happens inside the provider's own iframe, so nothing ever requests AniKoto's Cloudflare-protected AJAX endpoints at watch time.
|
||||||
|
|
||||||
|
| Provider | URL pattern | Requires |
|
||||||
|
|:---|:---|:---|
|
||||||
|
| **MegaPlay** | `megaplay.buzz/stream/mal\|ani/{id}/{ep}/{lang}` | MAL or AniList ID |
|
||||||
|
| **AniXo** | `anixo.buzz/embed/ani/{anilistId}/{ep}/{lang}` | AniList ID |
|
||||||
|
| **Megavid** | `megavid.buzz/{mal\|ani}/{id}/{ep}/{lang}` | MAL or AniList ID |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From an AniKoto slug (MAL/AniList IDs resolved automatically, cached 60min)
|
||||||
|
curl ".../api/stream/embeds?slug=one-piece-odmau&ep=1"
|
||||||
|
|
||||||
|
# From direct IDs — zero scraping, instant response
|
||||||
|
curl ".../api/stream/embeds?mal=21&ep=1&lang=dub&provider=megavid,megaplay"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": {
|
||||||
|
"ids": { "animeId": 1642, "malId": "21", "anilistId": "21" },
|
||||||
|
"providers": [
|
||||||
|
{ "id": "megaplay", "url": "https://megaplay.buzz/stream/mal/21/1/sub", "iframeReady": true },
|
||||||
|
{ "id": "anixo", "url": "https://anixo.buzz/embed/ani/21/1/sub?color=%23e63946", "iframeReady": true },
|
||||||
|
{ "id": "megavid", "url": "https://megavid.buzz/mal/21/1/sub?color=%23e63946&autoplay=true", "iframeReady": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Drop any `url` straight into an `<iframe allow="autoplay; fullscreen">` — all three are embed-only players (Megavid rejects bare curl with 403 by design; browsers load it fine).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Architecture Overview](#architecture-overview)
|
1. [Architecture Overview](#architecture-overview)
|
||||||
2. [Streaming Workflow](#streaming-workflow)
|
2. [Direct Embed Providers](#direct-embed-providers)
|
||||||
3. [Endpoint Reference](#endpoint-reference)
|
3. [Streaming Workflow](#streaming-workflow)
|
||||||
4. [M3U8 Proxy](#m3u8-proxy)
|
4. [Endpoint Reference](#endpoint-reference)
|
||||||
5. [Video Player Integration](#video-player-integration)
|
5. [M3U8 Proxy](#m3u8-proxy)
|
||||||
6. [Error Handling](#error-handling)
|
6. [Video Player Integration](#video-player-integration)
|
||||||
7. [Performance Tips](#performance-tips)
|
7. [Error Handling](#error-handling)
|
||||||
|
8. [Performance Tips](#performance-tips)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"version": "2.4.1",
|
"version": "2.5.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"version": "2.4.1",
|
"version": "2.5.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.0",
|
"axios": "^1.8.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"version": "2.4.1",
|
"version": "2.5.1",
|
||||||
"description": "A restful API to provide all information about anime from anikototv.to",
|
"description": "A restful API to provide all information about anime from anikototv.to",
|
||||||
"main": "./server.js",
|
"main": "./server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+11
-11
@@ -4,28 +4,28 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="google-site-verification" content="YOUR_VERIFICATION_CODE" />
|
<meta name="google-site-verification" content="YOUR_VERIFICATION_CODE" />
|
||||||
<title>AniKotoAPI — Free Anime REST API | 38 Endpoints</title>
|
<title>AniKotoAPI — Free Anime REST API | 40 Endpoints</title>
|
||||||
<meta property="og:title" content="AniKotoAPI — Free Anime REST API | 38 Endpoints" />
|
<meta property="og:title" content="AniKotoAPI — Free Anime REST API | 40 Endpoints" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://anikototvapi.vercel.app" />
|
<meta property="og:url" content="https://anikototvapi.vercel.app" />
|
||||||
<meta property="og:image" content="https://anikototvapi.vercel.app/share.png" />
|
<meta property="og:image" content="https://anikototvapi.vercel.app/share.png" />
|
||||||
<meta property="og:description" content="Free anime REST API with 38 endpoints. Search, filter, stream episodes, get rankings, and more. No API key required. Built with Node.js, Express, and Cheerio." />
|
<meta property="og:description" content="Free anime REST API with 40 endpoints. Search, filter, stream episodes, get rankings, and more. No API key required. Built with Node.js, Express, and Cheerio." />
|
||||||
<meta property="og:site_name" content="AniKotoAPI" />
|
<meta property="og:site_name" content="AniKotoAPI" />
|
||||||
<meta property="og:locale" content="en_US" />
|
<meta property="og:locale" content="en_US" />
|
||||||
<meta property="og:image:secure_url" content="https://anikototvapi.vercel.app/share.png" />
|
<meta property="og:image:secure_url" content="https://anikototvapi.vercel.app/share.png" />
|
||||||
<meta property="og:image:type" content="image/jpeg" />
|
<meta property="og:image:type" content="image/jpeg" />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="601" />
|
<meta property="og:image:height" content="601" />
|
||||||
<meta property="og:image:alt" content="AniKotoAPI - Free Anime REST API with 38 endpoints" />
|
<meta property="og:image:alt" content="AniKotoAPI - Free Anime REST API with 40 endpoints" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content="@Shineii86" />
|
<meta name="twitter:site" content="@Shineii86" />
|
||||||
<meta name="twitter:creator" content="@Shineii86" />
|
<meta name="twitter:creator" content="@Shineii86" />
|
||||||
<meta name="twitter:title" content="AniKotoAPI — Free Anime REST API | 38 Endpoints" />
|
<meta name="twitter:title" content="AniKotoAPI — Free Anime REST API | 40 Endpoints" />
|
||||||
<meta name="twitter:description" content="Free anime REST API with 38 endpoints. Search, filter, stream episodes, get rankings, and more. No API key required." />
|
<meta name="twitter:description" content="Free anime REST API with 40 endpoints. Search, filter, stream episodes, get rankings, and more. No API key required." />
|
||||||
<meta name="twitter:image" content="https://anikototvapi.vercel.app/share.png" />
|
<meta name="twitter:image" content="https://anikototvapi.vercel.app/share.png" />
|
||||||
<meta name="twitter:image:alt" content="AniKotoAPI - Free Anime REST API with 38 endpoints" />
|
<meta name="twitter:image:alt" content="AniKotoAPI - Free Anime REST API with 40 endpoints" />
|
||||||
<meta name="twitter:domain" content="anikototvapi.vercel.app" />
|
<meta name="twitter:domain" content="anikototvapi.vercel.app" />
|
||||||
<meta name="description" content="Free anime REST API with 38 endpoints. Search, filter, stream episodes with M3U8 proxy, get top rankings, schedules, and more. No API key required. Open source Node.js + Express." />
|
<meta name="description" content="Free anime REST API with 40 endpoints. Search, filter, stream episodes with M3U8 proxy, get top rankings, schedules, and more. No API key required. Open source Node.js + Express." />
|
||||||
<meta name="author" content="Shinei Nouzen" />
|
<meta name="author" content="Shinei Nouzen" />
|
||||||
<meta name="keywords" content="anime api, free anime api, rest api, anime data, anime streaming api, hls proxy, m3u8 proxy, anime episodes, anime search, anime database, nodejs api, express api, web scraping api, anikoto, anime list api, no api key" />
|
<meta name="keywords" content="anime api, free anime api, rest api, anime data, anime streaming api, hls proxy, m3u8 proxy, anime episodes, anime search, anime database, nodejs api, express api, web scraping api, anikoto, anime list api, no api key" />
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebAPI",
|
"@type": "WebAPI",
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"description": "Free anime REST API with 38 endpoints. Search, filter, stream episodes with M3U8 proxy, get rankings, schedules, and more. No API key required.",
|
"description": "Free anime REST API with 40 endpoints. Search, filter, stream episodes with M3U8 proxy, get rankings, schedules, and more. No API key required.",
|
||||||
"url": "https://anikototvapi.vercel.app",
|
"url": "https://anikototvapi.vercel.app",
|
||||||
"image": "https://anikototvapi.vercel.app/share.png",
|
"image": "https://anikototvapi.vercel.app/share.png",
|
||||||
"documentation": "https://github.com/Shineii86/AniKotoAPI",
|
"documentation": "https://github.com/Shineii86/AniKotoAPI",
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="hero-badge"><span class="dot"></span> Free · No API key · Open source</div>
|
<div class="hero-badge"><span class="dot"></span> Free · No API key · Open source</div>
|
||||||
<h1>The anime data API for your next project</h1>
|
<h1>The anime data API for your next project</h1>
|
||||||
<p class="hero-sub">Browse, search, and stream anime data from AniKoto. Real-time scraping with 38 endpoints, in-memory caching, and zero configuration.</p>
|
<p class="hero-sub">Browse, search, and stream anime data from AniKoto. Real-time scraping with 40 endpoints, in-memory caching, and zero configuration.</p>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<a href="#playground" class="btn btn-primary">
|
<a href="#playground" class="btn btn-primary">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||||
@@ -311,7 +311,7 @@ fetch("https://anikototvapi.vercel.app/api")
|
|||||||
<section class="stats">
|
<section class="stats">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="reveal"><div class="stat-value" data-count="38">0</div><div class="stat-label">API Endpoints</div></div>
|
<div class="reveal"><div class="stat-value" data-count="40">0</div><div class="stat-label">API Endpoints</div></div>
|
||||||
<div class="reveal reveal-delay-1"><div class="stat-value" data-count="0">0</div><div class="stat-label">API Keys Required</div></div>
|
<div class="reveal reveal-delay-1"><div class="stat-value" data-count="0">0</div><div class="stat-label">API Keys Required</div></div>
|
||||||
<div class="reveal reveal-delay-2"><div class="stat-value" data-count="40">0</div><div class="stat-label">Anime Genres</div></div>
|
<div class="reveal reveal-delay-2"><div class="stat-value" data-count="40">0</div><div class="stat-label">Anime Genres</div></div>
|
||||||
<div class="reveal reveal-delay-3"><div class="stat-value">Live</div><div class="stat-label">Real-time Data</div></div>
|
<div class="reveal reveal-delay-3"><div class="stat-value">Live</div><div class="stat-label">Real-time Data</div></div>
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@ services:
|
|||||||
startCommand: npm start
|
startCommand: npm start
|
||||||
envVars:
|
envVars:
|
||||||
- key: ALLOWED_ORIGINS
|
- key: ALLOWED_ORIGINS
|
||||||
value: https://anikototv.to,https://anikoto.cz,https://anikoto.me,https://anikoto.net,https://anikoto.se
|
value: https://anikototv.to,https://anikoto.cz,https://anikoto.me,https://anikoto.net,https://anikototv.se
|
||||||
|
|||||||
@@ -42,11 +42,12 @@ const BASE_URL = "https://anikototv.to";
|
|||||||
* // Access alternative domains
|
* // Access alternative domains
|
||||||
* ALT_DOMAINS[0] // "https://anikoto.cz"
|
* ALT_DOMAINS[0] // "https://anikoto.cz"
|
||||||
*/
|
*/
|
||||||
|
// Verified against the official domain hub: https://anikoto.site/#domains
|
||||||
const ALT_DOMAINS = [
|
const ALT_DOMAINS = [
|
||||||
"https://anikoto.cz",
|
"https://anikoto.cz",
|
||||||
"https://anikoto.me",
|
"https://anikoto.me",
|
||||||
"https://anikoto.net",
|
"https://anikoto.net",
|
||||||
"https://anikoto.se"
|
"https://anikototv.se"
|
||||||
];
|
];
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKotoAPI — embedProviders.config.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Configuration for direct embed providers (Megavid, AniXo, MegaPlay).
|
||||||
|
* These providers expose iframe-ready player URLs keyed by AniList/MAL
|
||||||
|
* IDs — no scraping and no Cloudflare-challenged requests required.
|
||||||
|
* URL patterns verified against aluukill/AniCult's provider registry.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* EMBED_PROVIDERS, EMBED_PROVIDER_IDS, DEFAULT_EMBED_COLOR
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// PROVIDER DEFINITIONS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Default accent color applied to provider players ----
|
||||||
|
const DEFAULT_EMBED_COLOR = "%23e63946";
|
||||||
|
|
||||||
|
// ---- FEATURE: Direct embed provider registry ----
|
||||||
|
/**
|
||||||
|
* Each provider builds an iframe-ready URL from external IDs.
|
||||||
|
* Builders return null when required IDs are missing.
|
||||||
|
*
|
||||||
|
* @type {Object<string, {id: string, name: string, origin: string, requires: string[], build: Function}>}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* EMBED_PROVIDERS.megavid.build({ malId: 21, episode: 1, lang: "sub" });
|
||||||
|
* // "https://megavid.buzz/mal/21/1/sub?color=%23e63946&autoplay=true"
|
||||||
|
*/
|
||||||
|
const EMBED_PROVIDERS = {
|
||||||
|
megaplay: {
|
||||||
|
id: "megaplay",
|
||||||
|
name: "MegaPlay",
|
||||||
|
origin: "https://megaplay.buzz",
|
||||||
|
requires: ["malId | anilistId"],
|
||||||
|
build({ malId, anilistId, episode, lang }) {
|
||||||
|
const safeLang = lang === "dub" ? "dub" : "sub";
|
||||||
|
if (malId) return `https://megaplay.buzz/stream/mal/${malId}/${episode}/${safeLang}`;
|
||||||
|
if (anilistId) return `https://megaplay.buzz/stream/ani/${anilistId}/${episode}/${safeLang}`;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
anixo: {
|
||||||
|
id: "anixo",
|
||||||
|
name: "AniXo",
|
||||||
|
origin: "https://anixo.buzz",
|
||||||
|
requires: ["anilistId"],
|
||||||
|
build({ anilistId, episode, lang }) {
|
||||||
|
const safeLang = lang === "dub" ? "dub" : "sub";
|
||||||
|
if (!anilistId) return null;
|
||||||
|
return `https://anixo.buzz/embed/ani/${anilistId}/${episode}/${safeLang}?color=${DEFAULT_EMBED_COLOR}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
megavid: {
|
||||||
|
id: "megavid",
|
||||||
|
name: "Megavid",
|
||||||
|
origin: "https://megavid.buzz",
|
||||||
|
requires: ["malId | anilistId"],
|
||||||
|
build({ malId, anilistId, episode, lang }) {
|
||||||
|
const safeLang = lang === "dub" ? "dub" : "sub";
|
||||||
|
const idType = malId ? "mal" : "ani";
|
||||||
|
const id = malId || anilistId;
|
||||||
|
if (!id) return null;
|
||||||
|
return `https://megavid.buzz/${idType}/${id}/${episode}/${safeLang}?color=${DEFAULT_EMBED_COLOR}&autoplay=true`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- FEATURE: Provider ID list for validation ----
|
||||||
|
const EMBED_PROVIDER_IDS = Object.keys(EMBED_PROVIDERS);
|
||||||
|
|
||||||
|
export { EMBED_PROVIDERS, EMBED_PROVIDER_IDS, DEFAULT_EMBED_COLOR };
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: embedProviders.config.js
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { extractAnimeInfo } from "../extractors/animeInfo.extractor.js";
|
import { extractAnimeInfo } from "../extractors/animeInfo.extractor.js";
|
||||||
|
import { resolveExternalIds } from "../extractors/embedProviders.extractor.js";
|
||||||
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -53,6 +54,21 @@ const getAnimeInfo = async (req, res, next) => {
|
|||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
}
|
}
|
||||||
const data = await extractAnimeInfo(animeSlug);
|
const data = await extractAnimeInfo(animeSlug);
|
||||||
|
|
||||||
|
// NOTE: Enrich with MAL/AniList IDs (cached 60min inside resolveExternalIds)
|
||||||
|
// so clients can pass them straight to /api/stream/embeds. Best-effort —
|
||||||
|
// a failed ID lookup never breaks the info response itself
|
||||||
|
try {
|
||||||
|
const ext = await resolveExternalIds(animeSlug);
|
||||||
|
data.externalIds = {
|
||||||
|
animeId: ext.animeId || 0,
|
||||||
|
malId: ext.malId || null,
|
||||||
|
anilistId: ext.anilistId || null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
data.externalIds = { animeId: 0, malId: null, anilistId: null };
|
||||||
|
}
|
||||||
|
|
||||||
setCache(cacheKey, data, TTL.info);
|
setCache(cacheKey, data, TTL.info);
|
||||||
res.json({ success: true, results: data });
|
res.json({ success: true, results: data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKotoAPI — embedProviders.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for direct embed provider URLs (Megavid, AniXo, MegaPlay).
|
||||||
|
* Returns iframe-ready player URLs that bypass Cloudflare entirely —
|
||||||
|
* no scraping of the source site's AJAX layer required when the client
|
||||||
|
* supplies mal/anilist IDs directly.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getEmbedProviders - Express route handler for GET /api/stream/embeds
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { buildEpisodeEmbeds } from "../extractors/embedProviders.extractor.js";
|
||||||
|
import { EMBED_PROVIDER_IDS } from "../configs/embedProviders.config.js";
|
||||||
|
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: EMBED PROVIDERS
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Direct embed provider URLs for an episode ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/stream/embeds requests. Builds iframe-ready embed
|
||||||
|
* URLs for Megavid/AniXo/MegaPlay from a slug or direct external IDs.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} [req.query.slug] - Anime slug (IDs resolved automatically)
|
||||||
|
* @param {string} [req.query.ep] - Episode number (default 1)
|
||||||
|
* @param {string} [req.query.lang] - "sub" or "dub" (default "sub")
|
||||||
|
* @param {string} [req.query.provider] - Comma-separated provider filter
|
||||||
|
* @param {string} [req.query.mal] - Direct MAL ID (skips slug lookup)
|
||||||
|
* @param {string} [req.query.anilist] - Direct AniList ID
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with iframe-ready embed URLs
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/stream/embeds?slug=one-piece-odmau&ep=1150&lang=sub
|
||||||
|
* GET /api/stream/embeds?mal=21&ep=1&provider=megavid
|
||||||
|
* Response: { success: true, results: { ids, providers: [{ id, name, url }] } }
|
||||||
|
*
|
||||||
|
* @throws {400} If no identifying parameter is provided or provider is unknown
|
||||||
|
*/
|
||||||
|
const getEmbedProviders = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { slug, ep, episode, lang, provider, mal, anilist } = req.query;
|
||||||
|
|
||||||
|
// NOTE: Accept both ?ep= and ?episode= for convenience; clamp lang
|
||||||
|
// to the sub/dub values the embed providers understand
|
||||||
|
const resolvedEp = parseInt(ep || episode || "1", 10) || 1;
|
||||||
|
const resolvedLang = String(lang || "sub").toLowerCase() === "dub" ? "dub" : "sub";
|
||||||
|
const requested = provider
|
||||||
|
? String(provider).split(",").map((p) => p.trim().toLowerCase()).filter(Boolean)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// NOTE: Validate providers before touching cache or upstream — a typo
|
||||||
|
// should fail fast with the list of valid IDs, not build wrong URLs
|
||||||
|
if (requested) {
|
||||||
|
const unknown = requested.filter((p) => !EMBED_PROVIDER_IDS.includes(p));
|
||||||
|
if (unknown.length) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Unknown provider(s): ${unknown.join(", ")}. Valid: ${EMBED_PROVIDER_IDS.join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slug && !mal && !anilist) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Provide 'slug', 'mal' or 'anilist' (plus optional 'ep' and 'lang')"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Cache key composes every response-affecting input — episode,
|
||||||
|
// lang and provider filter all change the output, so each combination
|
||||||
|
// is cached separately (TTL.ids = 60min, IDs and URLs are stable)
|
||||||
|
const cacheKey = `embeds_${slug || ""}_${mal || ""}_${anilist || ""}_${resolvedEp}_${resolvedLang}_${(requested || []).join("-")}`;
|
||||||
|
const cached = getCache(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return res.json({ success: true, results: cached });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await buildEpisodeEmbeds({
|
||||||
|
slug: slug || null,
|
||||||
|
episode: resolvedEp,
|
||||||
|
lang: resolvedLang,
|
||||||
|
providers: requested,
|
||||||
|
malId: mal || null,
|
||||||
|
anilistId: anilist || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setCache(cacheKey, result, TTL.ids);
|
||||||
|
res.json({ success: true, results: result });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getEmbedProviders };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: embedProviders.controller.js
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKotoAPI — episodeLatest.controller.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Controller for the latest-episode checker. Returns just the newest
|
||||||
|
* episode for an anime — built for bots and notification jobs that
|
||||||
|
* only need "is there a new episode?" without the full episode list.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* getLatestEpisode - Express route handler for GET /api/episodes/latest
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extractEpisodeList } from "../extractors/episodeList.extractor.js";
|
||||||
|
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// CONTROLLER: LATEST EPISODE
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Latest episode checker endpoint ----
|
||||||
|
/**
|
||||||
|
* Handles GET /api/episodes/latest requests. Extracts the episode list
|
||||||
|
* (cached) and returns only the newest episode plus summary counts.
|
||||||
|
* Requires 'slug' query param.
|
||||||
|
*
|
||||||
|
* NOTE: Registered before /api/episodes/:id so the literal path wins
|
||||||
|
* the route match — Express evaluates routes in registration order.
|
||||||
|
*
|
||||||
|
* @param {object} req - Express request object
|
||||||
|
* @param {string} [req.query.slug] - Anime slug (or numeric anime ID)
|
||||||
|
* @param {object} res - Express response object
|
||||||
|
* @param {Function} next - Express next middleware function
|
||||||
|
* @returns {void} Sends JSON response with the latest episode
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* GET /api/episodes/latest?slug=one-piece-odmau
|
||||||
|
* Response: {
|
||||||
|
* success: true,
|
||||||
|
* results: {
|
||||||
|
* slug, animeId, totalEpisodes,
|
||||||
|
* latest: { episode_no, title, id, href, server_ids, mal_id }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @throws {400} If 'slug' query parameter is missing
|
||||||
|
* @throws {404} If no episodes are found for the anime
|
||||||
|
*/
|
||||||
|
const getLatestEpisode = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { slug } = req.query;
|
||||||
|
if (!slug) {
|
||||||
|
return res.status(400).json({ success: false, message: "Anime slug is required (?slug=)" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `ep_latest_${slug}`;
|
||||||
|
const cached = getCache(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return res.json({ success: true, results: cached });
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Reuses the episode-list extractor — same upstream call, and
|
||||||
|
// its 10min TTL cache means repeated bot polls cost one AJAX hit
|
||||||
|
const data = await extractEpisodeList(slug);
|
||||||
|
const episodes = data?.episodes || [];
|
||||||
|
|
||||||
|
if (!episodes.length) {
|
||||||
|
return res.status(404).json({ success: false, message: "No episodes found for this anime" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Episode numbers arrive as numbers — plain max reduce beats
|
||||||
|
// assuming list order (some anime are listed non-sequentially)
|
||||||
|
const latest = episodes.reduce((newest, ep) =>
|
||||||
|
(ep.episode_no > newest.episode_no ? ep : newest), episodes[0]);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
slug: data.slug || slug,
|
||||||
|
animeId: data.animeId || 0,
|
||||||
|
totalEpisodes: data.totalEpisodes || episodes.length,
|
||||||
|
latest: {
|
||||||
|
episode_no: latest.episode_no,
|
||||||
|
title: latest.title || "",
|
||||||
|
id: latest.id || null,
|
||||||
|
slug: latest.slug || "",
|
||||||
|
href: latest.href || "",
|
||||||
|
server_ids: latest.server_ids || "",
|
||||||
|
mal_id: latest.mal_id || null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setCache(cacheKey, result, TTL.episodes);
|
||||||
|
res.json({ success: true, results: result });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getLatestEpisode };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: episodeLatest.controller.js
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKotoAPI — embedProviders.extractor.js
|
||||||
|
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Resolves external IDs (MAL/AniList) for an anime and builds
|
||||||
|
* iframe-ready direct embed URLs for Megavid, AniXo and MegaPlay.
|
||||||
|
* MAL ID comes from the site's episode list (data-mal); AniList ID
|
||||||
|
* is looked up via the public AniList GraphQL API (cached).
|
||||||
|
* No provider scraping — pure URL construction.
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* resolveExternalIds, resolveAnilistIdFromMal, buildEpisodeEmbeds
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
import { EMBED_PROVIDERS } from "../configs/embedProviders.config.js";
|
||||||
|
import { extractEpisodeList } from "./episodeList.extractor.js";
|
||||||
|
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANILIST LOOKUP
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const ANILIST_GRAPHQL = "https://graphql.anilist.co";
|
||||||
|
const ANILIST_QUERY = `query ($idMal: Int!) { Media(idMal: $idMal, type: ANIME) { id idMal } }`;
|
||||||
|
|
||||||
|
// ---- FEATURE: MAL ID → AniList ID lookup (cached) ----
|
||||||
|
/**
|
||||||
|
* Resolves an AniList media ID from a MAL ID via the public GraphQL API.
|
||||||
|
* Results are cached for TTL.ids (60min) — IDs never change per anime.
|
||||||
|
*
|
||||||
|
* @param {string|number} malId - MyAnimeList ID
|
||||||
|
* @returns {Promise<string|null>} AniList ID or null on failure
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* await resolveAnilistIdFromMal(21); // "30019" (One Piece)
|
||||||
|
*/
|
||||||
|
const resolveAnilistIdFromMal = async (malId) => {
|
||||||
|
// NOTE: Cache-first — AniList IDs never change per MAL ID, so a hit here
|
||||||
|
// avoids a GraphQL round-trip on every embed request (TTL.ids = 60min)
|
||||||
|
const cacheKey = `anilist_from_mal_${malId}`;
|
||||||
|
const cached = getCache(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
ANILIST_GRAPHQL,
|
||||||
|
{ query: ANILIST_QUERY, variables: { idMal: parseInt(malId, 10) } },
|
||||||
|
{
|
||||||
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
|
timeout: 10000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// NOTE: Response nests under data.data.Media — axios already unwraps
|
||||||
|
// the HTTP body once, so one more .data access reaches the GraphQL envelope
|
||||||
|
const anilistId = data?.data?.Media?.id || null;
|
||||||
|
// NOTE: Only cache successes — transient AniList failures must not
|
||||||
|
// poison the cache for an hour
|
||||||
|
if (anilistId) setCache(cacheKey, anilistId, TTL.ids);
|
||||||
|
return anilistId;
|
||||||
|
} catch {
|
||||||
|
// TIP: Never throw here — a missing AniList ID only disables AniXo,
|
||||||
|
// MegaPlay/Megavid can still build URLs from the MAL ID alone
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// EXTERNAL ID RESOLUTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Resolve MAL + AniList IDs for an anime slug ----
|
||||||
|
/**
|
||||||
|
* Uses the site's episode list (data-mal attribute) to get the MAL ID,
|
||||||
|
* then looks up the AniList ID. Cached per slug.
|
||||||
|
*
|
||||||
|
* @param {string} slugOrId - Anime slug or numeric internal ID
|
||||||
|
* @returns {Promise<{animeId: number, malId: string|null, anilistId: string|null, slug: string}>}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const ids = await resolveExternalIds("one-piece-odmau");
|
||||||
|
* // { animeId: 1642, malId: "21", anilistId: "21", slug: "one-piece-odmau" }
|
||||||
|
*/
|
||||||
|
const resolveExternalIds = async (slugOrId) => {
|
||||||
|
const cacheKey = `ext_ids_${slugOrId}`;
|
||||||
|
const cached = getCache(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// NOTE: One watch-page + one AJAX round-trip yields both the internal
|
||||||
|
// animeId and the MAL ID (every episode anchor carries data-mal)
|
||||||
|
const episodeData = await extractEpisodeList(slugOrId);
|
||||||
|
const animeId = episodeData?.animeId || 0;
|
||||||
|
|
||||||
|
// NOTE: Dedup — every episode shares the same series-level MAL ID;
|
||||||
|
// take the first non-empty value and stop (no need to scan 1000+ eps)
|
||||||
|
let malId = null;
|
||||||
|
const episodes = episodeData?.episodes || [];
|
||||||
|
for (const ep of episodes) {
|
||||||
|
if (ep?.mal_id) { malId = String(ep.mal_id); break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
let anilistId = null;
|
||||||
|
if (malId) anilistId = await resolveAnilistIdFromMal(malId);
|
||||||
|
|
||||||
|
const result = { animeId, malId, anilistId, slug: slugOrId };
|
||||||
|
// NOTE: Only cache resolved slugs — failed lookups (dead slug, upstream
|
||||||
|
// block) should be retried on the next request instead of sticking for 60min
|
||||||
|
if (animeId || malId) setCache(cacheKey, result, TTL.ids);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// EMBED URL BUILDER
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Build direct embed URLs for an episode ----
|
||||||
|
/**
|
||||||
|
* Builds iframe-ready embed URLs for the requested providers.
|
||||||
|
* Accepts a slug (IDs resolved automatically) or direct mal/anilist IDs.
|
||||||
|
*
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} [options.slug] - Anime slug for ID resolution
|
||||||
|
* @param {number|string} [options.episode=1] - Episode number
|
||||||
|
* @param {string} [options.lang="sub"] - "sub" or "dub"
|
||||||
|
* @param {string[]|null} [options.providers=null] - Provider filter (null = all)
|
||||||
|
* @param {string|number|null} [options.malId=null] - Direct MAL ID (skips slug lookup)
|
||||||
|
* @param {string|number|null} [options.anilistId=null] - Direct AniList ID
|
||||||
|
* @returns {Promise<Object>} { slug, episode, lang, ids, providers[], unavailable[] }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const embeds = await buildEpisodeEmbeds({ slug: "one-piece-odmau", episode: 1150 });
|
||||||
|
* embeds.providers.forEach(p => console.log(p.id, p.url));
|
||||||
|
*/
|
||||||
|
const buildEpisodeEmbeds = async ({ slug, episode, lang = "sub", providers = null, malId = null, anilistId = null }) => {
|
||||||
|
// NOTE: Sanitize inputs — episode must be a positive integer, lang is
|
||||||
|
// clamped to the two values the providers actually accept
|
||||||
|
const safeEpisode = Math.max(1, parseInt(episode, 10) || 1);
|
||||||
|
const safeLang = lang === "dub" ? "dub" : "sub";
|
||||||
|
|
||||||
|
let resolvedMal = malId ? String(malId) : null;
|
||||||
|
let resolvedAnilist = anilistId ? String(anilistId) : null;
|
||||||
|
let animeId = 0;
|
||||||
|
let resolvedSlug = slug || null;
|
||||||
|
|
||||||
|
// TIP: Direct-ID fast path — clients that already hold mal/anilist IDs
|
||||||
|
// (e.g. AniList-powered frontends) skip the slug scrape entirely; the
|
||||||
|
// slug lookup only runs when at least one ID is still missing
|
||||||
|
if ((!resolvedMal || !resolvedAnilist) && slug) {
|
||||||
|
const ids = await resolveExternalIds(slug);
|
||||||
|
animeId = ids.animeId || 0;
|
||||||
|
resolvedSlug = ids.slug || slug;
|
||||||
|
if (!resolvedMal) resolvedMal = ids.malId || null;
|
||||||
|
if (!resolvedAnilist) resolvedAnilist = ids.anilistId ? String(ids.anilistId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedMal && !resolvedAnilist) {
|
||||||
|
return {
|
||||||
|
slug: resolvedSlug,
|
||||||
|
episode: safeEpisode,
|
||||||
|
lang: safeLang,
|
||||||
|
ids: { animeId, malId: null, anilistId: null },
|
||||||
|
providers: [],
|
||||||
|
unavailable: Object.values(EMBED_PROVIDERS).map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
reason: "No MAL or AniList ID available for this anime",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const wanted = providers && providers.length
|
||||||
|
? providers.map((p) => String(p).toLowerCase())
|
||||||
|
: Object.keys(EMBED_PROVIDERS);
|
||||||
|
|
||||||
|
const built = [];
|
||||||
|
const unavailable = [];
|
||||||
|
|
||||||
|
// NOTE: State-machine-lite loop — each provider either builds a URL
|
||||||
|
// ("built") or records why it can't ("unavailable"); the client always
|
||||||
|
// gets a complete picture instead of silently missing providers
|
||||||
|
for (const providerId of wanted) {
|
||||||
|
const provider = EMBED_PROVIDERS[providerId];
|
||||||
|
if (!provider) {
|
||||||
|
unavailable.push({ id: providerId, name: providerId, reason: "Unknown provider" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url = provider.build({
|
||||||
|
malId: resolvedMal,
|
||||||
|
anilistId: resolvedAnilist,
|
||||||
|
episode: safeEpisode,
|
||||||
|
lang: safeLang,
|
||||||
|
});
|
||||||
|
if (url) {
|
||||||
|
built.push({
|
||||||
|
id: provider.id,
|
||||||
|
name: provider.name,
|
||||||
|
url,
|
||||||
|
iframeReady: true,
|
||||||
|
origin: provider.origin,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
unavailable.push({
|
||||||
|
id: provider.id,
|
||||||
|
name: provider.name,
|
||||||
|
reason: `Requires ${provider.requires.join(" or ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug: resolvedSlug,
|
||||||
|
episode: safeEpisode,
|
||||||
|
lang: safeLang,
|
||||||
|
ids: { animeId, malId: resolvedMal, anilistId: resolvedAnilist },
|
||||||
|
providers: built,
|
||||||
|
unavailable,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { resolveExternalIds, resolveAnilistIdFromMal, buildEpisodeEmbeds };
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════ END: embedProviders.extractor.js
|
||||||
@@ -39,18 +39,37 @@ const extractHomeInfo = async () => {
|
|||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
|
|
||||||
const spotlights = [];
|
const spotlights = [];
|
||||||
// NOTE: #hotest .swiper-slide.item = spotlight carousel on homepage
|
// NOTE: Dual selector — new layout (2026-10): #hotest .swiper-slide with
|
||||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
// .cover img, h2.ak-title, .ak-desc, .ak-detail spans, a.btn.btn-primary.
|
||||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
// Old layout: .swiper-slide.item with a.play, .image div[style], h2.title,
|
||||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
// .desc, i.rating/i.quality/i.sub/i.dub/i.date. Both remain supported.
|
||||||
const title = $(el).find("h2.title.d-title").text().trim() || "";
|
$("#hotest .swiper-slide").each((i, el) => {
|
||||||
const japaneseTitle = $(el).find("h2.title.d-title").attr("data-jp") || "";
|
const watchHref = $(el).find("a.play, a.btn.btn-primary").attr("href") || "";
|
||||||
const description = $(el).find(".desc").text().trim() || "";
|
const slug = watchHref.split("/watch/").pop() || "";
|
||||||
const rating = $(el).find("i.rating").text().trim() || "";
|
const poster =
|
||||||
|
$(el).find(".cover img").attr("src") ||
|
||||||
|
$(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] ||
|
||||||
|
"";
|
||||||
|
const titleEl = $(el).find("h2.ak-title.d-title, h2.title.d-title").first();
|
||||||
|
const title = titleEl.text().trim() || "";
|
||||||
|
const japaneseTitle = titleEl.attr("data-jp") || "";
|
||||||
|
const description = $(el).find(".ak-desc, .desc").text().trim() || "";
|
||||||
|
// NOTE: New layout packs type/date/rating into .ak-detail span.it list
|
||||||
|
const detailItems = $(el)
|
||||||
|
.find(".ak-detail span.it")
|
||||||
|
.map((_, s) => $(s).text().trim())
|
||||||
|
.get();
|
||||||
|
const rating =
|
||||||
|
$(el).find("i.rating").text().trim() ||
|
||||||
|
detailItems.find((t) => /^\d{1,2}\.\d+$/.test(t)) ||
|
||||||
|
"";
|
||||||
const quality = $(el).find("i.quality").text().trim() || "";
|
const quality = $(el).find("i.quality").text().trim() || "";
|
||||||
const sub = parseInt($(el).find("i.sub").text().trim()) || 0;
|
const sub = parseInt($(el).find("i.sub").text().trim()) || 0;
|
||||||
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
||||||
const date = $(el).find("i.date").text().trim() || "";
|
const date =
|
||||||
|
$(el).find("i.date").text().trim() ||
|
||||||
|
(detailItems.length > 1 ? detailItems[1] : "");
|
||||||
|
const type = detailItems[0] || "";
|
||||||
|
|
||||||
if (slug) {
|
if (slug) {
|
||||||
spotlights.push({
|
spotlights.push({
|
||||||
@@ -63,7 +82,8 @@ const extractHomeInfo = async () => {
|
|||||||
quality,
|
quality,
|
||||||
sub,
|
sub,
|
||||||
dub,
|
dub,
|
||||||
date
|
date,
|
||||||
|
type
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,17 +52,37 @@ const extractSpotlight = async () => {
|
|||||||
|
|
||||||
const spotlights = [];
|
const spotlights = [];
|
||||||
|
|
||||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
// NOTE: Dual selector — new layout (2026-10): #hotest .swiper-slide with
|
||||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
// .cover img, h2.ak-title, .ak-desc, .ak-detail spans, a.btn.btn-primary.
|
||||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
// Old layout: .swiper-slide.item with a.play, .image div[style], h2.title,
|
||||||
const title = $(el).find("h2.title.d-title").text().trim() || "";
|
// .desc, i.rating/i.quality/i.sub/i.dub/i.date. Both remain supported.
|
||||||
const japaneseTitle = $(el).find("h2.title.d-title").attr("data-jp") || "";
|
$("#hotest .swiper-slide").each((i, el) => {
|
||||||
const description = $(el).find(".desc").text().trim() || "";
|
const watchHref = $(el).find("a.play, a.btn.btn-primary").attr("href") || "";
|
||||||
const rating = $(el).find("i.rating").text().trim() || "";
|
const slug = watchHref.split("/watch/").pop() || "";
|
||||||
|
const poster =
|
||||||
|
$(el).find(".cover img").attr("src") ||
|
||||||
|
$(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] ||
|
||||||
|
"";
|
||||||
|
const titleEl = $(el).find("h2.ak-title.d-title, h2.title.d-title").first();
|
||||||
|
const title = titleEl.text().trim() || "";
|
||||||
|
const japaneseTitle = titleEl.attr("data-jp") || "";
|
||||||
|
const description = $(el).find(".ak-desc, .desc").text().trim() || "";
|
||||||
|
// NOTE: New layout packs type/date/rating into .ak-detail span.it list
|
||||||
|
const detailItems = $(el)
|
||||||
|
.find(".ak-detail span.it")
|
||||||
|
.map((_, s) => $(s).text().trim())
|
||||||
|
.get();
|
||||||
|
const rating =
|
||||||
|
$(el).find("i.rating").text().trim() ||
|
||||||
|
detailItems.find((t) => /^\d{1,2}\.\d+$/.test(t)) ||
|
||||||
|
"";
|
||||||
const quality = $(el).find("i.quality").text().trim() || "";
|
const quality = $(el).find("i.quality").text().trim() || "";
|
||||||
const sub = parseInt($(el).find("i.sub").text().trim()) || 0;
|
const sub = parseInt($(el).find("i.sub").text().trim()) || 0;
|
||||||
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
||||||
const date = $(el).find("i.date").text().trim() || "";
|
const date =
|
||||||
|
$(el).find("i.date").text().trim() ||
|
||||||
|
(detailItems.length > 1 ? detailItems[1] : "");
|
||||||
|
const type = detailItems[0] || "";
|
||||||
|
|
||||||
if (slug) {
|
if (slug) {
|
||||||
spotlights.push({
|
spotlights.push({
|
||||||
@@ -75,7 +95,8 @@ const extractSpotlight = async () => {
|
|||||||
quality,
|
quality,
|
||||||
sub,
|
sub,
|
||||||
dub,
|
dub,
|
||||||
date
|
date,
|
||||||
|
type
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const TTL = {
|
|||||||
schedule: 1800000, // 30min — schedule changes daily
|
schedule: 1800000, // 30min — schedule changes daily
|
||||||
genres: 3600000, // 60min — genre list is static
|
genres: 3600000, // 60min — genre list is static
|
||||||
suggestions: 300000, // 5min — suggestions update moderate
|
suggestions: 300000, // 5min — suggestions update moderate
|
||||||
|
ids: 3600000, // 60min — MAL/AniList IDs are stable per anime
|
||||||
default: 300000 // 5min — safe default for unknown endpoints
|
default: 300000 // 5min — safe default for unknown endpoints
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+24
-2
@@ -24,9 +24,11 @@ import { getHomeInfo } from "../controllers/homeInfo.controller.js";
|
|||||||
import { getAnimeInfo } from "../controllers/animeInfo.controller.js";
|
import { getAnimeInfo } from "../controllers/animeInfo.controller.js";
|
||||||
import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
|
import { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
|
||||||
import { getEpisodeList } from "../controllers/episodeList.controller.js";
|
import { getEpisodeList } from "../controllers/episodeList.controller.js";
|
||||||
|
import { getLatestEpisode } from "../controllers/episodeLatest.controller.js";
|
||||||
import { getEpisodeListAjax } from "../controllers/episodeListAjax.controller.js";
|
import { getEpisodeListAjax } from "../controllers/episodeListAjax.controller.js";
|
||||||
import { getStreamInfo, getServerList, getMapperServers } from "../controllers/streamInfo.controller.js";
|
import { getStreamInfo, getServerList, getMapperServers } from "../controllers/streamInfo.controller.js";
|
||||||
import { getResolvedStream, getStreamQualities } from "../controllers/streamResolver.controller.js";
|
import { getResolvedStream, getStreamQualities } from "../controllers/streamResolver.controller.js";
|
||||||
|
import { getEmbedProviders } from "../controllers/embedProviders.controller.js";
|
||||||
import { getSchedule } from "../controllers/schedule.controller.js";
|
import { getSchedule } from "../controllers/schedule.controller.js";
|
||||||
import { getSpotlight } from "../controllers/spotlight.controller.js";
|
import { getSpotlight } from "../controllers/spotlight.controller.js";
|
||||||
import { getTrending } from "../controllers/trending.controller.js";
|
import { getTrending } from "../controllers/trending.controller.js";
|
||||||
@@ -153,6 +155,15 @@ app.use((req, res, next) => {
|
|||||||
// EPISODES
|
// EPISODES
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Latest episode checker (literal path must precede :id) ----
|
||||||
|
app.get("/api/episodes/latest", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await getLatestEpisode(req, res, next);
|
||||||
|
} catch (error) {
|
||||||
|
jsonError(res, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ---- FEATURE: Episode list for anime by ID ----
|
// ---- FEATURE: Episode list for anime by ID ----
|
||||||
app.get("/api/episodes/:id", async (req, res, next) => {
|
app.get("/api/episodes/:id", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -229,6 +240,15 @@ app.use((req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Direct embed providers (Megavid/AniXo/MegaPlay) ----
|
||||||
|
app.get("/api/stream/embeds", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await getEmbedProviders(req, res, next);
|
||||||
|
} catch (error) {
|
||||||
|
jsonError(res, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ---- FEATURE: M3U8 proxy — fetch and serve M3U8 playlists through API ----
|
// ---- FEATURE: M3U8 proxy — fetch and serve M3U8 playlists through API ----
|
||||||
app.get("/api/stream/proxy", async (req, res, next) => {
|
app.get("/api/stream/proxy", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -582,7 +602,7 @@ app.get("/api/stats", (req, res) => {
|
|||||||
ttl: "5 minutes",
|
ttl: "5 minutes",
|
||||||
description: "Map-based cache with TTL expiration",
|
description: "Map-based cache with TTL expiration",
|
||||||
},
|
},
|
||||||
endpoints: 38,
|
endpoints: 40,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -636,7 +656,8 @@ app.get("/api/openapi", (req, res) => {
|
|||||||
paths: {
|
paths: {
|
||||||
"/": { get: { summary: "Homepage data", tags: ["Home"] } },
|
"/": { get: { summary: "Homepage data", tags: ["Home"] } },
|
||||||
"/search": { get: { summary: "Search anime", tags: ["Search"] } },
|
"/search": { get: { summary: "Search anime", tags: ["Search"] } },
|
||||||
"/info": { get: { summary: "Anime info", tags: ["Anime"] } },
|
"/info": { get: { summary: "Anime info (incl. externalIds: malId/anilistId)", tags: ["Anime"] } },
|
||||||
|
"/episodes/latest": { get: { summary: "Latest episode checker", tags: ["Episodes"] } },
|
||||||
"/episodes/{id}": { get: { summary: "Episode list", tags: ["Episodes"] } },
|
"/episodes/{id}": { get: { summary: "Episode list", tags: ["Episodes"] } },
|
||||||
"/servers": { get: { summary: "Server list", tags: ["Streaming"] } },
|
"/servers": { get: { summary: "Server list", tags: ["Streaming"] } },
|
||||||
"/stream": { get: { summary: "Stream URL", tags: ["Streaming"] } },
|
"/stream": { get: { summary: "Stream URL", tags: ["Streaming"] } },
|
||||||
@@ -660,6 +681,7 @@ app.get("/api/openapi", (req, res) => {
|
|||||||
"/watch": { get: { summary: "Watch page data", tags: ["Streaming"] } },
|
"/watch": { get: { summary: "Watch page data", tags: ["Streaming"] } },
|
||||||
"/stream/resolve": { get: { summary: "Resolve actual stream URL (m3u8/mp4)", tags: ["Streaming"] } },
|
"/stream/resolve": { get: { summary: "Resolve actual stream URL (m3u8/mp4)", tags: ["Streaming"] } },
|
||||||
"/stream/qualities": { get: { summary: "Available quality options for M3U8 stream", tags: ["Streaming"] } },
|
"/stream/qualities": { get: { summary: "Available quality options for M3U8 stream", tags: ["Streaming"] } },
|
||||||
|
"/stream/embeds": { get: { summary: "Direct embed provider URLs (Megavid/AniXo/MegaPlay)", tags: ["Streaming"] } },
|
||||||
"/stream/proxy": { get: { summary: "M3U8 playlist proxy (rewrites URLs)", tags: ["Streaming"] } },
|
"/stream/proxy": { get: { summary: "M3U8 playlist proxy (rewrites URLs)", tags: ["Streaming"] } },
|
||||||
"/stream/ts-proxy": { get: { summary: "TS video segment proxy", tags: ["Streaming"] } },
|
"/stream/ts-proxy": { get: { summary: "TS video segment proxy", tags: ["Streaming"] } },
|
||||||
"/search/suggest": { get: { summary: "Search suggestions", tags: ["Search"] } },
|
"/search/suggest": { get: { summary: "Search suggestions", tags: ["Search"] } },
|
||||||
|
|||||||
@@ -37,10 +37,14 @@ const tests = [
|
|||||||
{ name: "Home", url: "/", check: (d) => d.results?.spotlights || d.results?.trending },
|
{ name: "Home", url: "/", check: (d) => d.results?.spotlights || d.results?.trending },
|
||||||
{ name: "Search", url: "/search?keyword=naruto", check: (d) => d.results?.data },
|
{ name: "Search", url: "/search?keyword=naruto", check: (d) => d.results?.data },
|
||||||
{ name: "Info", url: "/info?id=one-piece-odmau", check: (d) => d.results?.title },
|
{ name: "Info", url: "/info?id=one-piece-odmau", check: (d) => d.results?.title },
|
||||||
|
{ name: "Info External IDs", url: "/info?id=one-piece-odmau", check: (d) => d.results?.externalIds?.malId },
|
||||||
{ name: "Episodes", url: "/episodes/one-piece-odmau", check: (d) => d.results?.episodes },
|
{ name: "Episodes", url: "/episodes/one-piece-odmau", check: (d) => d.results?.episodes },
|
||||||
|
{ name: "Latest Episode", url: "/episodes/latest?slug=one-piece-odmau", check: (d) => d.results?.latest?.episode_no && d.results?.latest?.server_ids },
|
||||||
{ name: "Servers", url: "/servers?ids=1", check: (d) => d.results, optional: true },
|
{ name: "Servers", url: "/servers?ids=1", check: (d) => d.results, optional: true },
|
||||||
// Streaming
|
// Streaming
|
||||||
{ name: "Stream", url: "/watch?slug=one-piece-odmau&ep=1", check: (d) => d.results?.servers || d.results?.episodeNumber },
|
{ name: "Stream", url: "/watch?slug=one-piece-odmau&ep=1", check: (d) => d.results?.servers || d.results?.episodeNumber },
|
||||||
|
{ name: "Embed Providers", url: "/stream/embeds?slug=one-piece-odmau&ep=1", check: (d) => d.results?.providers?.length > 0 },
|
||||||
|
{ name: "Embed (direct MAL)", url: "/stream/embeds?mal=21&ep=1&provider=megavid", check: (d) => d.results?.providers?.[0]?.url?.includes("megavid.buzz") },
|
||||||
|
|
||||||
|
|
||||||
// Discovery endpoints
|
// Discovery endpoints
|
||||||
|
|||||||
Reference in New Issue
Block a user