3 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
11 changed files with 432 additions and 52 deletions
+67
View File
@@ -1,5 +1,72 @@
# 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
### Cloudflare Worker Implementation — Issue #2 Fix
+4 -1
View File
@@ -30,7 +30,7 @@ npm run dev
MiruroAPI/
├── server.js # Express entry point
├── src/
│ ├── helpers/ # AniList, pipe, cache integrations
│ ├── helpers/ # AniList, pipe + embed providers, cache integrations
│ └── routes/ # Express routes
├── public/ # Static files (landing page, Swagger UI)
└── docs/ # API documentation
@@ -43,6 +43,9 @@ MiruroAPI/
3. Update `docs/endpoints.md` with documentation
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
- Use CommonJS (`require`/`module.exports`)
+59 -23
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/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.3.2-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/Providers-12-a855f7?style=flat-square&logoColor=white" alt="Providers"/>
<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-49-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
<img src="https://img.shields.io/badge/Providers-14-a855f7?style=flat-square&logoColor=white" alt="Providers"/>
</p>
<p align="center">
<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/>
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 align="center">
@@ -87,13 +87,13 @@
### Why MiruroAPI?
- 🎬 **46 Endpoints** — Complete anime data coverage
- 🎬 **49 Endpoints** — Complete anime data coverage
- 🔍 **Full-Text Search** — Search anime by keyword with suggestions
- 🎭 **Characters & Voice Actors** — Full character data from AniList
- 🎯 **Advanced Filtering** — Genre, year, season, format, sort
- 🏆 **Trending & Popular** — Discover what's hot right now
- 📅 **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
- 🎭 **Character & Staff Search** — Search characters and staff by name
- 🎬 **Batch Episodes** — Fetch multiple episode sources in parallel
@@ -110,18 +110,35 @@ 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).
### Direct Embed Providers (v2.3.3)
Bypass Cloudflare entirely with direct embed URLs from third-party providers — no scraping needed:
- **Megavid** (`megavid.buzz`) — Direct video embed
- **AniXo** (`anixo.buzz`) — Direct video embed, CORS-friendly
```bash
# Get embed URLs for all providers
curl "https://mirurotvapi.vercel.app/api/embed/20/1?lang=sub"
# 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 |
| What Works | What's Blocked (pipe-based) |
|:---|:---|
| Search, suggestions, filter | Episodes, sources, stream |
| Info, characters, relations | Watch, download |
| Trending, popular, schedule | All pipe-dependent endpoints |
| Genres, tags, calendar | Streaming provider data |
| 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:
@@ -146,7 +163,8 @@ flowchart TD
C -- MISS --> E{"🔍 Which Source?"}
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"]
G --> I["Pipe Response<br/>base64url + gzip"]
@@ -161,6 +179,7 @@ flowchart TD
style E fill:#1e1e2e,stroke:#a855f7,color:#f1f5f9
style F fill:#1e1e2e,stroke:#06b6d4,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 I fill:#1e1e2e,stroke:#eab308,color:#f1f5f9
style J fill:#1e1e2e,stroke:#22c55e,color:#f1f5f9
@@ -178,7 +197,7 @@ flowchart TD
- **AniList GraphQL** for rich metadata
- **Miruro pipe** for streaming sources
- **Smart caching** with configurable TTL
- **46 RESTful endpoints**
- **49 RESTful endpoints**
- **Gzip compression** — 30-70% smaller responses
- **Request logging** — method, path, status, duration
- **Graceful error handling** per endpoint
@@ -211,6 +230,7 @@ flowchart TD
- **Sub/Dub** support per provider
- **Codec and fansub** metadata
- **Batch episode fetching** — multiple sources in parallel
- **Direct embed providers** (megavid, anixo) — bypass Cloudflare
- **Quality fallback** — auto 1080p → 720p → 360p
- **Subtitle extraction** — expose subtitle URLs
@@ -233,14 +253,14 @@ flowchart TD
| Feature | Description | Status |
|:---|:---|:---:|
| 🎬 46 API Endpoints | Complete anime data coverage | ✅ |
| 🎬 49 API Endpoints | Complete anime data coverage | ✅ |
| 🔍 Full-Text Search | Keyword search with pagination | ✅ |
| 💡 Search Suggestions | Fast autocomplete | ✅ |
| 🎯 Advanced Filtering | Genre, year, season, format, sort | ✅ |
| 🎭 Characters + Voice Actors | Full character data from AniList | ✅ |
| 🔗 Relations & Recommendations | Related anime discovery | ✅ |
| ⏭️ 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 | ✅ |
| 🚀 One-Click Deploy | Vercel button deployment | ✅ |
| 🏗️ Express Mode | Standalone server with `npm start` | ✅ |
@@ -265,6 +285,13 @@ flowchart TD
| 📺 **Miruro** | `miruro.bz` | 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
| Provider | Provider | Provider | Provider |
@@ -365,7 +392,7 @@ MiruroAPI/
│ │ └── 📄 cache.js # 💾 In-memory cache with TTL
│ │
│ └── 📂 routes/ # 🛤️ Express routes
│ └── 📄 apiRoutes.js # 🌐 Main API routes (46 endpoints)
│ └── 📄 apiRoutes.js # 🌐 Main API routes (49 endpoints)
│
├── 📄 server.js # 🚀 Express server entry point
├── 📄 package.json # 📦 Dependencies & scripts
@@ -536,14 +563,15 @@ console.log(resp.data);
"success": true,
"results": {
"status": "healthy",
"version": "2.3.2",
"version": "2.3.3",
"uptime": "0h 0m 34s",
"uptimeSeconds": 34,
"timestamp": "2026-06-09T09:55:00.884Z",
"node": "v24.14.1",
"memory": { "used": "13MB", "total": "15MB" },
"endpoints": 46,
"providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"]
"endpoints": 49,
"providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"],
"embedProviders": ["megavid", "anixo"]
}
}
```
@@ -583,7 +611,7 @@ console.log(resp.data);
"uptime": "0h 0m 34s",
"requests": { "total": 156, "errors": 3, "successRate": "98.1%" },
"cache": { "size": 12, "maxSize": 100, "ttl": "1 min" },
"endpoints": 46,
"endpoints": 49,
"timestamp": "2026-06-09T09:55:00.884Z"
}
}
@@ -1693,7 +1721,13 @@ Yes! Use <code>npm start</code> to run the Express server on any VPS, Docker con
<details>
<summary><b>🎬 Which streaming providers are available?</b></summary>
<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>
@@ -1731,14 +1765,14 @@ Use <code>/api/pipe-health</code> to check which pipe methods are working. It sh
### ✅ Completed
- [x] 🎬 46 API endpoints covering all data
- [x] 🎬 49 API endpoints covering all data
- [x] 🔍 Full-text search with pagination
- [x] 💡 Search suggestions for autocomplete
- [x] 🎯 Advanced filtering (genre, year, season, format, sort)
- [x] 🎭 Characters + voice actors from AniList
- [x] 🔗 Relations and recommendations
- [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] 🚀 One-click Vercel deployment
- [x] 🐳 Docker support
@@ -1746,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] 🔒 Security hardening (XOR keys in env vars, CORS restriction, input sanitization)
- [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
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)
- [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
- [Compare](#compare)
- [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
```
+29 -3
View File
@@ -5,13 +5,39 @@ This guide explains how to get streaming URLs from MiruroAPI in 3 steps.
## Overview
```
Step 1: Get Episodes → /api/episodes/:anilistId
Step 2: Get Stream URLs → /api/watch/:provider/:anilistId/:category/:slug
Step 3: Play M3U8 → Use HLS player
Option A (Pipe-based — may be Cloudflare-blocked):
Step 1: Get Episodes → /api/episodes/:anilistId
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
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",
"version": "1.4.0",
"version": "2.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "miruro-api",
"version": "1.4.0",
"version": "2.3.2",
"license": "MIT",
"dependencies": {
"axios": "^1.8.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"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",
"main": "server.js",
"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" });
}
} else {
// Default: allow all origins for public API usage by anime frontends
res.setHeader("Access-Control-Allow-Origin", origin || "*");
}
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");
if (req.method === "OPTIONS") {
@@ -301,7 +303,7 @@ app.use((req, res) => {
if (!process.env.VERCEL) {
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 {
const res = await axios.get(`${origin}${PIPE_PATH}?e=${encodedReq}`, {
headers: HEADERS,
timeout: 20000,
timeout: 15000,
maxRedirects: 5,
});
@@ -126,6 +126,10 @@ const methodDirect = async (encodedReq) => {
lastError = e;
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 (
status &&
status >= 400 &&
@@ -136,13 +140,17 @@ const methodDirect = async (encodedReq) => {
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) {
const delay = Math.pow(2, attempt) * 1000;
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 errors = [];
const availableMethods = METHODS.filter((m) => !m.requires || m.requires());
for (const method of METHODS) {
if (method.requires && !method.requires()) continue;
if (availableMethods.length === 0) {
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 {
const result = await method.fn(encodedReq);
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(
`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 = {
getEpisodes,
getSources,
@@ -590,6 +647,9 @@ module.exports = {
deepTranslate,
injectSourceSlugs,
pipeHealthCheck,
EMBED_PROVIDERS,
getEmbedUrl,
getEmbedUrls,
};
// ══════════════════════════════════════════════════════════════ END: pipe.js
+142 -13
View File
@@ -678,7 +678,12 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
jsonResponse(res, result);
} 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) => {
try {
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);
jsonResponse(res, result);
} 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,
});
} 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);
jsonResponse(res, result);
} 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,
});
} 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
// ══════════════════════════════════════════════════════════════
@@ -1124,7 +1243,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
success: true,
results: {
status: "healthy",
version: "2.3.2",
version: "2.3.3",
uptime: `${hours}h ${minutes}m ${seconds}s`,
uptimeSeconds: uptime,
timestamp: new Date().toISOString(),
@@ -1138,8 +1257,9 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
size: require("../helpers/cache").getCacheSize(),
maxSize: 100,
},
endpoints: 46,
endpoints: 49,
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(
([, v]) => v.status === "failed"
);
const skipped = Object.entries(results).filter(
([, v]) => v.status === "skipped"
);
res.json({
success: true,
@@ -1161,10 +1284,11 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
methods: results,
working: working.length,
failed: failed.length,
skipped: skipped.length,
total: Object.keys(results).length,
recommendation:
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]
? `Preferred: ${working[0][0]} (${working[0][1].latency})`
: "No methods available",
@@ -1172,7 +1296,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
},
});
} 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(),
description: "Map-based cache with TTL expiration and FIFO eviction",
},
endpoints: 46,
endpoints: 49,
embedProviders: 2,
timestamp: new Date().toISOString(),
},
});
@@ -1211,7 +1336,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
info: {
title: "MiruroAPI",
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" },
},
servers: [
@@ -1250,7 +1375,8 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
"/anime/{id}/recommendations": { get: { summary: "Recommendations", tags: ["Anime Details"] } },
"/episodes/{id}": { get: { summary: "Episode list", 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"] } },
"/download": { get: { summary: "Download URL", 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"] } },
"/timeline/{id}": { get: { summary: "Anime timeline", tags: ["Utility"] } },
"/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"] } },
"/stats": { get: { summary: "API statistics", tags: ["System"] } },
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
+9 -1
View File
@@ -82,6 +82,8 @@ const tests = [
// ---- FEATURE: Streaming endpoints ----
{ name: "Episodes", url: "/episodes/20" },
{ 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: "Download", url: "/download?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" },
{ name: "Watch", url: "/watch/kiwi/20/sub/animepahe-1" },
@@ -91,6 +93,12 @@ const tests = [
{ name: "Genre Stats", url: "/stats/genre" },
{ name: "Calendar", url: "/calendar" },
{ 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
*/
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
for (const test of tests) {