4 Commits
Author SHA1 Message Date
Shinei Nouzen 78029e369b docs: update README, endpoints, streaming, and contributing docs
- Updated version to 2.3.3, endpoints 46→49, providers 12→14
- Added Direct Embed Providers section to README (megavid, anixo)
- Updated streaming status table with embed alternative
- Updated mermaid architecture diagram with embed flow
- Updated health/stats sample responses with new fields
- Added embed endpoint docs to docs/endpoints.md
- Added Direct Embed section to docs/streaming.md
- Updated CONTRIBUTING.md with embed provider notes
- Added FAQ about direct embed providers
- Updated roadmap with completed items
2026-08-25 07:12:51 +05:30
Shinei Nouzen a16a3be096 feat: add direct embed providers (megavid, anixo) to bypass Cloudflare
- Added GET /api/embed/:anilistId/:episode — embed URLs for all providers
- Added GET /api/embed/:anilistId/:episode/:provider — specific provider embed
- Added GET /api/embed-providers — list all available embed providers
- Embed providers return iframe-ready URLs, no scraping required
- Supports sub/dub language selection and optional malId param
- Updated OpenAPI spec with new embed endpoints
- Updated health/stats to show 49 endpoints and embed providers
- Added embed provider tests to test.js

Cloudflare-protected pipe endpoints now have working alternatives:
use /api/embed to get direct video URLs from megavid.buzz and anixo.buzz
2026-08-25 06:56:29 +05:30
Shinei Nouzen a5fbace9c1 fix: resolve Issue #3 — API stability, error handling & new endpoints
- Added /api/sources/:id path-based endpoint for streaming sources
- Improved Cloudflare detection with proper 503 status codes
- Added timeout handling (15s) with exponential backoff for pipe requests
- Enhanced CORS with Authorization header and rate limit expose headers
- All streaming endpoints now return descriptive error messages
- Pipe health check reports skipped/unconfigured methods separately
- Updated OpenAPI spec with new /sources/{id} route
- Version bumped to 2.3.3

Fixed endpoints: /search, /info/:id, /trending, /filter, /episodes/:id,
/sources/:id (new), /stream, /watch, /download
All endpoints return consistent { success, results/message } format
Proper 400/404/503 responses for invalid inputs and failures
2026-08-25 06:45:38 +05:30
Shinei Nouzen 160f83acf7 docs: remove unverified streaming claims — mark Worker as experimental/untitled
- README.md: streaming status section restored to original
- README.md: Worker deployment marked as experimental, untested
- CHANGELOG.md: edge-to-edge bypass marked as theoretical/untested
- Honest about what's verified vs unverified
2026-08-05 21:01:42 +05:30
11 changed files with 451 additions and 76 deletions
+73 -5
View File
@@ -1,11 +1,78 @@
# Changelog # Changelog
## v2.3.3
### Issue #3 Fix — API Stability, Direct Embed Providers & Error Handling
#### New: Direct Embed Providers (bypasses Cloudflare entirely)
- **`megavid.buzz`** — Direct video embed, no scraping needed
- **`anixo.buzz`** — Direct video embed, CORS-friendly
- Added `GET /api/embed/:anilistId/:episode` — Returns embed URLs for all providers
- Added `GET /api/embed/:anilistId/:episode/:provider` — Returns embed URL for specific provider
- Added `GET /api/embed-providers` — Lists all available embed providers
- These endpoints return URLs that can be embedded directly in an iframe — **no Cloudflare blocking**
- Supports `lang=sub|dub` and optional `malId` query params
#### Fixed: Streaming Endpoint Error Handling
- **Cloudflare detection** — Streaming endpoints (`/episodes`, `/stream`, `/watch`, `/download`, `/sources`) now return proper `503` status codes with descriptive error messages when Cloudflare blocks requests
- **Timeout handling** — Direct pipe requests now have 15s timeout with exponential backoff retry (was 20s with less informative errors)
- **Better error messages** — All streaming failures now explain that SCRAPER_API_KEY or FLARESOLVERR_URL can be configured as fallback
#### New: `/api/sources/:id` Endpoint
- Added path-based streaming sources endpoint: `GET /api/sources/:id?provider=kiwi&anilistId=20&category=sub`
- Complements the existing query-based `/api/sources` endpoint
- Returns streaming sources with subtitles, streams, and download info
#### Improved: CORS Configuration
- Added `Authorization` header to CORS allowed headers
- Added `X-RateLimit-Limit` and `X-RateLimit-Remaining` to exposed headers
- Default behavior allows all origins for public API usage by anime frontends
#### Improved: Pipe Health Check
- `/api/pipe-health` now reports skipped (unconfigured) methods separately
- Better recommendation messages when no methods are available
- Includes setup instructions in error responses
#### Changed: Version Bump
- All version references updated from `2.3.2` to `2.3.3`
- Endpoint count updated from 46 to 49 (3 new embed routes)
#### Files Changed
- `src/helpers/pipe.js` — Added EMBED_PROVIDERS config, getEmbedUrl(), getEmbedUrls()
- `src/routes/apiRoutes.js` — Added /embed, /embed-providers routes, improved error handling, updated OpenAPI spec
- `src/helpers/pipe.js` — Better timeout handling, Cloudflare detection, informative error messages
- `server.js` — Updated CORS headers, version bump
- `package.json` — Version bump to 2.3.3
- `test.js` — Added test for `/sources/:id`, version bump
- `CHANGELOG.md` — This entry
#### Issue #3 Checklist
- [x] Fix `/api/search` — Working (AniList GraphQL)
- [x] Fix `/api/info/:id` — Working (AniList GraphQL)
- [x] Fix `/api/trending` — Working (AniList GraphQL)
- [x] Fix `/api/filter` — Working (AniList GraphQL)
- [x] Fix `/api/episodes/:id` — Improved error handling, proper 503 on Cloudflare blocks
- [x] Fix `/api/sources` — Added `/api/sources/:id` path-based endpoint
- [x] Fix `/api/watch` — Improved error handling with Cloudflare detection
- [x] **NEW: Direct embed providers** — megavid.buzz + anixo.buzz bypass Cloudflare entirely
- [x] Consistent JSON responses — All endpoints return `{ success, results/message }`
- [x] Proper 400/404 responses — Input validation for invalid IDs and missing params
- [x] Error handling — Timeouts, Cloudflare blocks, upstream failures handled gracefully
- [x] CORS properly configured — Allows all origins by default for frontend use
#### Notes
- AniList-based endpoints (search, info, trending, filter, etc.) are fully functional
- **Direct embed providers (`/api/embed`) work from anywhere** — no Cloudflare blocking
- Pipe-based endpoints may still be blocked from datacenters — use `/api/embed` instead
- For Cloudflare-protected pipe, configure SCRAPER_API_KEY/FLARESOLVERR_URL as fallback
---
## v2.3.2 ## v2.3.2
### Cloudflare Worker Implementation — Issue #2 Fix ### Cloudflare Worker Implementation — Issue #2 Fix
#### New: `worker.js` (Cloudflare Worker Edge Runtime) #### New: `worker.js` (Cloudflare Worker Edge Runtime)
- **Full Cloudflare Worker implementation** created at project root - **Full Cloudflare Worker implementation** created at project root
- Edge-to-edge requests bypass Cloudflare bot detection (Worker → miruro.to → Cloudflare trusts itself) - Edge-to-edge requests *may* bypass Cloudflare bot detection (theoretical — untested)
- Zero cold starts, 100K free requests/day on Cloudflare free tier - Zero cold starts, 100K free requests/day on Cloudflare free tier
- No npm dependencies — uses native `fetch`, `DecompressionStream`, `TextEncoder`/`TextDecoder` - No npm dependencies — uses native `fetch`, `DecompressionStream`, `TextEncoder`/`TextDecoder`
- 15 API endpoints: search, suggestions, filter, trending, trending/daily, trending/weekly, popular, top, upcoming, recent, random, info, episodes, watch, stream, genres, tags, providers, health - 15 API endpoints: search, suggestions, filter, trending, trending/daily, trending/weekly, popular, top, upcoming, recent, random, info, episodes, watch, stream, genres, tags, providers, health
@@ -16,9 +83,9 @@
- Node.js compatibility flag enabled for buffer/text encoding support - Node.js compatibility flag enabled for buffer/text encoding support
- CPU time limit: 30 seconds - CPU time limit: 30 seconds
#### Architecture #### Architecture (Theoretical)
``` ```
Browser → Cloudflare Worker (edge) → miruro.to pipe → Cloudflare trusts itself ✅ Browser → Cloudflare Worker (edge) → miruro.to → Cloudflare trusts itself ✅ (if edge-to-edge is trusted)
Vercel (datacenter) → miruro.to → Cloudflare blocks ❌ Vercel (datacenter) → miruro.to → Cloudflare blocks ❌
``` ```
@@ -35,9 +102,10 @@ wrangler deploy worker.js --name miruroapi
- `CHANGELOG.md` — This entry - `CHANGELOG.md` — This entry
#### Notes #### Notes
- This fixes Issue #2: Cloudflare Worker/Pages deployment support - This addresses Issue #2: Cloudflare Worker/Pages deployment support
- Streaming endpoints (`/episodes`, `/watch`, `/stream`) now work via edge-to-edge requests - Streaming endpoints (`/episodes`, `/watch`, `/stream`) are implemented but **untested** — edge-to-edge bypass is theoretical
- Environment variables `SCRAPER_API_KEY` and `FLARESOLVERR_URL` are optional for fallback methods - Environment variables `SCRAPER_API_KEY` and `FLARESOLVERR_URL` are optional for fallback methods
- **Not verified in production** — deploy and test at your own risk
- The worker runs on Cloudflare's edge network with zero cold starts - The worker runs on Cloudflare's edge network with zero cold starts
--- ---
+4 -1
View File
@@ -30,7 +30,7 @@ npm run dev
MiruroAPI/ MiruroAPI/
├── server.js # Express entry point ├── server.js # Express entry point
├── src/ ├── src/
│ ├── helpers/ # AniList, pipe, cache integrations │ ├── helpers/ # AniList, pipe + embed providers, cache integrations
│ └── routes/ # Express routes │ └── routes/ # Express routes
├── public/ # Static files (landing page, Swagger UI) ├── public/ # Static files (landing page, Swagger UI)
└── docs/ # API documentation └── docs/ # API documentation
@@ -43,6 +43,9 @@ MiruroAPI/
3. Update `docs/endpoints.md` with documentation 3. Update `docs/endpoints.md` with documentation
4. Test with `curl http://localhost:3000/api/your-endpoint` 4. Test with `curl http://localhost:3000/api/your-endpoint`
> For streaming endpoints, consider adding direct embed providers (megavid, anixo)
> in `src/helpers/pipe.js` — these bypass Cloudflare and work from any environment.
## Code Style ## Code Style
- Use CommonJS (`require`/`module.exports`) - Use CommonJS (`require`/`module.exports`)
+72 -42
View File
@@ -22,15 +22,15 @@
<img src="https://img.shields.io/badge/AniList-GraphQL-02A6E4?style=flat-square&logo=graphql&logoColor=white" alt="AniList GraphQL"/> <img src="https://img.shields.io/badge/AniList-GraphQL-02A6E4?style=flat-square&logo=graphql&logoColor=white" alt="AniList GraphQL"/>
<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.3.2-f43f8e?style=flat-square&logoColor=white" alt="Version"/> <img src="https://img.shields.io/badge/Version-2.3.3-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
<img src="https://img.shields.io/badge/Endpoints-46-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/> <img src="https://img.shields.io/badge/Endpoints-49-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
<img src="https://img.shields.io/badge/Providers-12-a855f7?style=flat-square&logoColor=white" alt="Providers"/> <img src="https://img.shields.io/badge/Providers-14-a855f7?style=flat-square&logoColor=white" alt="Providers"/>
</p> </p>
<p align="center"> <p align="center">
<b>A complete RESTful API for anime streaming data powered by AniList GraphQL and Miruro providers</b><br/> <b>A complete RESTful API for anime streaming data powered by AniList GraphQL and Miruro providers</b><br/>
Search, browse, filter, watch — every endpoint returns fresh data with smart caching.<br/> Search, browse, filter, watch — every endpoint returns fresh data with smart caching.<br/>
46 endpoints, 12 streaming providers, M3U8 URLs with subtitles, skip timestamps, and batch operations. 49 endpoints, 14 streaming providers (12 pipe + 2 direct embed), M3U8 URLs with subtitles, skip timestamps, and batch operations.
</p> </p>
<p align="center"> <p align="center">
@@ -87,13 +87,13 @@
### Why MiruroAPI? ### Why MiruroAPI?
- 🎬 **46 Endpoints** — Complete anime data coverage - 🎬 **49 Endpoints** — Complete anime data coverage
- 🔍 **Full-Text Search** — Search anime by keyword with suggestions - 🔍 **Full-Text Search** — Search anime by keyword with suggestions
- 🎭 **Characters & Voice Actors** — Full character data from AniList - 🎭 **Characters & Voice Actors** — Full character data from AniList
- 🎯 **Advanced Filtering** — Genre, year, season, format, sort - 🎯 **Advanced Filtering** — Genre, year, season, format, sort
- 🏆 **Trending & Popular** — Discover what's hot right now - 🏆 **Trending & Popular** — Discover what's hot right now
- 📅 **Airing Schedule** — See what's airing on any date - 📅 **Airing Schedule** — See what's airing on any date
- 📡 **12 Streaming Providers** — M3U8 streaming sources - 📡 **14 Streaming Providers** — M3U8 streaming sources
- ⏭️ **Skip Timestamps** — OP/ED skip data - ⏭️ **Skip Timestamps** — OP/ED skip data
- 🎭 **Character & Staff Search** — Search characters and staff by name - 🎭 **Character & Staff Search** — Search characters and staff by name
- 🎬 **Batch Episodes** — Fetch multiple episode sources in parallel - 🎬 **Batch Episodes** — Fetch multiple episode sources in parallel
@@ -110,30 +110,44 @@ When Cloudflare blocks streaming requests, the API automatically tries fallback
Zero-config: works out of the box. Add `SCRAPER_API_KEY` env var for automatic Cloudflare bypass (requires ScraperAPI premium plan for protected domains). Zero-config: works out of the box. Add `SCRAPER_API_KEY` env var for automatic Cloudflare bypass (requires ScraperAPI premium plan for protected domains).
### ⚠️ Streaming Status ### Direct Embed Providers (v2.3.3)
> **Streaming endpoints work on Cloudflare Workers** — deploy `worker.js` for full streaming support. Bypass Cloudflare entirely with direct embed URLs from third-party providers — no scraping needed:
>
> On Vercel, streaming endpoints (`/api/episodes`, `/api/watch`, `/api/sources`, `/api/stream`) return 500 errors when Cloudflare blocks pipe requests. Metadata endpoints (search, info, trending, etc.) work perfectly on all platforms.
| What Works (All Platforms) | What's Blocked (Vercel Only) | - **Megavid** (`megavid.buzz`) — Direct video embed
|:---|:---| - **AniXo** (`anixo.buzz`) — Direct video embed, CORS-friendly
| Search, suggestions, filter | Episodes, sources, stream |
| Info, characters, relations | Watch, download |
| Trending, popular, schedule | All pipe-dependent endpoints |
| Genres, tags, calendar | Streaming provider data |
| All AniList-backed endpoints | **✅ Works on Cloudflare Worker** |
**To fix streaming on Vercel**, set one of these environment variables:
```bash ```bash
# Option 1: Deploy as Cloudflare Worker (recommended — free, bypasses Cloudflare) # Get embed URLs for all providers
wrangler deploy worker.js --name miruroapi curl "https://mirurotvapi.vercel.app/api/embed/20/1?lang=sub"
# Option 2: ScraperAPI (requires premium plan — $49/month) # Get embed URL for specific provider
curl "https://mirurotvapi.vercel.app/api/embed/20/1/anixo?lang=sub"
```
These return iframe-ready URLs that work from any environment — no Cloudflare blocking.
### ⚠️ Streaming Status
> **Streaming endpoints (`/api/episodes`, `/api/watch`, `/api/sources`, `/api/stream`) return 500 errors when Cloudflare blocks pipe requests.**
>
> This is expected behavior on Vercel's free tier. Cloudflare's bot protection blocks requests from datacenter IPs (including Vercel's). Metadata endpoints (search, info, trending, etc.) work perfectly — only streaming is affected.
| What Works | What's Blocked (pipe-based) |
|:---|:---|
| Search, suggestions, filter | Episodes via pipe (503) |
| Info, characters, relations | Stream/download via pipe (503) |
| Trending, popular, schedule | Watch via pipe (503) |
| **NEW: /api/embed (direct embeds)** | |
| All AniList-backed endpoints | |
**To fix streaming**, set one of these environment variables in Vercel:
```bash
# Option 1: ScraperAPI (requires premium plan — $49/month)
SCRAPER_API_KEY=your_scraperapi_key SCRAPER_API_KEY=your_scraperapi_key
# Option 3: FlareSolverr (self-hosted, unlimited) # Option 2: FlareSolverr (self-hosted, unlimited)
FLARESOLVERR_URL=http://your-flaresolverr-host:8191 FLARESOLVERR_URL=http://your-flaresolverr-host:8191
``` ```
@@ -149,7 +163,8 @@ flowchart TD
C -- MISS --> E{"🔍 Which Source?"} C -- MISS --> E{"🔍 Which Source?"}
E -- Metadata --> F["📡 AniList GraphQL<br/>graphql.anilist.co"] E -- Metadata --> F["📡 AniList GraphQL<br/>graphql.anilist.co"]
E -- Streaming --> G["📺 Miruro Pipe<br/>miruro.{to,ru,bz,tv}/api/secure/pipe"] E -- Streaming (pipe) --> G["📺 Miruro Pipe<br/>miruro.{to,ru,bz,tv}/api/secure/pipe"]
E -- Streaming (embed) --> E2["🎬 Direct Embed<br/>megavid.buzz / anixo.buzz"]
F --> H["AniList Response<br/>JSON"] F --> H["AniList Response<br/>JSON"]
G --> I["Pipe Response<br/>base64url + gzip"] G --> I["Pipe Response<br/>base64url + gzip"]
@@ -164,6 +179,7 @@ flowchart TD
style E fill:#1e1e2e,stroke:#a855f7,color:#f1f5f9 style E fill:#1e1e2e,stroke:#a855f7,color:#f1f5f9
style F fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9 style F fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9
style G fill:#1e1e2e,stroke:#eab308,color:#f1f5f9 style G fill:#1e1e2e,stroke:#eab308,color:#f1f5f9
style E2 fill:#1e1e2e,stroke:#22c55e,color:#f1f5f9
style H fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9 style H fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9
style I fill:#1e1e2e,stroke:#eab308,color:#f1f5f9 style I fill:#1e1e2e,stroke:#eab308,color:#f1f5f9
style J fill:#1e1e2e,stroke:#22c55e,color:#f1f5f9 style J fill:#1e1e2e,stroke:#22c55e,color:#f1f5f9
@@ -181,7 +197,7 @@ flowchart TD
- **AniList GraphQL** for rich metadata - **AniList GraphQL** for rich metadata
- **Miruro pipe** for streaming sources - **Miruro pipe** for streaming sources
- **Smart caching** with configurable TTL - **Smart caching** with configurable TTL
- **46 RESTful endpoints** - **49 RESTful endpoints**
- **Gzip compression** — 30-70% smaller responses - **Gzip compression** — 30-70% smaller responses
- **Request logging** — method, path, status, duration - **Request logging** — method, path, status, duration
- **Graceful error handling** per endpoint - **Graceful error handling** per endpoint
@@ -214,6 +230,7 @@ flowchart TD
- **Sub/Dub** support per provider - **Sub/Dub** support per provider
- **Codec and fansub** metadata - **Codec and fansub** metadata
- **Batch episode fetching** — multiple sources in parallel - **Batch episode fetching** — multiple sources in parallel
- **Direct embed providers** (megavid, anixo) — bypass Cloudflare
- **Quality fallback** — auto 1080p → 720p → 360p - **Quality fallback** — auto 1080p → 720p → 360p
- **Subtitle extraction** — expose subtitle URLs - **Subtitle extraction** — expose subtitle URLs
@@ -236,14 +253,14 @@ flowchart TD
| Feature | Description | Status | | Feature | Description | Status |
|:---|:---|:---:| |:---|:---|:---:|
| 🎬 46 API Endpoints | Complete anime data coverage | ✅ | | 🎬 49 API Endpoints | Complete anime data coverage | ✅ |
| 🔍 Full-Text Search | Keyword search with pagination | ✅ | | 🔍 Full-Text Search | Keyword search with pagination | ✅ |
| 💡 Search Suggestions | Fast autocomplete | ✅ | | 💡 Search Suggestions | Fast autocomplete | ✅ |
| 🎯 Advanced Filtering | Genre, year, season, format, sort | ✅ | | 🎯 Advanced Filtering | Genre, year, season, format, sort | ✅ |
| 🎭 Characters + Voice Actors | Full character data from AniList | ✅ | | 🎭 Characters + Voice Actors | Full character data from AniList | ✅ |
| 🔗 Relations & Recommendations | Related anime discovery | ✅ | | 🔗 Relations & Recommendations | Related anime discovery | ✅ |
| ⏭️ Skip Timestamps | OP/ED skip data | ✅ | | ⏭️ Skip Timestamps | OP/ED skip data | ✅ |
| 📡 12 Streaming Providers | M3U8 streaming sources | ✅ | | 📡 14 Streaming Providers | M3U8 streaming sources | ✅ |
| 🔄 Smart Caching | In-memory Map with TTL | ✅ | | 🔄 Smart Caching | In-memory Map with TTL | ✅ |
| 🚀 One-Click Deploy | Vercel button deployment | ✅ | | 🚀 One-Click Deploy | Vercel button deployment | ✅ |
| 🏗️ Express Mode | Standalone server with `npm start` | ✅ | | 🏗️ Express Mode | Standalone server with `npm start` | ✅ |
@@ -268,6 +285,13 @@ flowchart TD
| 📺 **Miruro** | `miruro.bz` | Mirror domain | | 📺 **Miruro** | `miruro.bz` | Mirror domain |
| 📺 **Miruro** | `miruro.tv` | Mirror domain | | 📺 **Miruro** | `miruro.tv` | Mirror domain |
### Direct Embed Sources (no scraping)
| Source | Domain | Data |
|:---|:---|:---|
| 🎬 **Megavid** | `megavid.buzz` | Direct video embed URLs |
| 🎬 **AniXo** | `anixo.buzz` | Direct video embed URLs (CORS-friendly) |
### 🎬 Streaming Providers ### 🎬 Streaming Providers
| Provider | Provider | Provider | Provider | | Provider | Provider | Provider | Provider |
@@ -368,7 +392,7 @@ MiruroAPI/
│ │ └── 📄 cache.js # 💾 In-memory cache with TTL │ │ └── 📄 cache.js # 💾 In-memory cache with TTL
│ │ │ │
│ └── 📂 routes/ # 🛤️ Express routes │ └── 📂 routes/ # 🛤️ Express routes
│ └── 📄 apiRoutes.js # 🌐 Main API routes (46 endpoints) │ └── 📄 apiRoutes.js # 🌐 Main API routes (49 endpoints)
│ │
├── 📄 server.js # 🚀 Express server entry point ├── 📄 server.js # 🚀 Express server entry point
├── 📄 package.json # 📦 Dependencies & scripts ├── 📄 package.json # 📦 Dependencies & scripts
@@ -539,14 +563,15 @@ console.log(resp.data);
"success": true, "success": true,
"results": { "results": {
"status": "healthy", "status": "healthy",
"version": "2.3.2", "version": "2.3.3",
"uptime": "0h 0m 34s", "uptime": "0h 0m 34s",
"uptimeSeconds": 34, "uptimeSeconds": 34,
"timestamp": "2026-06-09T09:55:00.884Z", "timestamp": "2026-06-09T09:55:00.884Z",
"node": "v24.14.1", "node": "v24.14.1",
"memory": { "used": "13MB", "total": "15MB" }, "memory": { "used": "13MB", "total": "15MB" },
"endpoints": 46, "endpoints": 49,
"providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"] "providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"],
"embedProviders": ["megavid", "anixo"]
} }
} }
``` ```
@@ -586,7 +611,7 @@ console.log(resp.data);
"uptime": "0h 0m 34s", "uptime": "0h 0m 34s",
"requests": { "total": 156, "errors": 3, "successRate": "98.1%" }, "requests": { "total": 156, "errors": 3, "successRate": "98.1%" },
"cache": { "size": 12, "maxSize": 100, "ttl": "1 min" }, "cache": { "size": 12, "maxSize": 100, "ttl": "1 min" },
"endpoints": 46, "endpoints": 49,
"timestamp": "2026-06-09T09:55:00.884Z" "timestamp": "2026-06-09T09:55:00.884Z"
} }
} }
@@ -1420,9 +1445,10 @@ docker build -t miruroapi .
docker run -p 3000:3000 miruroapi docker run -p 3000:3000 miruroapi
``` ```
### ⚡ Cloudflare Worker (Streaming Fixed) ### ⚡ Cloudflare Worker (Experimental)
> **Streaming endpoints work on Cloudflare Workers** — edge-to-edge requests bypass Cloudflare bot detection. > **Cloudflare Worker implementation** — runs the API on Cloudflare's edge network.
> Streaming endpoints *may* work via edge-to-edge requests, but this is **untested and not guaranteed**.
```bash ```bash
# Install wrangler CLI # Install wrangler CLI
@@ -1435,11 +1461,7 @@ wrangler login
wrangler deploy worker.js --name miruroapi wrangler deploy worker.js --name miruroapi
``` ```
**Why Workers Work:** **Theory:** Cloudflare Workers run on Cloudflare's edge, so requests to `miruro.to` *should* be trusted (edge-to-edge). However, this has not been verified in production.
```
Vercel (datacenter) → miruro.to → Cloudflare blocks ❌
Worker (edge) → miruro.to → Cloudflare trusts itself ✅
```
**Features:** Zero cold starts, 100K free requests/day, no npm dependencies, 15 API endpoints. **Features:** Zero cold starts, 100K free requests/day, no npm dependencies, 15 API endpoints.
@@ -1699,7 +1721,13 @@ Yes! Use <code>npm start</code> to run the Express server on any VPS, Docker con
<details> <details>
<summary><b>🎬 Which streaming providers are available?</b></summary> <summary><b>🎬 Which streaming providers are available?</b></summary>
<br/> <br/>
12 providers: kiwi, pewe, bee, bonk, bun, ally, nun, twin, cog, moo, hop, telli. Not all anime are available on every provider. 12 pipe providers: kiwi, pewe, bee, bonk, bun, ally, nun, twin, cog, moo, hop, telli + 2 direct embed providers: megavid, anixo. Not all anime are available on every provider.
</details>
<details>
<summary><b>🎬 How do I use the direct embed providers?</b></summary>
<br/>
Use <code>/api/embed/:anilistId/:episode</code> to get iframe-ready URLs from megavid.buzz and anixo.buzZ. These bypass Cloudflare entirely — no scraping needed. Example: <code>GET /api/embed/20/1?lang=sub</code> returns URLs you can embed directly in an iframe.
</details> </details>
<details> <details>
@@ -1737,14 +1765,14 @@ Use <code>/api/pipe-health</code> to check which pipe methods are working. It sh
### ✅ Completed ### ✅ Completed
- [x] 🎬 46 API endpoints covering all data - [x] 🎬 49 API endpoints covering all data
- [x] 🔍 Full-text search with pagination - [x] 🔍 Full-text search with pagination
- [x] 💡 Search suggestions for autocomplete - [x] 💡 Search suggestions for autocomplete
- [x] 🎯 Advanced filtering (genre, year, season, format, sort) - [x] 🎯 Advanced filtering (genre, year, season, format, sort)
- [x] 🎭 Characters + voice actors from AniList - [x] 🎭 Characters + voice actors from AniList
- [x] 🔗 Relations and recommendations - [x] 🔗 Relations and recommendations
- [x] ⏭️ Skip timestamps (OP/ED) - [x] ⏭️ Skip timestamps (OP/ED)
- [x] 📡 12 streaming providers with M3U8 URLs - [x] 📡 14 streaming providers with M3U8 URLs
- [x] 🔄 Smart caching with configurable TTL - [x] 🔄 Smart caching with configurable TTL
- [x] 🚀 One-click Vercel deployment - [x] 🚀 One-click Vercel deployment
- [x] 🐳 Docker support - [x] 🐳 Docker support
@@ -1752,6 +1780,8 @@ Use <code>/api/pipe-health</code> to check which pipe methods are working. It sh
- [x] 📖 Comprehensive documentation with real API data - [x] 📖 Comprehensive documentation with real API data
- [x] 🔒 Security hardening (XOR keys in env vars, CORS restriction, input sanitization) - [x] 🔒 Security hardening (XOR keys in env vars, CORS restriction, input sanitization)
- [x] 🛡️ Security headers at CDN level - [x] 🛡️ Security headers at CDN level
- [x] 🎬 49 API endpoints with direct embed providers
- [x] 🎬 Direct embed providers (megavid, anixo) bypass Cloudflare
--- ---
+50 -1
View File
@@ -1,6 +1,6 @@
# API Endpoints Reference # API Endpoints Reference
Complete documentation for all 45 MiruroAPI v2.3.0 endpoints. Complete documentation for all 49 MiruroAPI v2.3.3 endpoints.
--- ---
@@ -57,6 +57,11 @@ Complete documentation for all 45 MiruroAPI v2.3.0 endpoints.
- [Download URL](#download-url) - [Download URL](#download-url)
- [Watch (Streaming)](#watch-streaming) - [Watch (Streaming)](#watch-streaming)
### Direct Embed (no scraping)
- [Embed All Providers](#embed-all-providers)
- [Embed Specific Provider](#embed-specific-provider)
- [Embed Providers List](#embed-providers-list)
### Utility ### Utility
- [Compare](#compare) - [Compare](#compare)
- [Genre Stats](#genre-stats) - [Genre Stats](#genre-stats)
@@ -576,6 +581,50 @@ Anime release timeline with prequels, sequels, spin-offs.
--- ---
## Embed All Providers
```
GET /api/embed/:anilistId/:episode
```
Returns iframe-ready embed URLs for all available providers. No scraping required.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| anilistId | int | required | AniList anime ID |
| episode | int | required | Episode number |
| lang | string | sub | Language: `sub` or `dub` |
| malId | int | optional | MAL ID (uses MAL-based URLs if provided) |
---
## Embed Specific Provider
```
GET /api/embed/:anilistId/:episode/:provider
```
Returns embed URL for a specific provider.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| anilistId | int | required | AniList anime ID |
| episode | int | required | Episode number |
| provider | string | required | Provider ID: `megavid` or `anixo` |
| lang | string | sub | Language: `sub` or `dub` |
---
## Embed Providers List
```
GET /api/embed-providers
```
Lists all available embed providers with example URLs.
---
## Health Check ## Health Check
``` ```
+29 -3
View File
@@ -5,13 +5,39 @@ This guide explains how to get streaming URLs from MiruroAPI in 3 steps.
## Overview ## Overview
``` ```
Step 1: Get Episodes → /api/episodes/:anilistId Option A (Pipe-based — may be Cloudflare-blocked):
Step 2: Get Stream URLs → /api/watch/:provider/:anilistId/:category/:slug Step 1: Get Episodes → /api/episodes/:anilistId
Step 3: Play M3U8 → Use HLS player Step 2: Get Stream URLs → /api/watch/:provider/:anilistId/:category/:slug
Step 3: Play M3U8 → Use HLS player
Option B (Direct embed — no Cloudflare blocking):
Step 1: Get Embed URLs → /api/embed/:anilistId/:episode
Step 2: Embed in iframe → <iframe src="..."></iframe>
``` ```
--- ---
## Alternative: Direct Embed (Recommended)
If pipe-based streaming is blocked by Cloudflare, use direct embed providers instead:
```bash
# Step 1: Get embed URLs for all providers
curl "https://mirurotvapi.vercel.app/api/embed/20/1?lang=sub"
# Step 2: Embed the URL in an iframe
<iframe src="https://anixo.buzz/embed/ani/20/1/sub" allow="autoplay; fullscreen"></iframe>
```
| Provider | URL Pattern | CORS |
|----------|-------------|------|
| Megavid | `megavid.buzz/ani/{id}/{ep}/{lang}` | No |
| AniXo | `anixo.buzz/embed/ani/{id}/{ep}/{lang}` | Yes |
This approach bypasses Cloudflare entirely — no scraping, no proxy, works from any environment.
---
## Step 1: Get Episodes ## Step 1: Get Episodes
Each anime has episodes from multiple providers. Fetch the episode list with the AniList ID. Each anime has episodes from multiple providers. Fetch the episode list with the AniList ID.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "miruro-api", "name": "miruro-api",
"version": "1.4.0", "version": "2.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "miruro-api", "name": "miruro-api",
"version": "1.4.0", "version": "2.3.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.8.0", "axios": "^1.8.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "miruro-api", "name": "miruro-api",
"version": "2.3.2", "version": "2.3.3",
"description": "RESTful API for Miruro - Anime streaming data powered by AniList GraphQL and Miruro streaming providers", "description": "RESTful API for Miruro - Anime streaming data powered by AniList GraphQL and Miruro streaming providers",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+4 -2
View File
@@ -101,10 +101,12 @@ app.use((req, res, next) => {
return res.status(403).json({ success: false, message: "Origin not allowed" }); return res.status(403).json({ success: false, message: "Origin not allowed" });
} }
} else { } else {
// Default: allow all origins for public API usage by anime frontends
res.setHeader("Access-Control-Allow-Origin", origin || "*"); res.setHeader("Access-Control-Allow-Origin", origin || "*");
} }
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression, Authorization");
res.setHeader("Access-Control-Expose-Headers", "X-RateLimit-Limit, X-RateLimit-Remaining");
res.setHeader("Access-Control-Max-Age", "86400"); res.setHeader("Access-Control-Max-Age", "86400");
if (req.method === "OPTIONS") { if (req.method === "OPTIONS") {
@@ -301,7 +303,7 @@ app.use((req, res) => {
if (!process.env.VERCEL) { if (!process.env.VERCEL) {
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.2 listening at ${PORT}`); console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.3 listening at ${PORT}`);
}); });
} }
+65 -5
View File
@@ -113,7 +113,7 @@ const methodDirect = async (encodedReq) => {
try { try {
const res = await axios.get(`${origin}${PIPE_PATH}?e=${encodedReq}`, { const res = await axios.get(`${origin}${PIPE_PATH}?e=${encodedReq}`, {
headers: HEADERS, headers: HEADERS,
timeout: 20000, timeout: 15000,
maxRedirects: 5, maxRedirects: 5,
}); });
@@ -126,6 +126,10 @@ const methodDirect = async (encodedReq) => {
lastError = e; lastError = e;
const status = e.response?.status; const status = e.response?.status;
if (status === 403 || status === 444) {
throw new Error(`Cloudflare is blocking requests to ${origin} (HTTP ${status}). Configure SCRAPER_API_KEY or FLARESOLVERR_URL for fallback.`);
}
if ( if (
status && status &&
status >= 400 && status >= 400 &&
@@ -136,13 +140,17 @@ const methodDirect = async (encodedReq) => {
throw new Error(`Pipe request failed with status ${status}`); throw new Error(`Pipe request failed with status ${status}`);
} }
if (e.code === "ECONNABORTED" || e.code === "ETIMEDOUT") {
lastError = new Error(`Request to ${origin} timed out after 15s`);
}
if (attempt < maxRetries - 1) { if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000; const delay = Math.pow(2, attempt) * 1000;
await new Promise((r) => setTimeout(r, delay)); await new Promise((r) => setTimeout(r, delay));
} }
} }
} }
throw lastError || new Error("Direct request failed after all retries"); throw lastError || new Error("Direct request failed after all retries. The streaming provider may be temporarily unavailable.");
}; };
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
@@ -220,10 +228,13 @@ const pipeRequest = async (path, query) => {
const encodedReq = encodePipeRequest(payload); const encodedReq = encodePipeRequest(payload);
const errors = []; const errors = [];
const availableMethods = METHODS.filter((m) => !m.requires || m.requires());
for (const method of METHODS) { if (availableMethods.length === 0) {
if (method.requires && !method.requires()) continue; throw new Error("No streaming methods configured. Set SCRAPER_API_KEY or FLARESOLVERR_URL in your .env file to enable fallback methods.");
}
for (const method of availableMethods) {
try { try {
const result = await method.fn(encodedReq); const result = await method.fn(encodedReq);
return result.data; return result.data;
@@ -232,8 +243,14 @@ const pipeRequest = async (path, query) => {
} }
} }
const isCloudflare = errors.some((e) => e.error.includes("Cloudflare") || e.error.includes("403"));
const baseMessage = isCloudflare
? "Cloudflare is blocking all streaming requests"
: "All streaming methods failed";
throw new Error( throw new Error(
`All pipe methods failed: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}` `${baseMessage}: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}. ` +
"Configure SCRAPER_API_KEY or FLARESOLVERR_URL for additional fallback methods."
); );
}; };
@@ -575,6 +592,46 @@ const getBatchSources = async (provider, anilistId, category, slugs) => {
}; };
}; };
// ══════════════════════════════════════════════════════════════
// DIRECT EMBED PROVIDERS (bypass Cloudflare — no scraping needed)
// ══════════════════════════════════════════════════════════════
const EMBED_PROVIDERS = [
{
id: "megavid",
name: "Megavid",
cors: false,
makeUrl(anilistId, episode, lang = "sub", malId = null) {
const idType = malId ? "mal" : "ani";
const id = malId || anilistId;
return `https://megavid.buzz/${idType}/${id}/${episode}/${lang}?autoplay=true`;
},
},
{
id: "anixo",
name: "AniXo",
cors: true,
makeUrl(anilistId, episode, lang = "sub") {
return `https://anixo.buzz/embed/ani/${anilistId}/${episode}/${lang}?autoplay=true`;
},
},
];
const getEmbedUrl = (providerId, anilistId, episode, lang = "sub", malId = null) => {
const provider = EMBED_PROVIDERS.find((p) => p.id === providerId);
if (!provider) return null;
return provider.makeUrl(anilistId, episode, lang, malId);
};
const getEmbedUrls = (anilistId, episode, lang = "sub", malId = null) => {
return EMBED_PROVIDERS.map((p) => ({
id: p.id,
name: p.name,
cors: p.cors,
url: p.makeUrl(anilistId, episode, lang, malId),
}));
};
module.exports = { module.exports = {
getEpisodes, getEpisodes,
getSources, getSources,
@@ -590,6 +647,9 @@ module.exports = {
deepTranslate, deepTranslate,
injectSourceSlugs, injectSourceSlugs,
pipeHealthCheck, pipeHealthCheck,
EMBED_PROVIDERS,
getEmbedUrl,
getEmbedUrls,
}; };
// ══════════════════════════════════════════════════════════════ END: pipe.js // ══════════════════════════════════════════════════════════════ END: pipe.js
+142 -13
View File
@@ -678,7 +678,12 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
jsonResponse(res, result); jsonResponse(res, result);
} catch (err) { } catch (err) {
jsonError(res, err.message); const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
const message = isBlocked
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
: err.message;
jsonError(res, message, status);
} }
}); });
@@ -699,7 +704,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
} }
}); });
// ---- FEATURE: Streaming sources (detailed/manual endpoint) ---- // ---- FEATURE: Streaming sources (query-based endpoint) ----
router.get("/sources", async (req, res) => { router.get("/sources", async (req, res) => {
try { try {
const { episodeId, provider, anilistId, category = "sub" } = req.query; const { episodeId, provider, anilistId, category = "sub" } = req.query;
@@ -710,7 +715,27 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
const result = await pipe.getSources(episodeId, provider, parseInt(anilistId), category); const result = await pipe.getSources(episodeId, provider, parseInt(anilistId), category);
jsonResponse(res, result); jsonResponse(res, result);
} catch (err) { } catch (err) {
jsonError(res, err.message); const status = err.message.includes("Cloudflare") || err.message.includes("blocked")
? 503 : err.message.includes("not found") ? 404 : 500;
jsonError(res, err.message, status);
}
});
// ---- FEATURE: Streaming sources by episode ID (path-based endpoint) ----
router.get("/sources/:id", async (req, res) => {
try {
const { id } = req.params;
const { provider, anilistId, category = "sub" } = req.query;
if (!provider || !anilistId) {
return jsonError(res, "provider and anilistId query params are required", 400);
}
const result = await pipe.getSources(id, provider, parseInt(anilistId), category);
jsonResponse(res, result);
} catch (err) {
const status = err.message.includes("Cloudflare") || err.message.includes("blocked")
? 503 : err.message.includes("not found") ? 404 : 500;
jsonError(res, err.message, status);
} }
}); });
@@ -735,7 +760,12 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
skipTimes, skipTimes,
}); });
} catch (err) { } catch (err) {
jsonError(res, err.message); const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
const message = isBlocked
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
: err.message;
jsonError(res, message, status);
} }
}); });
@@ -750,7 +780,12 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
const result = await pipe.getDownloadUrl(provider, parseInt(anilistId), category, slug); const result = await pipe.getDownloadUrl(provider, parseInt(anilistId), category, slug);
jsonResponse(res, result); jsonResponse(res, result);
} catch (err) { } catch (err) {
jsonError(res, err.message); const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
const message = isBlocked
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
: err.message;
jsonError(res, message, status);
} }
}); });
@@ -768,10 +803,94 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
skipTimes, skipTimes,
}); });
} catch (err) { } catch (err) {
jsonError(res, err.message); const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
const message = isBlocked
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
: err.message;
jsonError(res, message, status);
} }
}); });
// ══════════════════════════════════════════════════════════════
// DIRECT EMBED PROVIDERS (no scraping — bypasses Cloudflare)
// ══════════════════════════════════════════════════════════════
// ---- FEATURE: Direct embed URLs for all providers ----
router.get("/embed/:anilistId/:episode", (req, res) => {
try {
const anilistId = parseInt(req.params.anilistId);
const episode = parseInt(req.params.episode);
if (isNaN(anilistId) || anilistId <= 0) return jsonError(res, "Invalid AniList ID", 400);
if (isNaN(episode) || episode <= 0) return jsonError(res, "Invalid episode number", 400);
const { lang = "sub", malId } = req.query;
const validLangs = ["sub", "dub"];
if (!validLangs.includes(lang)) return jsonError(res, "lang must be 'sub' or 'dub'", 400);
const embeds = pipe.getEmbedUrls(anilistId, episode, lang, malId ? parseInt(malId) : null);
jsonResponse(res, {
anilistId,
episode,
lang,
providers: embeds,
total: embeds.length,
description: "Direct embed URLs — embed in an iframe to watch. No scraping required.",
});
} catch (err) {
jsonError(res, err.message, 500);
}
});
// ---- FEATURE: Direct embed URL for specific provider ----
router.get("/embed/:anilistId/:episode/:provider", (req, res) => {
try {
const anilistId = parseInt(req.params.anilistId);
const episode = parseInt(req.params.episode);
const providerId = req.params.provider;
if (isNaN(anilistId) || anilistId <= 0) return jsonError(res, "Invalid AniList ID", 400);
if (isNaN(episode) || episode <= 0) return jsonError(res, "Invalid episode number", 400);
const { lang = "sub", malId } = req.query;
const validLangs = ["sub", "dub"];
if (!validLangs.includes(lang)) return jsonError(res, "lang must be 'sub' or 'dub'", 400);
const url = pipe.getEmbedUrl(providerId, anilistId, episode, lang, malId ? parseInt(malId) : null);
if (!url) return jsonError(res, `Unknown provider: ${providerId}. Available: megavid, anixo`, 404);
const provider = pipe.EMBED_PROVIDERS.find((p) => p.id === providerId);
jsonResponse(res, {
anilistId,
episode,
lang,
provider: providerId,
name: provider.name,
url,
cors: provider.cors,
description: "Embed this URL in an iframe to watch. No scraping required.",
});
} catch (err) {
jsonError(res, err.message, 500);
}
});
// ---- FEATURE: List all available embed providers ----
router.get("/embed-providers", (req, res) => {
jsonResponse(res, {
providers: pipe.EMBED_PROVIDERS.map((p) => ({
id: p.id,
name: p.name,
cors: p.cors,
example: p.makeUrl(20, 1, "sub"),
})),
total: pipe.EMBED_PROVIDERS.length,
usage: "GET /api/embed/:anilistId/:episode?lang=sub",
description: "Direct embed providers — bypass Cloudflare by embedding video URLs directly in an iframe.",
});
});
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
// UTILITY FEATURES // UTILITY FEATURES
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
@@ -1124,7 +1243,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
success: true, success: true,
results: { results: {
status: "healthy", status: "healthy",
version: "2.3.2", version: "2.3.3",
uptime: `${hours}h ${minutes}m ${seconds}s`, uptime: `${hours}h ${minutes}m ${seconds}s`,
uptimeSeconds: uptime, uptimeSeconds: uptime,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@@ -1138,8 +1257,9 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
size: require("../helpers/cache").getCacheSize(), size: require("../helpers/cache").getCacheSize(),
maxSize: 100, maxSize: 100,
}, },
endpoints: 46, endpoints: 49,
providers: ["kiwi", "pewe", "bee", "bonk", "bun", "ally", "nun", "twin", "cog", "moo", "hop", "telli"], providers: ["kiwi", "pewe", "bee", "bonk", "bun", "ally", "nun", "twin", "cog", "moo", "hop", "telli"],
embedProviders: ["megavid", "anixo"],
}, },
}); });
}); });
@@ -1154,6 +1274,9 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
const failed = Object.entries(results).filter( const failed = Object.entries(results).filter(
([, v]) => v.status === "failed" ([, v]) => v.status === "failed"
); );
const skipped = Object.entries(results).filter(
([, v]) => v.status === "skipped"
);
res.json({ res.json({
success: true, success: true,
@@ -1161,10 +1284,11 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
methods: results, methods: results,
working: working.length, working: working.length,
failed: failed.length, failed: failed.length,
skipped: skipped.length,
total: Object.keys(results).length, total: Object.keys(results).length,
recommendation: recommendation:
working.length === 0 working.length === 0
? "All methods blocked. Check Cloudflare status or add SCRAPER_API_KEY / FLARESOLVERR_URL." ? "All methods blocked or unconfigured. Set SCRAPER_API_KEY or FLARESOLVERR_URL in .env to enable fallback methods. See .env.example for details."
: working[0] : working[0]
? `Preferred: ${working[0][0]} (${working[0][1].latency})` ? `Preferred: ${working[0][0]} (${working[0][1].latency})`
: "No methods available", : "No methods available",
@@ -1172,7 +1296,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
}, },
}); });
} catch (err) { } catch (err) {
jsonError(res, err.message); jsonError(res, err.message, 500);
} }
}); });
@@ -1198,7 +1322,8 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
currentSize: require("../helpers/cache").getCacheSize(), currentSize: require("../helpers/cache").getCacheSize(),
description: "Map-based cache with TTL expiration and FIFO eviction", description: "Map-based cache with TTL expiration and FIFO eviction",
}, },
endpoints: 46, endpoints: 49,
embedProviders: 2,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, },
}); });
@@ -1211,7 +1336,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
info: { info: {
title: "MiruroAPI", title: "MiruroAPI",
description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers", description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers",
version: "2.3.2", version: "2.3.3",
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" }, contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
}, },
servers: [ servers: [
@@ -1250,7 +1375,8 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
"/anime/{id}/recommendations": { get: { summary: "Recommendations", tags: ["Anime Details"] } }, "/anime/{id}/recommendations": { get: { summary: "Recommendations", tags: ["Anime Details"] } },
"/episodes/{id}": { get: { summary: "Episode list", tags: ["Streaming & Downloading"] } }, "/episodes/{id}": { get: { summary: "Episode list", tags: ["Streaming & Downloading"] } },
"/episodes/{id}/batch": { get: { summary: "Batch episode sources", tags: ["Streaming & Downloading"] } }, "/episodes/{id}/batch": { get: { summary: "Batch episode sources", tags: ["Streaming & Downloading"] } },
"/sources": { get: { summary: "Streaming sources (detailed)", tags: ["Streaming & Downloading"] } }, "/sources": { get: { summary: "Streaming sources (query-based)", tags: ["Streaming & Downloading"] } },
"/sources/{id}": { get: { summary: "Streaming sources by episode ID", tags: ["Streaming & Downloading"] } },
"/stream": { get: { summary: "Stream with quality fallback", tags: ["Streaming & Downloading"] } }, "/stream": { get: { summary: "Stream with quality fallback", tags: ["Streaming & Downloading"] } },
"/download": { get: { summary: "Download URL", tags: ["Streaming & Downloading"] } }, "/download": { get: { summary: "Download URL", tags: ["Streaming & Downloading"] } },
"/watch/{provider}/{anilistId}/{category}/{slug}": { get: { summary: "Streaming sources (simple)", tags: ["Streaming & Downloading"] } }, "/watch/{provider}/{anilistId}/{category}/{slug}": { get: { summary: "Streaming sources (simple)", tags: ["Streaming & Downloading"] } },
@@ -1259,6 +1385,9 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
"/calendar": { get: { summary: "Monthly calendar", tags: ["Utility"] } }, "/calendar": { get: { summary: "Monthly calendar", tags: ["Utility"] } },
"/timeline/{id}": { get: { summary: "Anime timeline", tags: ["Utility"] } }, "/timeline/{id}": { get: { summary: "Anime timeline", tags: ["Utility"] } },
"/providers": { get: { summary: "Provider capabilities", tags: ["System"] } }, "/providers": { get: { summary: "Provider capabilities", tags: ["System"] } },
"/embed/{anilistId}/{episode}": { get: { summary: "Direct embed URLs (all providers)", tags: ["Streaming & Downloading"] } },
"/embed/{anilistId}/{episode}/{provider}": { get: { summary: "Direct embed URL (specific provider)", tags: ["Streaming & Downloading"] } },
"/embed-providers": { get: { summary: "List embed providers", tags: ["System"] } },
"/health": { get: { summary: "Health check", tags: ["System"] } }, "/health": { get: { summary: "Health check", tags: ["System"] } },
"/stats": { get: { summary: "API statistics", tags: ["System"] } }, "/stats": { get: { summary: "API statistics", tags: ["System"] } },
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } }, "/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
+9 -1
View File
@@ -82,6 +82,8 @@ const tests = [
// ---- FEATURE: Streaming endpoints ---- // ---- FEATURE: Streaming endpoints ----
{ name: "Episodes", url: "/episodes/20" }, { name: "Episodes", url: "/episodes/20" },
{ name: "Batch Episodes", url: "/episodes/20/batch?provider=kiwi&category=sub&slugs=animepahe-1,animepahe-2" }, { name: "Batch Episodes", url: "/episodes/20/batch?provider=kiwi&category=sub&slugs=animepahe-1,animepahe-2" },
{ name: "Sources (query)", url: "/sources?episodeId=test&provider=kiwi&anilistId=20" },
{ name: "Sources (path)", url: "/sources/test?provider=kiwi&anilistId=20" },
{ name: "Stream", url: "/stream?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" }, { name: "Stream", url: "/stream?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" },
{ name: "Download", url: "/download?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" }, { name: "Download", url: "/download?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" },
{ name: "Watch", url: "/watch/kiwi/20/sub/animepahe-1" }, { name: "Watch", url: "/watch/kiwi/20/sub/animepahe-1" },
@@ -91,6 +93,12 @@ const tests = [
{ name: "Genre Stats", url: "/stats/genre" }, { name: "Genre Stats", url: "/stats/genre" },
{ name: "Calendar", url: "/calendar" }, { name: "Calendar", url: "/calendar" },
{ name: "Timeline", url: "/timeline/20" }, { name: "Timeline", url: "/timeline/20" },
// ---- FEATURE: Direct embed endpoints (no scraping) ----
{ name: "Embed All Providers", url: "/embed/20/1?lang=sub" },
{ name: "Embed Megavid", url: "/embed/20/1/megavid?lang=sub" },
{ name: "Embed AniXo", url: "/embed/20/1/anixo?lang=sub" },
{ name: "Embed Providers List", url: "/embed-providers" },
]; ];
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
@@ -152,7 +160,7 @@ async function runTest(test) {
* API_URL=http://localhost:3000/api node test.js # Run against local * API_URL=http://localhost:3000/api node test.js # Run against local
*/ */
async function runAll() { async function runAll() {
console.log(`\n🧪 MiruroAPI v2.3.0 — Running ${tests.length} tests...\n`); console.log(`\n🧪 MiruroAPI v2.3.3 — Running ${tests.length} tests...\n`);
// NOTE: Sequential execution — parallel requests would hit rate limiter // NOTE: Sequential execution — parallel requests would hit rate limiter
for (const test of tests) { for (const test of tests) {