mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
Compare commits
2
Commits
1d71a1e557
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c854d763c6 | ||
|
|
0f2258a8d5 |
@@ -2,3 +2,4 @@ node_modules/
|
||||
.env
|
||||
.DS_Store
|
||||
*.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/),
|
||||
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
|
||||
|
||||
### Security
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
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/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/Version-2.4.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/Version-2.5.1-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
||||
<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/Maintenance-Active-22c55e?style=flat-square&logo=github&logoColor=white" alt="Maintenance"/>
|
||||
</p>
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
### 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
|
||||
- 📺 **Episode Lists** — AJAX-loaded episode data with server info
|
||||
- 🎯 **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.**
|
||||
>
|
||||
> 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 |
|
||||
|:---|:---|
|
||||
@@ -164,7 +166,8 @@ FLARESOLVERR_URL=http://your-flaresolverr-host:8191
|
||||
### ⚡ Core
|
||||
- **Real-time scraping** from anikototv.to
|
||||
- **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
|
||||
- **Mapper API integration** for extra servers
|
||||
- **Graceful error handling** per endpoint
|
||||
@@ -215,7 +218,7 @@ FLARESOLVERR_URL=http://your-flaresolverr-host:8191
|
||||
|
||||
| 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 | ✅ |
|
||||
| 📺 Episode Lists | AJAX-loaded episode data | ✅ |
|
||||
| 🎯 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.me` | ✅ Active | Mirror domain |
|
||||
| **AniKoto** | `anikoto.net` | ✅ Active | Mirror domain |
|
||||
| **AniKoto** | `anikoto.se` | ✅ Active | Mirror domain |
|
||||
| **AniKoto** | `anikototv.se` | ✅ Active | Mirror domain |
|
||||
|
||||
### Data Available
|
||||
|
||||
@@ -333,7 +336,7 @@ AniKotoAPI/
|
||||
│ │
|
||||
│ ├── 📂 docs/ # 📚 API documentation
|
||||
│ │ ├── 📄 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
|
||||
│ │ ├── 📄 examples.md # Code examples (cURL, JS, Python)
|
||||
│ │ ├── 📄 architecture.md # Project structure, tech stack
|
||||
@@ -344,32 +347,36 @@ AniKotoAPI/
|
||||
│ ├── 📂 configs/ # 🔧 Configuration files
|
||||
│ │ ├── 📄 dataUrl.js # 🌐 URL patterns for anikototv.to
|
||||
│ │ ├── 📄 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
|
||||
│ │ ├── 📄 animeInfo.controller.js
|
||||
│ │ ├── 📄 search.controller.js
|
||||
│ │ ├── 📄 episodeList.controller.js
|
||||
│ │ ├── 📄 episodeLatest.controller.js # 🆕 Latest-episode checker (bots)
|
||||
│ │ ├── 📄 episodeListAjax.controller.js
|
||||
│ │ ├── 📄 streamInfo.controller.js
|
||||
│ │ ├── 📄 streamResolver.controller.js # 🆕 Stream URL resolution + quality detection
|
||||
│ │ ├── 📄 embedProviders.controller.js # 🆕 Direct embed provider URLs
|
||||
│ │ ├── 📄 filter.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
|
||||
│ │ ├── 📄 search.extractor.js # 🔍 Search results
|
||||
│ │ ├── 📄 animeInfo.extractor.js # 📺 Anime details
|
||||
│ │ ├── 📄 streamInfo.extractor.js # 📡 Streaming servers
|
||||
│ │ ├── 📄 streamResolver.extractor.js # 🔗 Stream URL resolution, M3U8 parser
|
||||
│ │ ├── 📄 embedProviders.extractor.js # 🆕 MAL/AniList ID resolution + embed URLs
|
||||
│ │ ├── 📄 filter.extractor.js # 🎯 Filtered results
|
||||
│ │ ├── 📄 watchPage.extractor.js # ▶️ Watch page data
|
||||
│ │ ├── 📄 seasons.extractor.js # 📅 Season info from watch page
|
||||
│ │ ├── 📄 watchOrder.extractor.js # 📋 Watch order from sidebar
|
||||
│ │ ├── 📄 episodeList.extractor.js # 📋 Episode lists
|
||||
│ │ └── 📄 ... (16 more)
|
||||
│ │ └── 📄 ... (17 more)
|
||||
│ │
|
||||
│ ├── 📂 helper/ # 🛠️ Utility functions
|
||||
│ │ ├── 📄 cache.helper.js # 💾 In-memory caching
|
||||
@@ -384,7 +391,7 @@ AniKotoAPI/
|
||||
│ │ └── 📄 creatorInfo.js # ✍️ Creator attribution injection
|
||||
│ │
|
||||
│ └── 📂 routes/ # 🛤️ Express routes
|
||||
│ ├── 📄 apiRoutes.js # 🌐 Main API routes (38 endpoints)
|
||||
│ ├── 📄 apiRoutes.js # 🌐 Main API routes (40 endpoints)
|
||||
│ └── 📄 category.route.js # 🏷️ Category routes
|
||||
│
|
||||
├── 📄 server.js # 🚀 Express server entry point
|
||||
@@ -2344,6 +2351,7 @@ CMD ["node", "server.js"]
|
||||
|
||||
| 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.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 |
|
||||
@@ -2457,7 +2465,7 @@ The API can access 10,000+ anime titles from anikototv.to. The most-popular endp
|
||||
|
||||
### ✅ Completed
|
||||
|
||||
- [x] 🎬 38 API endpoints covering all data
|
||||
- [x] 🎬 40 API endpoints covering all data
|
||||
- [x] 🔍 Full-text search with pagination
|
||||
- [x] 📺 Episode lists via AJAX loading
|
||||
- [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 ME](https://anikoto.me) | 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
|
||||
|
||||
|
||||
+23
-17
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* AniKotoAPI — Architecture Document
|
||||
* Version : 2.4.1
|
||||
* Generated: 2026-08-14
|
||||
* Version : 2.5.1
|
||||
* Generated: 2026-10-13
|
||||
* ============================================================================
|
||||
*
|
||||
* 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 │ │ │
|
||||
* │ │ └──────────────────────┬───────────────────────────────────┘ │ │
|
||||
* │ │ │ │ │
|
||||
* │ │ ▼ │ │
|
||||
* │ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||||
* │ │ │ EXTRACTOR / SCRAPER LAYER (28 extractors) │ │ │
|
||||
* │ │ │ EXTRACTOR / SCRAPER LAYER (29 extractors) │ │ │
|
||||
* │ │ │ HTML fetching · Cheerio parsing · Data transformation │ │ │
|
||||
* │ │ └──────────────────────┬───────────────────────────────────┘ │ │
|
||||
* │ │ │ │ │
|
||||
@@ -180,15 +180,18 @@
|
||||
* ├── src/
|
||||
* │ ├── configs/
|
||||
* │ │ ├── header.config.js # Default HTTP headers for upstream requests
|
||||
* │ │ └── ids.config.js # Genre / Type / Status numeric ID mappings
|
||||
* │ ├── controllers/ # 28 route handlers (one per endpoint)
|
||||
* │ │ ├── ids.config.js # Genre / Type / Status numeric ID mappings
|
||||
* │ │ └── embedProviders.config.js # Direct embed provider registry (Megavid/AniXo/MegaPlay)
|
||||
* │ ├── controllers/ # 30 route handlers (one per endpoint)
|
||||
* │ │ ├── homeInfo.controller.js
|
||||
* │ │ ├── animeInfo.controller.js
|
||||
* │ │ ├── search.controller.js
|
||||
* │ │ ├── episodeList.controller.js
|
||||
* │ │ ├── episodeLatest.controller.js
|
||||
* │ │ ├── episodeListAjax.controller.js
|
||||
* │ │ ├── streamInfo.controller.js
|
||||
* │ │ ├── streamResolver.controller.js
|
||||
* │ │ ├── embedProviders.controller.js
|
||||
* │ │ ├── schedule.controller.js
|
||||
* │ │ ├── spotlight.controller.js
|
||||
* │ │ ├── trending.controller.js
|
||||
@@ -210,7 +213,7 @@
|
||||
* │ │ ├── recentlyUpdatedTabs.controller.js
|
||||
* │ │ ├── completedAnime.controller.js
|
||||
* │ │ └── category.controller.js
|
||||
* │ ├── extractors/ # 28 HTML scrapers (one per data source)
|
||||
* │ ├── extractors/ # 29 HTML scrapers (one per data source)
|
||||
* │ │ ├── homeInfo.extractor.js
|
||||
* │ │ ├── animeInfo.extractor.js
|
||||
* │ │ ├── search.extractor.js
|
||||
@@ -218,6 +221,7 @@
|
||||
* │ │ ├── episodeListAjax.extractor.js
|
||||
* │ │ ├── streamInfo.extractor.js
|
||||
* │ │ ├── streamResolver.extractor.js
|
||||
* │ │ ├── embedProviders.extractor.js
|
||||
* │ │ ├── schedule.extractor.js
|
||||
* │ │ ├── spotlight.extractor.js
|
||||
* │ │ ├── trending.extractor.js
|
||||
@@ -297,7 +301,7 @@
|
||||
* └────────────────────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* ┌────────────────────────────────────────────────────────────────────────┐
|
||||
* │ Controllers (28 handlers) │
|
||||
* │ Controllers (30 handlers) │
|
||||
* ├────────────────────────────────────────────────────────────────────────┤
|
||||
* │ One controller per API endpoint. Responsibilities: │
|
||||
* │ • 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: │
|
||||
* │ • Build the upstream URL (via mirror.helper) │
|
||||
@@ -360,7 +364,7 @@
|
||||
* │ Routes │
|
||||
* ├────────────────────────────────────────────────────────────────────────┤
|
||||
* │ apiRoutes.js │
|
||||
* │ • Defines all 28 endpoint paths with HTTP methods │
|
||||
* │ • Defines all 40 endpoint paths with HTTP methods │
|
||||
* │ • Maps paths to controller functions │
|
||||
* │ • Embeds OpenAPI 3.0 spec for auto-documentation │
|
||||
* │ │
|
||||
@@ -778,7 +782,7 @@
|
||||
* │ 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: │
|
||||
* │ - HTTP status code (200, or expected error code) │
|
||||
* │ - Response shape (success: true, data present) │
|
||||
@@ -832,12 +836,13 @@
|
||||
* │ VERSION HISTORY │
|
||||
* ├──────────────────────────────────────────────────────────────────────┤
|
||||
* │ │
|
||||
* │ v2.4.1 (Current) │
|
||||
* │ • 28 endpoints, 28 extractors │
|
||||
* │ • LRU cache with per-endpoint TTL │
|
||||
* │ v2.5.1 (Current) │
|
||||
* │ • 40 endpoints, 29 extractors, 30 controllers │
|
||||
* │ • Cache TTLs per endpoint + 60min MAL/AniList ID cache │
|
||||
* │ • 5-mirror failover system │
|
||||
* │ • Vercel serverless deployment │
|
||||
* │ • 35 integration tests │
|
||||
* │ • Direct embed providers (Megavid/AniXo/MegaPlay) │
|
||||
* │ • 40 integration tests │
|
||||
* │ • OpenAPI 3.0 spec embedded in apiRoutes.js │
|
||||
* │ │
|
||||
* └──────────────────────────────────────────────────────────────────────┘
|
||||
@@ -861,13 +866,14 @@
|
||||
* │ Search & Filter │ search, filter, azList, category │
|
||||
* │ Anime Detail │ animeInfo, watchPage, status │
|
||||
* │ Episode & Stream │ episodeList, episodeListAjax, │
|
||||
* │ │ streamInfo, streamResolver, download │
|
||||
* │ │ streamInfo, streamResolver, download,│
|
||||
* │ │ embedProviders, episodeLatest │
|
||||
* │ Lists │ newRelease, trendingSidebar, │
|
||||
* │ │ topAnimeRankings, recentlyUpdated, │
|
||||
* │ │ completedAnime, upcomingAnime │
|
||||
* │ 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 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 |
|
||||
|-------|------|----------|-------------|
|
||||
@@ -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.
|
||||
|
||||
> **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:**
|
||||
|
||||
```bash
|
||||
@@ -203,7 +205,12 @@ curl "https://anikototvapi.vercel.app/api/info?id=naruto-shippuuden-movie-6-road
|
||||
"producers": ["TV Tokyo", "Shueisha"],
|
||||
"genres": ["Action", "Comedy", "Supernatural"],
|
||||
"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 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
|
||||
|
||||
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,
|
||||
"results": {
|
||||
"status": "healthy",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"uptime": "0h 1m 21s",
|
||||
"uptimeSeconds": 81,
|
||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||
@@ -924,7 +1013,7 @@ curl "https://anikototvapi.vercel.app/api/stats"
|
||||
"ttl": "5 minutes",
|
||||
"description": "Map-based cache with TTL expiration"
|
||||
},
|
||||
"endpoints": 38,
|
||||
"endpoints": 40,
|
||||
"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,
|
||||
"results": {
|
||||
"status": "healthy",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"uptime": "0h 1m 21s",
|
||||
"uptimeSeconds": 81,
|
||||
"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,
|
||||
"results": {
|
||||
"status": "healthy",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"uptime": "0h 1m 21s",
|
||||
"uptimeSeconds": 81,
|
||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||
|
||||
+11
-11
@@ -1,13 +1,13 @@
|
||||
# // === HEADER ===
|
||||
# AniKotoAPI v2.4.1
|
||||
# AniKotoAPI v2.5.1
|
||||
# Free REST API for Anime Data
|
||||
# Scraping anikototv.to with Cheerio
|
||||
|
||||
---
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> 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 |
|
||||
|---|---|
|
||||
| **38+ Endpoints** | Full coverage of anime data, streaming, and discovery |
|
||||
| **28 HTML Scrapers** | Cheerio-based scrapers for robust data extraction |
|
||||
| **28 Route Handlers** | Organized controllers with clean separation of concerns |
|
||||
| **40+ Endpoints** | Full coverage of anime data, streaming, and discovery |
|
||||
| **29 HTML Scrapers** | Cheerio-based scrapers for robust data extraction |
|
||||
| **30 Route Handlers** | Organized controllers with clean separation of concerns |
|
||||
| **5 Mirror Domains** | Automatic failover across multiple source domains |
|
||||
| **LRU Cache** | Per-endpoint TTL caching for performance |
|
||||
| **Rate Limiting** | 100 requests per 15 minutes per IP |
|
||||
@@ -194,12 +194,12 @@ AniKotoAPI/
|
||||
│ ├── info.js # Anime info
|
||||
│ ├── search.js # Search
|
||||
│ ├── stream.js # Streaming
|
||||
│ └── ... # 38+ route handlers
|
||||
│ └── ... # 40 route handlers
|
||||
├── scrapers/ # Cheerio HTML scrapers
|
||||
│ ├── home.js
|
||||
│ ├── info.js
|
||||
│ ├── search.js
|
||||
│ └── ... # 28 scrapers
|
||||
│ └── ... # 29 scrapers
|
||||
├── utils/ # Shared utilities
|
||||
│ ├── cache.js # LRU cache with TTL
|
||||
│ ├── rateLimit.js # IP-based rate limiter
|
||||
@@ -218,8 +218,8 @@ AniKotoAPI/
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
- **Scraping Layer**: 28 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.
|
||||
- **Scraping Layer**: 29 Cheerio-based scrapers parse HTML from anikototv.to, isolating page-specific logic from route handlers.
|
||||
- **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.
|
||||
- **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.
|
||||
@@ -350,7 +350,7 @@ All errors return JSON with a consistent structure:
|
||||
|
||||
| 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 |
|
||||
| [examples.md](./examples.md) | Working code examples in cURL, JS, Python |
|
||||
| [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 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 |
|
||||
|:---|:---|
|
||||
@@ -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 |
|
||||
| AZ list, random, genre list | All AJAX-dependent endpoints |
|
||||
| All HTML-scraped endpoints | |
|
||||
| **`/api/stream/embeds` — direct provider iframes** | |
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Streaming Workflow](#streaming-workflow)
|
||||
3. [Endpoint Reference](#endpoint-reference)
|
||||
4. [M3U8 Proxy](#m3u8-proxy)
|
||||
5. [Video Player Integration](#video-player-integration)
|
||||
6. [Error Handling](#error-handling)
|
||||
7. [Performance Tips](#performance-tips)
|
||||
2. [Direct Embed Providers](#direct-embed-providers)
|
||||
3. [Streaming Workflow](#streaming-workflow)
|
||||
4. [Endpoint Reference](#endpoint-reference)
|
||||
5. [M3U8 Proxy](#m3u8-proxy)
|
||||
6. [Video Player Integration](#video-player-integration)
|
||||
7. [Error Handling](#error-handling)
|
||||
8. [Performance Tips](#performance-tips)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "AniKotoAPI",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "AniKotoAPI",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "AniKotoAPI",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"description": "A restful API to provide all information about anime from anikototv.to",
|
||||
"main": "./server.js",
|
||||
"scripts": {
|
||||
|
||||
+11
-11
@@ -4,28 +4,28 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="google-site-verification" content="YOUR_VERIFICATION_CODE" />
|
||||
<title>AniKotoAPI — Free Anime REST API | 38 Endpoints</title>
|
||||
<meta property="og:title" content="AniKotoAPI — Free Anime REST API | 38 Endpoints" />
|
||||
<title>AniKotoAPI — Free Anime REST API | 40 Endpoints</title>
|
||||
<meta property="og:title" content="AniKotoAPI — Free Anime REST API | 40 Endpoints" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://anikototvapi.vercel.app" />
|
||||
<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:locale" content="en_US" />
|
||||
<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:width" content="1200" />
|
||||
<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:site" content="@Shineii86" />
|
||||
<meta name="twitter:creator" content="@Shineii86" />
|
||||
<meta name="twitter:title" content="AniKotoAPI — Free Anime REST API | 38 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:title" content="AniKotoAPI — Free Anime REST API | 40 Endpoints" />
|
||||
<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: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="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="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" />
|
||||
@@ -234,7 +234,7 @@
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebAPI",
|
||||
"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",
|
||||
"image": "https://anikototvapi.vercel.app/share.png",
|
||||
"documentation": "https://github.com/Shineii86/AniKotoAPI",
|
||||
@@ -271,7 +271,7 @@
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -311,7 +311,7 @@ fetch("https://anikototvapi.vercel.app/api")
|
||||
<section class="stats">
|
||||
<div class="container">
|
||||
<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-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>
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ services:
|
||||
startCommand: npm start
|
||||
envVars:
|
||||
- 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
|
||||
* ALT_DOMAINS[0] // "https://anikoto.cz"
|
||||
*/
|
||||
// Verified against the official domain hub: https://anikoto.site/#domains
|
||||
const ALT_DOMAINS = [
|
||||
"https://anikoto.cz",
|
||||
"https://anikoto.me",
|
||||
"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 { resolveExternalIds } from "../extractors/embedProviders.extractor.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 });
|
||||
}
|
||||
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);
|
||||
res.json({ success: true, results: data });
|
||||
} 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 spotlights = [];
|
||||
// NOTE: #hotest .swiper-slide.item = spotlight carousel on homepage
|
||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||
const title = $(el).find("h2.title.d-title").text().trim() || "";
|
||||
const japaneseTitle = $(el).find("h2.title.d-title").attr("data-jp") || "";
|
||||
const description = $(el).find(".desc").text().trim() || "";
|
||||
const rating = $(el).find("i.rating").text().trim() || "";
|
||||
// NOTE: Dual selector — new layout (2026-10): #hotest .swiper-slide with
|
||||
// .cover img, h2.ak-title, .ak-desc, .ak-detail spans, a.btn.btn-primary.
|
||||
// Old layout: .swiper-slide.item with a.play, .image div[style], h2.title,
|
||||
// .desc, i.rating/i.quality/i.sub/i.dub/i.date. Both remain supported.
|
||||
$("#hotest .swiper-slide").each((i, el) => {
|
||||
const watchHref = $(el).find("a.play, a.btn.btn-primary").attr("href") || "";
|
||||
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 sub = parseInt($(el).find("i.sub").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) {
|
||||
spotlights.push({
|
||||
@@ -63,7 +82,8 @@ const extractHomeInfo = async () => {
|
||||
quality,
|
||||
sub,
|
||||
dub,
|
||||
date
|
||||
date,
|
||||
type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,17 +52,37 @@ const extractSpotlight = async () => {
|
||||
|
||||
const spotlights = [];
|
||||
|
||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||
const title = $(el).find("h2.title.d-title").text().trim() || "";
|
||||
const japaneseTitle = $(el).find("h2.title.d-title").attr("data-jp") || "";
|
||||
const description = $(el).find(".desc").text().trim() || "";
|
||||
const rating = $(el).find("i.rating").text().trim() || "";
|
||||
// NOTE: Dual selector — new layout (2026-10): #hotest .swiper-slide with
|
||||
// .cover img, h2.ak-title, .ak-desc, .ak-detail spans, a.btn.btn-primary.
|
||||
// Old layout: .swiper-slide.item with a.play, .image div[style], h2.title,
|
||||
// .desc, i.rating/i.quality/i.sub/i.dub/i.date. Both remain supported.
|
||||
$("#hotest .swiper-slide").each((i, el) => {
|
||||
const watchHref = $(el).find("a.play, a.btn.btn-primary").attr("href") || "";
|
||||
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 sub = parseInt($(el).find("i.sub").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) {
|
||||
spotlights.push({
|
||||
@@ -75,7 +95,8 @@ const extractSpotlight = async () => {
|
||||
quality,
|
||||
sub,
|
||||
dub,
|
||||
date
|
||||
date,
|
||||
type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -164,6 +164,7 @@ const TTL = {
|
||||
schedule: 1800000, // 30min — schedule changes daily
|
||||
genres: 3600000, // 60min — genre list is static
|
||||
suggestions: 300000, // 5min — suggestions update moderate
|
||||
ids: 3600000, // 60min — MAL/AniList IDs are stable per anime
|
||||
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 { getSearchResults, getSearchSuggestions } from "../controllers/search.controller.js";
|
||||
import { getEpisodeList } from "../controllers/episodeList.controller.js";
|
||||
import { getLatestEpisode } from "../controllers/episodeLatest.controller.js";
|
||||
import { getEpisodeListAjax } from "../controllers/episodeListAjax.controller.js";
|
||||
import { getStreamInfo, getServerList, getMapperServers } from "../controllers/streamInfo.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 { getSpotlight } from "../controllers/spotlight.controller.js";
|
||||
import { getTrending } from "../controllers/trending.controller.js";
|
||||
@@ -153,6 +155,15 @@ app.use((req, res, next) => {
|
||||
// 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 ----
|
||||
app.get("/api/episodes/:id", async (req, res, next) => {
|
||||
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 ----
|
||||
app.get("/api/stream/proxy", async (req, res, next) => {
|
||||
try {
|
||||
@@ -582,7 +602,7 @@ app.get("/api/stats", (req, res) => {
|
||||
ttl: "5 minutes",
|
||||
description: "Map-based cache with TTL expiration",
|
||||
},
|
||||
endpoints: 38,
|
||||
endpoints: 40,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
@@ -636,7 +656,8 @@ app.get("/api/openapi", (req, res) => {
|
||||
paths: {
|
||||
"/": { get: { summary: "Homepage data", tags: ["Home"] } },
|
||||
"/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"] } },
|
||||
"/servers": { get: { summary: "Server list", 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"] } },
|
||||
"/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/embeds": { get: { summary: "Direct embed provider URLs (Megavid/AniXo/MegaPlay)", tags: ["Streaming"] } },
|
||||
"/stream/proxy": { get: { summary: "M3U8 playlist proxy (rewrites URLs)", tags: ["Streaming"] } },
|
||||
"/stream/ts-proxy": { get: { summary: "TS video segment proxy", tags: ["Streaming"] } },
|
||||
"/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: "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 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: "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 },
|
||||
// Streaming
|
||||
{ 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
|
||||
|
||||
Reference in New Issue
Block a user