Author SHA1 Message Date
cloudflare-workers-and-pages[bot] 8d0e0497f7 Add Cloudflare Workers configuration 2026-07-02 04:05:05 +00:00
17 changed files with 2302 additions and 2137 deletions
-5
View File
@@ -27,8 +27,3 @@ SCRAPER_API_KEY=
# Deploy on Render.com free tier, Oracle Cloud, or any VPS # Deploy on Render.com free tier, Oracle Cloud, or any VPS
# Solves Cloudflare Turnstile challenges automatically # Solves Cloudflare Turnstile challenges automatically
FLARESOLVERR_URL= FLARESOLVERR_URL=
# Method 4: Scrape.do (1,000 free credits/month)
# Sign up at https://scrape.do/ (no credit card required)
# Handles Cloudflare, proxy rotation, CAPTCHA solving
SCRAPE_DO_TOKEN=
+6
View File
@@ -2,3 +2,9 @@ node_modules/
.env .env
.vercel/ .vercel/
deploy.cjs deploy.cjs
# wrangler files
.wrangler
.dev.vars*
!.dev.vars.example
!.env.example
-110
View File
@@ -1,115 +1,5 @@
# 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
### Cloudflare Worker Implementation — Issue #2 Fix
#### New: `worker.js` (Cloudflare Worker Edge Runtime)
- **Full Cloudflare Worker implementation** created at project root
- Edge-to-edge requests *may* bypass Cloudflare bot detection (theoretical — untested)
- Zero cold starts, 100K free requests/day on Cloudflare free tier
- 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
#### New: `wrangler.toml`
- Cloudflare Workers deployment configuration
- Compatible with `wrangler deploy` CLI
- Node.js compatibility flag enabled for buffer/text encoding support
- CPU time limit: 30 seconds
#### Architecture (Theoretical)
```
Browser → Cloudflare Worker (edge) → miruro.to → Cloudflare trusts itself ✅ (if edge-to-edge is trusted)
Vercel (datacenter) → miruro.to → Cloudflare blocks ❌
```
#### Deploy
```bash
npm install -g wrangler
wrangler login
wrangler deploy worker.js --name miruroapi
```
#### Files Changed
- `worker.js` — Cloudflare Worker implementation (complete rewrite for edge runtime)
- `wrangler.toml` — Cloudflare Workers deployment configuration
- `CHANGELOG.md` — This entry
#### Notes
- This addresses Issue #2: Cloudflare Worker/Pages deployment support
- 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
- **Not verified in production** — deploy and test at your own risk
- The worker runs on Cloudflare's edge network with zero cold starts
---
## v2.3.1 ## v2.3.1
### Cloudflare Worker Edition — Streaming Bypasses Cloudflare ### Cloudflare Worker Edition — Streaming Bypasses Cloudflare
+1 -61
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 + embed providers, cache integrations │ ├── helpers/ # AniList, pipe, 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,9 +43,6 @@ 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`)
@@ -53,37 +50,6 @@ MiruroAPI/
- Follow existing patterns in the codebase - Follow existing patterns in the codebase
- Keep helpers focused on one task - Keep helpers focused on one task
### File Headers
```js
/*
* ======= • ======= • ======= • ======= • =======• =======
* MiruroAPI — filename.js
* Repository: https://github.com/Shineii86/MiruroAPI
*
* @description
* What this module does.
*
* @author Shinei Nouzen
* @license MIT
* ======= • ======= • ======= • ======= • =======• =======
*/
```
### Section Markers
```js
// ══════════════════════════════════════════════════════════════
// SECTION NAME
// ══════════════════════════════════════════════════════════════
```
### Feature Markers
```js
// ---- FEATURE: Name ----
```
## Commit Messages ## Commit Messages
Use [Conventional Commits](https://www.conventionalcommits.org/): Use [Conventional Commits](https://www.conventionalcommits.org/):
@@ -109,32 +75,6 @@ Use [Conventional Commits](https://www.conventionalcommits.org/):
- Include expected vs actual behavior - Include expected vs actual behavior
- Include your Node.js version - Include your Node.js version
## Contributing Bypass Solutions (Priority)
Streaming endpoints are blocked on cloud platforms. This is the most impactful contribution you can make.
### What to Build
Add a new fallback method in `src/helpers/pipe.js`. The file already has 3 methods (direct, ScraperAPI, FlareSolverr) — add yours as a 4th.
### Free/Open Source Options
| Tool | Language | Install |
|------|----------|---------|
| [nodriver](https://github.com/ultrafunkamsterdam/nodriver) | Python | `pip install nodriver` |
| [Camoufox](https://github.com/nicandris/camoufox) | Python | `pip install camoufox` |
| [curl_cffi](https://github.com/nicandris/curl_cffi) | Python | `pip install curl_cffi` |
| [patchright](https://github.com/nicandris/patchright) | Python | `pip install patchright` |
| [Scrape.do](https://scrape.do) | HTTP API | 1K free credits/mo |
### Steps
1. Fork and clone
2. Add your method in `src/helpers/pipe.js`
3. Add your API key/URL to `.env.example`
4. Test: `curl http://localhost:3000/api/episodes/20`
5. Submit PR
## License ## License
By contributing, you agree that your contributions will be licensed under the MIT License. By contributing, you agree that your contributions will be licensed under the MIT License.
+73 -136
View File
@@ -1,6 +1,3 @@
> [!NOTE]
> **API MAYBE Temporarily Paused** due to suspiciously too many requests. My hosted version of this API is only for testing purposes. You MUST host your own instance to use the API.
<div align="center"> <div align="center">
<img src="https://capsule-render.vercel.app/api?type=waving&height=300&color=gradient&text=𝙈𝙞𝙧𝙪𝙧𝙤𝘼𝙋𝙄&fontAlignY=30&fontSize=100&desc=𝖠%20𝖢𝗈𝗆𝗉𝗅𝖾𝗍𝖾%20𝖱𝖤𝖲𝖳𝖿𝗎𝗅%20𝖠𝖯𝖨%20𝖥𝗈𝗋%20𝖠𝗇𝗂𝗆𝖾%20𝖲𝗍𝗋𝖾𝖺𝗆𝗂𝗇𝗀&descSize=25" /> <img src="https://capsule-render.vercel.app/api?type=waving&height=300&color=gradient&text=𝙈𝙞𝙧𝙪𝙧𝙤𝘼𝙋𝙄&fontAlignY=30&fontSize=100&desc=𝖠%20𝖢𝗈𝗆𝗉𝗅𝖾𝗍𝖾%20𝖱𝖤𝖲𝖳𝖿𝗎𝗅%20𝖠𝖯𝖨%20𝖥𝗈𝗋%20𝖠𝗇𝗂𝗆𝖾%20𝖲𝗍𝗋𝖾𝖺𝗆𝗂𝗇𝗀&descSize=25" />
@@ -22,15 +19,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.3-f43f8e?style=flat-square&logoColor=white" alt="Version"/> <img src="https://img.shields.io/badge/Version-2.3.0-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/Endpoints-46-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"/> <img src="https://img.shields.io/badge/Providers-12-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/>
49 endpoints, 14 streaming providers (12 pipe + 2 direct embed), M3U8 URLs with subtitles, skip timestamps, and batch operations. 46 endpoints, 12 streaming providers, M3U8 URLs with subtitles, skip timestamps, and batch operations.
</p> </p>
<p align="center"> <p align="center">
@@ -87,13 +84,13 @@
### Why MiruroAPI? ### Why MiruroAPI?
- 🎬 **49 Endpoints** — Complete anime data coverage - 🎬 **46 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
- 📡 **14 Streaming Providers** — M3U8 streaming sources - 📡 **12 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
@@ -101,7 +98,7 @@
- 🔒 **CORS Enabled** — Works from any frontend, no proxy needed - 🔒 **CORS Enabled** — Works from any frontend, no proxy needed
- 🚀 **Zero-Config Deploy** — One click to Vercel, or run standalone with Express - 🚀 **Zero-Config Deploy** — One click to Vercel, or run standalone with Express
### 🔄 Self-Healing System (v2.3.2) ### 🔄 Self-Healing System (v2.3.0)
When Cloudflare blocks streaming requests, the API automatically tries fallback methods: When Cloudflare blocks streaming requests, the API automatically tries fallback methods:
- **Direct** → Mirror rotation across 4 domains (miruro.to, miruro.ru, miruro.bz, miruro.tv) - **Direct** → Mirror rotation across 4 domains (miruro.to, miruro.ru, miruro.bz, miruro.tv)
@@ -110,35 +107,18 @@ 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).
### 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 Status
> **Streaming endpoints (`/api/episodes`, `/api/watch`, `/api/sources`, `/api/stream`) return 500 errors when Cloudflare blocks pipe requests.** > **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. > 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) | | What Works | What's Blocked |
|:---|:---| |:---|:---|
| Search, suggestions, filter | Episodes via pipe (503) | | Search, suggestions, filter | Episodes, sources, stream |
| Info, characters, relations | Stream/download via pipe (503) | | Info, characters, relations | Watch, download |
| Trending, popular, schedule | Watch via pipe (503) | | Trending, popular, schedule | All pipe-dependent endpoints |
| **NEW: /api/embed (direct embeds)** | | | Genres, tags, calendar | Streaming provider data |
| All AniList-backed endpoints | | | All AniList-backed endpoints | |
**To fix streaming**, set one of these environment variables in Vercel: **To fix streaming**, set one of these environment variables in Vercel:
@@ -163,8 +143,7 @@ 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 (pipe) --> G["📺 Miruro Pipe<br/>miruro.{to,ru,bz,tv}/api/secure/pipe"] E -- Streaming --> 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"]
@@ -179,7 +158,6 @@ 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
@@ -197,7 +175,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
- **49 RESTful endpoints** - **46 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
@@ -230,7 +208,6 @@ 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
@@ -253,14 +230,14 @@ flowchart TD
| Feature | Description | Status | | Feature | Description | Status |
|:---|:---|:---:| |:---|:---|:---:|
| 🎬 49 API Endpoints | Complete anime data coverage | ✅ | | 🎬 46 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 | ✅ |
| 📡 14 Streaming Providers | M3U8 streaming sources | ✅ | | 📡 12 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` | ✅ |
@@ -285,13 +262,6 @@ 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 |
@@ -392,7 +362,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 (49 endpoints) │ └── 📄 apiRoutes.js # 🌐 Main API routes (46 endpoints)
│ │
├── 📄 server.js # 🚀 Express server entry point ├── 📄 server.js # 🚀 Express server entry point
├── 📄 package.json # 📦 Dependencies & scripts ├── 📄 package.json # 📦 Dependencies & scripts
@@ -563,15 +533,14 @@ console.log(resp.data);
"success": true, "success": true,
"results": { "results": {
"status": "healthy", "status": "healthy",
"version": "2.3.3", "version": "2.3.0",
"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": 49, "endpoints": 46,
"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"]
} }
} }
``` ```
@@ -611,7 +580,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": 49, "endpoints": 46,
"timestamp": "2026-06-09T09:55:00.884Z" "timestamp": "2026-06-09T09:55:00.884Z"
} }
} }
@@ -1445,10 +1414,9 @@ docker build -t miruroapi .
docker run -p 3000:3000 miruroapi docker run -p 3000:3000 miruroapi
``` ```
### ⚡ Cloudflare Worker (Experimental) ### ⚡ Cloudflare Worker (Streaming Works!)
> **Cloudflare Worker implementation** — runs the API on Cloudflare's edge network. > **Streaming endpoints work on Workers** — requests to Miruro bypass Cloudflare bot detection because Workers run on Cloudflare's edge network (edge-to-edge traffic is trusted).
> Streaming endpoints *may* work via edge-to-edge requests, but this is **untested and not guaranteed**.
```bash ```bash
# Install wrangler CLI # Install wrangler CLI
@@ -1457,19 +1425,20 @@ npm install -g wrangler
# Login to Cloudflare # Login to Cloudflare
wrangler login wrangler login
# Deploy worker # Deploy
wrangler deploy worker.js --name miruroapi wrangler deploy worker.js --name miruroapi
``` ```
**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. Your API is live at: `https://miruroapi.YOUR_SUBDOMAIN.workers.dev`
**Features:** Zero cold starts, 100K free requests/day, no npm dependencies, 15 API endpoints. | Feature | Vercel | Cloudflare Worker |
|---------|--------|-------------------|
| Streaming (pipe) | Blocked (403) | Works |
| Cold starts | ~500ms | Zero |
| Free tier | Unlimited | 100K req/day |
| npm deps | Yes | No (native fetch) |
**Optional env vars:** > See `worker.js` for the full Worker implementation with 15 endpoints.
```bash
wrangler secret put SCRAPER_API_KEY # ScraperAPI premium key ($49/mo)
wrangler secret put FLARESOLVERR_URL # FlareSolverr Docker URL
```
--- ---
@@ -1508,8 +1477,7 @@ wrangler secret put FLARESOLVERR_URL # FlareSolverr Docker URL
| Version | Date | Key Changes | | Version | Date | Key Changes |
|:---|:---|:---| |:---|:---|:---|
| **2.3.2** | 2026-08-05 | Cloudflare Worker implementation — edge-to-edge streaming bypass, wrangler.toml, 15 Worker endpoints | | **2.3.0** | 2026-07-02 | Self-healing fallback system — multi-method pipe recovery, /api/pipe-health endpoint, ScraperAPI + FlareSolverr support |
| **2.3.1** | 2026-07-02 | Self-healing fallback system — multi-method pipe recovery, /api/pipe-health endpoint, ScraperAPI + FlareSolverr support |
| **2.1.4** | 2026-06-25 | Security hardening — XOR keys to env vars, CORS restriction, input sanitization, bug fixes (multi-search, deepTranslate, getBestStream) | | **2.1.4** | 2026-06-25 | Security hardening — XOR keys to env vars, CORS restriction, input sanitization, bug fixes (multi-search, deepTranslate, getBestStream) |
| **2.1.3** | 2026-06-25 | Diagnostic sweep — tags, random, character/staff 404 fixes, pipe retry with backoff, security headers | | **2.1.3** | 2026-06-25 | Diagnostic sweep — tags, random, character/staff 404 fixes, pipe retry with backoff, security headers |
| **2.1.2** | 2026-06-25 | Streaming architecture — pru proxy decode, CDN CORS proxy, raw stream URLs | | **2.1.2** | 2026-06-25 | Streaming architecture — pru proxy decode, CDN CORS proxy, raw stream URLs |
@@ -1550,9 +1518,42 @@ Cloudflare detects:
- Missing browser fingerprint headers - Missing browser fingerprint headers
- Non-residential traffic patterns - Non-residential traffic patterns
#### Fix 1: FlareSolverr (Best — Free, Works!) #### Fix 1: Cloudflare Worker (Best — Free, Works!)
FlareSolverr runs a headless browser that bypasses Cloudflare automatically. This is the most reliable fix. Workers run on Cloudflare's edge network. Edge-to-edge requests are trusted — no bot detection.
```bash
# 1. Install wrangler
npm install -g wrangler
# 2. Login
wrangler login
# 3. Deploy
wrangler deploy worker.js --name miruroapi
# 4. Your streaming now works at:
# https://miruroapi.YOUR_SUBDOMAIN.workers.dev/api/episodes/20
```
**Cost:** Free (100K requests/day). No API key needed.
#### Fix 2: ScraperAPI (Paid — $49/month)
ScraperAPI proxies your requests through residential IPs, bypassing Cloudflare.
```bash
# 1. Sign up at https://www.scraperapi.com/ (free 1,000 requests/month)
# 2. Get your API key from the dashboard
# 3. Add to Vercel environment variables:
SCRAPER_API_KEY=your_api_key_here
```
**Cost:** $49/month (premium plan required for protected domains like Miruro).
#### Fix 3: FlareSolverr (Self-Hosted — Unlimited)
FlareSolverr runs a headless browser that bypasses Cloudflare automatically.
```bash ```bash
# 1. Run FlareSolverr on your own server # 1. Run FlareSolverr on your own server
@@ -1569,22 +1570,9 @@ FLARESOLVERR_URL=http://your-server-ip:8191
- Public IP address - Public IP address
- Docker installed - Docker installed
#### Fix 2: ScraperAPI (Paid — $49/month) #### Fix 4: Self-Host the Entire API
ScraperAPI proxies your requests through residential IPs, bypassing Cloudflare. If you run the API on your own VPS (not Vercel), requests come from your residential/datacenter IP which may not be blocked.
```bash
# 1. Sign up at https://www.scraperapi.com/
# 2. Get your API key from the dashboard
# 3. Add to Vercel environment variables:
SCRAPER_API_KEY=your_api_key_here
```
**Cost:** $49/month (premium plan required for protected domains like Miruro).
#### Fix 3: Self-Host the Entire API
If you run the API on your own VPS (not Vercel), requests come from your IP which may not be blocked.
```bash ```bash
# Clone and install # Clone and install
@@ -1623,49 +1611,6 @@ Expected response when streaming works:
} }
``` ```
### 🤝 Contributing Bypass Solutions
Streaming is blocked on all cloud platforms. We need help! If you can implement any of these, PRs are welcome:
#### Free / Open Source (Best for self-hosted)
| Tool | Language | How It Works | Notes |
|------|----------|-------------|-------|
| [nodriver](https://github.com/ultrafunkamsterdam/nodriver) | Python | CDP-based browser automation, bypasses automation detection | Successor to undetected-chromedriver |
| [Camoufox](https://github.com/nicandris/camoufox) | Python | Stealth Firefox with fingerprint spoofing | 82% baseline Turnstile bypass |
| [curl_cffi](https://github.com/nicandris/curl_cffi) | Python | TLS fingerprint impersonation | Lightweight, no browser needed |
| [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) | Docker | Headless Chrome proxy server | Can't solve interactive CAPTCHAs |
| [patchright](https://github.com/nicandris/patchright) | Python | Patched Playwright fork | Drop-in Playwright replacement |
| [scrapy-impersonate](https://github.com/driver-utils/scrapy-impersonate) | Python | Scrapy with TLS impersonation | For Scrapy users |
#### Paid APIs (Good for cloud deployments)
| Service | Free Tier | Price | API Example |
|---------|-----------|-------|-------------|
| [Scrape.do](https://scrape.do) | 1,000 credits/mo | $0.08/1K | `https://api.scrape.do/?url=...&token=...` |
| [ZenRows](https://zenrows.com) | Trial | $49/mo | `https://api.zenrows.com/?url=...&apikey=...` |
| [ScrapingBee](https://scrapingbee.com) | Trial | $49/mo | `https://app.scrapingbee.com/api/v1/?...` |
#### What to Implement
1. **Node.js integration** — Add `nodriver` or `Camoufox` as a pipe fallback in `src/helpers/pipe.js`
2. **Scrape.do adapter** — Simple HTTP proxy, 1K free credits/mo, no browser needed
3. **Playwright stealth mode** — Use `patchright` or `playwright-extra-stealth` for browser-based bypass
4. **Residential proxy support** — Add proxy rotation for pipe requests
#### How to Contribute
1. Fork the repo
2. Add your bypass method as a new fallback in `src/helpers/pipe.js`
3. Add your API key to `.env.example`
4. Test with: `curl http://localhost:3000/api/episodes/20`
5. Submit a PR
```bash
# Example: Add Scrape.do as a fallback
SCRAPE_DO_TOKEN=your_token_here
```
### 🐛 Debug Mode ### 🐛 Debug Mode
```bash ```bash
@@ -1721,13 +1666,7 @@ 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 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. 12 providers: kiwi, pewe, bee, bonk, bun, ally, nun, twin, cog, moo, hop, telli. 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>
@@ -1765,14 +1704,14 @@ Use <code>/api/pipe-health</code> to check which pipe methods are working. It sh
### ✅ Completed ### ✅ Completed
- [x] 🎬 49 API endpoints covering all data - [x] 🎬 46 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] 📡 14 streaming providers with M3U8 URLs - [x] 📡 12 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
@@ -1780,8 +1719,6 @@ 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
--- ---
+48
View File
@@ -5,6 +5,7 @@
``` ```
MiruroAPI/ MiruroAPI/
├── server.js # Express entry point, port 3000 ├── server.js # Express entry point, port 3000
├── worker.js # Cloudflare Worker edition (edge runtime)
├── package.json # name: "miruro-api" ├── package.json # name: "miruro-api"
├── vercel.json # Routes /api/* and /* to server.js ├── vercel.json # Routes /api/* and /* to server.js
├── Dockerfile # Docker support (node:20-alpine) ├── Dockerfile # Docker support (node:20-alpine)
@@ -199,4 +200,51 @@ Error responses:
| cors | ^2.8.5 | CORS headers | | cors | ^2.8.5 | CORS headers |
| dotenv | ^16.4.0 | Environment variables | | dotenv | ^16.4.0 | Environment variables |
## Cloudflare Worker Architecture (v2.3.1)
The Worker edition (`worker.js`) runs the same API on Cloudflare's edge network. Key difference: **streaming endpoints work** because edge-to-edge requests bypass Cloudflare bot detection.
### Why Workers Work
```
Vercel (datacenter IP) → miruro.to → Cloudflare WAF → 403 Blocked
Cloudflare Worker (edge) → miruro.to → Cloudflare internal network → 200 OK
```
### Worker Runtime Differences
| Feature | Express (Node.js) | Cloudflare Worker |
|---------|-------------------|-------------------|
| HTTP client | axios | native fetch |
| Decompression | zlib (Node.js) | DecompressionStream (Web API) |
| Caching | in-memory Map | Workers KV (optional) |
| npm packages | Yes | No (bundle only) |
| Cold starts | ~500ms | Zero |
| CPU limit | None | 10ms (free) / 30s (paid) |
### Worker Endpoints (15)
| Endpoint | Description |
|----------|-------------|
| `GET /api/health` | System health check |
| `GET /api/search?query=` | Full-text anime search |
| `GET /api/suggestions?query=` | Autocomplete suggestions |
| `GET /api/filter?genre=` | Advanced filter |
| `GET /api/trending` | Trending anime |
| `GET /api/popular` | Most popular |
| `GET /api/top` | Top scored |
| `GET /api/random` | Random anime |
| `GET /api/info/:id` | Full anime info |
| `GET /api/anime/:id/characters` | Characters + voice actors |
| `GET /api/anime/:id/relations` | Related anime |
| `GET /api/genres` | All genres |
| `GET /api/episodes/:id` | Episode list (pipe) |
| `GET /api/watch/:provider/:anilistId/:category/:slug` | Streaming sources (pipe) |
### Deploy
```bash
npm install -g wrangler
wrangler login
wrangler deploy worker.js --name miruroapi
```
+1 -50
View File
@@ -1,6 +1,6 @@
# API Endpoints Reference # API Endpoints Reference
Complete documentation for all 49 MiruroAPI v2.3.3 endpoints. Complete documentation for all 45 MiruroAPI v2.3.0 endpoints.
--- ---
@@ -57,11 +57,6 @@ Complete documentation for all 49 MiruroAPI v2.3.3 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)
@@ -581,50 +576,6 @@ 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
``` ```
-26
View File
@@ -5,39 +5,13 @@ This guide explains how to get streaming URLs from MiruroAPI in 3 steps.
## Overview ## Overview
``` ```
Option A (Pipe-based — may be Cloudflare-blocked):
Step 1: Get Episodes → /api/episodes/:anilistId Step 1: Get Episodes → /api/episodes/:anilistId
Step 2: Get Stream URLs → /api/watch/:provider/:anilistId/:category/:slug Step 2: Get Stream URLs → /api/watch/:provider/:anilistId/:category/:slug
Step 3: Play M3U8 → Use HLS player 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.
+1466 -3
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -1,12 +1,14 @@
{ {
"name": "miruro-api", "name": "miruro-api",
"version": "2.3.3", "version": "2.3.0",
"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": {
"start": "node server.js", "start": "node server.js",
"dev": "nodemon server.js", "dev": "nodemon server.js",
"test": "node test.js" "test": "node test.js",
"deploy": "wrangler deploy",
"preview": "wrangler dev"
}, },
"dependencies": { "dependencies": {
"axios": "^1.8.0", "axios": "^1.8.0",
@@ -16,7 +18,8 @@
"express": "^4.21.0" "express": "^4.21.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.0" "nodemon": "^3.1.0",
"wrangler": "^4.106.0"
}, },
"keywords": [ "keywords": [
"anime", "anime",
+2 -4
View File
@@ -101,12 +101,10 @@ 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, Authorization"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression");
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") {
@@ -303,7 +301,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.3 listening at ${PORT}`); console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.0 listening at ${PORT}`);
}); });
} }
+5 -65
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: 15000, timeout: 20000,
maxRedirects: 5, maxRedirects: 5,
}); });
@@ -126,10 +126,6 @@ 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 &&
@@ -140,17 +136,13 @@ 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. The streaming provider may be temporarily unavailable."); throw lastError || new Error("Direct request failed after all retries");
}; };
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
@@ -228,13 +220,10 @@ 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());
if (availableMethods.length === 0) { for (const method of METHODS) {
throw new Error("No streaming methods configured. Set SCRAPER_API_KEY or FLARESOLVERR_URL in your .env file to enable fallback methods."); if (method.requires && !method.requires()) continue;
}
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;
@@ -243,14 +232,8 @@ 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(
`${baseMessage}: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}. ` + `All pipe methods failed: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}`
"Configure SCRAPER_API_KEY or FLARESOLVERR_URL for additional fallback methods."
); );
}; };
@@ -592,46 +575,6 @@ 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,
@@ -647,9 +590,6 @@ module.exports = {
deepTranslate, deepTranslate,
injectSourceSlugs, injectSourceSlugs,
pipeHealthCheck, pipeHealthCheck,
EMBED_PROVIDERS,
getEmbedUrl,
getEmbedUrls,
}; };
// ══════════════════════════════════════════════════════════════ END: pipe.js // ══════════════════════════════════════════════════════════════ END: pipe.js
+13 -142
View File
@@ -678,12 +678,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
jsonResponse(res, result); jsonResponse(res, result);
} catch (err) { } catch (err) {
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403"); jsonError(res, err.message);
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);
} }
}); });
@@ -704,7 +699,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
} }
}); });
// ---- FEATURE: Streaming sources (query-based endpoint) ---- // ---- FEATURE: Streaming sources (detailed/manual 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;
@@ -715,27 +710,7 @@ 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) {
const status = err.message.includes("Cloudflare") || err.message.includes("blocked") jsonError(res, err.message);
? 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);
} }
}); });
@@ -760,12 +735,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
skipTimes, skipTimes,
}); });
} catch (err) { } catch (err) {
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403"); jsonError(res, err.message);
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);
} }
}); });
@@ -780,12 +750,7 @@ 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) {
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403"); jsonError(res, err.message);
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);
} }
}); });
@@ -803,94 +768,10 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
skipTimes, skipTimes,
}); });
} catch (err) { } catch (err) {
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403"); jsonError(res, err.message);
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
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
@@ -1243,7 +1124,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
success: true, success: true,
results: { results: {
status: "healthy", status: "healthy",
version: "2.3.3", version: "2.3.0",
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(),
@@ -1257,9 +1138,8 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
size: require("../helpers/cache").getCacheSize(), size: require("../helpers/cache").getCacheSize(),
maxSize: 100, maxSize: 100,
}, },
endpoints: 49, endpoints: 46,
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"],
}, },
}); });
}); });
@@ -1274,9 +1154,6 @@ 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,
@@ -1284,11 +1161,10 @@ 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 or unconfigured. Set SCRAPER_API_KEY or FLARESOLVERR_URL in .env to enable fallback methods. See .env.example for details." ? "All methods blocked. Check Cloudflare status or add SCRAPER_API_KEY / FLARESOLVERR_URL."
: 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",
@@ -1296,7 +1172,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
}, },
}); });
} catch (err) { } catch (err) {
jsonError(res, err.message, 500); jsonError(res, err.message);
} }
}); });
@@ -1322,8 +1198,7 @@ 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: 49, endpoints: 46,
embedProviders: 2,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, },
}); });
@@ -1336,7 +1211,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.3", version: "2.3.0",
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" }, contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
}, },
servers: [ servers: [
@@ -1375,8 +1250,7 @@ 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 (query-based)", tags: ["Streaming & Downloading"] } }, "/sources": { get: { summary: "Streaming sources (detailed)", 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"] } },
@@ -1385,9 +1259,6 @@ 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"] } },
+1 -9
View File
@@ -82,8 +82,6 @@ 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" },
@@ -93,12 +91,6 @@ 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" },
]; ];
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
@@ -160,7 +152,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.3 — Running ${tests.length} tests...\n`); console.log(`\n🧪 MiruroAPI v2.3.0 — 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) {
+647 -1482
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "miruroapi",
"compatibility_date": "2026-07-02",
"observability": {
"enabled": true
},
"assets": {
"directory": "public"
},
"compatibility_flags": [
"nodejs_compat"
]
}
-22
View File
@@ -1,22 +0,0 @@
name = "miruroapi"
main = "worker.js"
compatibility_date = "2024-01-01"
# Cloudflare Worker configuration for MiruroAPI
# Deploy with: wrangler deploy
# Environment variables (set via wrangler secret put or dashboard)
# SCRAPER_API_KEY = "" # ScraperAPI premium key (optional, $49/mo)
# FLARESOLVERR_URL = "" # FlareSolverr Docker instance URL (optional)
# Route configuration (optional — for custom domains)
# routes = [
# { pattern = "api.your-domain.com/api/*", zone_name = "your-domain.com" }
# ]
# Limits
[limits]
cpu_ms = 30000
# Compatibility flags
compatibility_flags = ["nodejs_compat"]