mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 18:25:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78029e369b | ||
|
|
a16a3be096 | ||
|
|
a5fbace9c1 | ||
|
|
160f83acf7 | ||
|
|
5b020e53b0 | ||
|
|
a2ba1df831 | ||
|
|
d08bb86931 | ||
|
|
4912b5d6eb | ||
|
|
5d2534eff2 | ||
|
|
45b436d752 | ||
|
|
4293683ae4 | ||
|
|
2c97ca2dd8 | ||
|
|
3f667fe87f | ||
|
|
015aa67ec2 | ||
|
|
a918ed9d1a | ||
|
|
125f088570 |
+7
-1
@@ -16,9 +16,10 @@ PRU_PROXY_KEY=a54d389c18527d9fd3e7f0643e27edbe
|
|||||||
# SELF-HEALING FALLBACK METHODS (optional, enables auto-recovery)
|
# SELF-HEALING FALLBACK METHODS (optional, enables auto-recovery)
|
||||||
# ══════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# Method 2: ScraperAPI (free 1,000 req/month, no CC needed)
|
# Method 2: ScraperAPI (requires premium plan — $49/month)
|
||||||
# Sign up at https://www.scraperapi.com/
|
# Sign up at https://www.scraperapi.com/
|
||||||
# Enables automatic fallback when direct requests are blocked by Cloudflare
|
# Enables automatic fallback when direct requests are blocked by Cloudflare
|
||||||
|
# NOTE: Miruro is a protected domain — free plan will NOT work
|
||||||
SCRAPER_API_KEY=
|
SCRAPER_API_KEY=
|
||||||
|
|
||||||
# Method 3: FlareSolverr (self-hosted browser proxy)
|
# Method 3: FlareSolverr (self-hosted browser proxy)
|
||||||
@@ -26,3 +27,8 @@ 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=
|
||||||
|
|||||||
+141
-1
@@ -1,5 +1,145 @@
|
|||||||
# 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
|
||||||
|
### Cloudflare Worker Edition — Streaming Bypasses Cloudflare
|
||||||
|
|
||||||
|
#### New: `worker.js` (Cloudflare Worker)
|
||||||
|
- Full API running on Cloudflare's edge network
|
||||||
|
- **Streaming endpoints work** — edge-to-edge requests bypass bot detection
|
||||||
|
- Zero cold starts, 100K free requests/day
|
||||||
|
- No npm dependencies (native `fetch` + `DecompressionStream`)
|
||||||
|
- 15 endpoints: search, trending, info, episodes, watch, genres, random
|
||||||
|
|
||||||
|
#### Why Workers Work
|
||||||
|
```
|
||||||
|
Vercel (datacenter) → miruro.to → Cloudflare blocks ❌
|
||||||
|
Worker (edge) → miruro.to → Cloudflare trusts itself ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Deploy
|
||||||
|
```bash
|
||||||
|
npm install -g wrangler
|
||||||
|
wrangler login
|
||||||
|
wrangler deploy worker.js --name miruroapi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Files Changed
|
||||||
|
- `worker.js` — Cloudflare Worker implementation (885 lines)
|
||||||
|
- `README.md` — Added Worker deployment section, updated troubleshooting
|
||||||
|
- `docs/architecture.md` — Added Worker architecture section
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.3.0
|
## v2.3.0
|
||||||
### Self-Healing Fallback System — Auto-Recovery from Cloudflare Blocks
|
### Self-Healing Fallback System — Auto-Recovery from Cloudflare Blocks
|
||||||
|
|
||||||
@@ -25,7 +165,7 @@ GET /api/pipe-health
|
|||||||
#### Configuration (all optional, zero-config to start)
|
#### Configuration (all optional, zero-config to start)
|
||||||
| Env Var | Purpose | Free? |
|
| Env Var | Purpose | Free? |
|
||||||
|---------|---------|-------|
|
|---------|---------|-------|
|
||||||
| `SCRAPER_API_KEY` | ScraperAPI proxy | Yes (1K req/mo) |
|
| `SCRAPER_API_KEY` | ScraperAPI proxy | No ($49/mo premium required for protected domains) |
|
||||||
| `FLARESOLVERR_URL` | FlareSolverr browser | Yes (self-hosted) |
|
| `FLARESOLVERR_URL` | FlareSolverr browser | Yes (self-hosted) |
|
||||||
| `PIPE_OBF_KEY` | XOR decode key | N/A |
|
| `PIPE_OBF_KEY` | XOR decode key | N/A |
|
||||||
|
|
||||||
|
|||||||
+61
-1
@@ -30,7 +30,7 @@ npm run dev
|
|||||||
MiruroAPI/
|
MiruroAPI/
|
||||||
├── server.js # Express entry point
|
├── server.js # Express entry point
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── helpers/ # AniList, pipe, cache integrations
|
│ ├── helpers/ # AniList, pipe + embed providers, cache integrations
|
||||||
│ └── routes/ # Express routes
|
│ └── routes/ # Express routes
|
||||||
├── public/ # Static files (landing page, Swagger UI)
|
├── public/ # Static files (landing page, Swagger UI)
|
||||||
└── docs/ # API documentation
|
└── docs/ # API documentation
|
||||||
@@ -43,6 +43,9 @@ MiruroAPI/
|
|||||||
3. Update `docs/endpoints.md` with documentation
|
3. Update `docs/endpoints.md` with documentation
|
||||||
4. Test with `curl http://localhost:3000/api/your-endpoint`
|
4. Test with `curl http://localhost:3000/api/your-endpoint`
|
||||||
|
|
||||||
|
> For streaming endpoints, consider adding direct embed providers (megavid, anixo)
|
||||||
|
> in `src/helpers/pipe.js` — these bypass Cloudflare and work from any environment.
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
- Use CommonJS (`require`/`module.exports`)
|
- Use CommonJS (`require`/`module.exports`)
|
||||||
@@ -50,6 +53,37 @@ 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/):
|
||||||
@@ -75,6 +109,32 @@ 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.
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
> [!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" />
|
||||||
@@ -19,15 +22,15 @@
|
|||||||
<img src="https://img.shields.io/badge/AniList-GraphQL-02A6E4?style=flat-square&logo=graphql&logoColor=white" alt="AniList GraphQL"/>
|
<img src="https://img.shields.io/badge/AniList-GraphQL-02A6E4?style=flat-square&logo=graphql&logoColor=white" alt="AniList GraphQL"/>
|
||||||
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
||||||
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
||||||
<img src="https://img.shields.io/badge/Version-2.3.0-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
<img src="https://img.shields.io/badge/Version-2.3.3-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
||||||
<img src="https://img.shields.io/badge/Endpoints-46-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
<img src="https://img.shields.io/badge/Endpoints-49-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
||||||
<img src="https://img.shields.io/badge/Providers-12-a855f7?style=flat-square&logoColor=white" alt="Providers"/>
|
<img src="https://img.shields.io/badge/Providers-14-a855f7?style=flat-square&logoColor=white" alt="Providers"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<b>A complete RESTful API for anime streaming data powered by AniList GraphQL and Miruro providers</b><br/>
|
<b>A complete RESTful API for anime streaming data powered by AniList GraphQL and Miruro providers</b><br/>
|
||||||
Search, browse, filter, watch — every endpoint returns fresh data with smart caching.<br/>
|
Search, browse, filter, watch — every endpoint returns fresh data with smart caching.<br/>
|
||||||
46 endpoints, 12 streaming providers, M3U8 URLs with subtitles, skip timestamps, and batch operations.
|
49 endpoints, 14 streaming providers (12 pipe + 2 direct embed), M3U8 URLs with subtitles, skip timestamps, and batch operations.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -84,13 +87,13 @@
|
|||||||
|
|
||||||
### Why MiruroAPI?
|
### Why MiruroAPI?
|
||||||
|
|
||||||
- 🎬 **46 Endpoints** — Complete anime data coverage
|
- 🎬 **49 Endpoints** — Complete anime data coverage
|
||||||
- 🔍 **Full-Text Search** — Search anime by keyword with suggestions
|
- 🔍 **Full-Text Search** — Search anime by keyword with suggestions
|
||||||
- 🎭 **Characters & Voice Actors** — Full character data from AniList
|
- 🎭 **Characters & Voice Actors** — Full character data from AniList
|
||||||
- 🎯 **Advanced Filtering** — Genre, year, season, format, sort
|
- 🎯 **Advanced Filtering** — Genre, year, season, format, sort
|
||||||
- 🏆 **Trending & Popular** — Discover what's hot right now
|
- 🏆 **Trending & Popular** — Discover what's hot right now
|
||||||
- 📅 **Airing Schedule** — See what's airing on any date
|
- 📅 **Airing Schedule** — See what's airing on any date
|
||||||
- 📡 **12 Streaming Providers** — M3U8 streaming sources
|
- 📡 **14 Streaming Providers** — M3U8 streaming sources
|
||||||
- ⏭️ **Skip Timestamps** — OP/ED skip data
|
- ⏭️ **Skip Timestamps** — OP/ED skip data
|
||||||
- 🎭 **Character & Staff Search** — Search characters and staff by name
|
- 🎭 **Character & Staff Search** — Search characters and staff by name
|
||||||
- 🎬 **Batch Episodes** — Fetch multiple episode sources in parallel
|
- 🎬 **Batch Episodes** — Fetch multiple episode sources in parallel
|
||||||
@@ -98,14 +101,31 @@
|
|||||||
- 🔒 **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.0)
|
### 🔄 Self-Healing System (v2.3.2)
|
||||||
|
|
||||||
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)
|
||||||
- **ScraperAPI** → Free proxy bypass (1K req/month)
|
- **ScraperAPI** → Proxy bypass via premium residential IPs ($49/mo)
|
||||||
- **FlareSolverr** → Self-hosted browser proxy
|
- **FlareSolverr** → Self-hosted browser proxy
|
||||||
|
|
||||||
Zero-config: works out of the box. Add `SCRAPER_API_KEY` env var for automatic Cloudflare bypass.
|
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
|
||||||
|
|
||||||
@@ -113,18 +133,18 @@ Zero-config: works out of the box. Add `SCRAPER_API_KEY` env var for automatic C
|
|||||||
>
|
>
|
||||||
> 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 |
|
| What Works | What's Blocked (pipe-based) |
|
||||||
|:---|:---|
|
|:---|:---|
|
||||||
| Search, suggestions, filter | Episodes, sources, stream |
|
| Search, suggestions, filter | Episodes via pipe (503) |
|
||||||
| Info, characters, relations | Watch, download |
|
| Info, characters, relations | Stream/download via pipe (503) |
|
||||||
| Trending, popular, schedule | All pipe-dependent endpoints |
|
| Trending, popular, schedule | Watch via pipe (503) |
|
||||||
| Genres, tags, calendar | Streaming provider data |
|
| **NEW: /api/embed (direct embeds)** | |
|
||||||
| 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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Option 1: ScraperAPI (free 1K requests/month)
|
# Option 1: ScraperAPI (requires premium plan — $49/month)
|
||||||
SCRAPER_API_KEY=your_scraperapi_key
|
SCRAPER_API_KEY=your_scraperapi_key
|
||||||
|
|
||||||
# Option 2: FlareSolverr (self-hosted, unlimited)
|
# Option 2: FlareSolverr (self-hosted, unlimited)
|
||||||
@@ -143,7 +163,8 @@ flowchart TD
|
|||||||
C -- MISS --> E{"🔍 Which Source?"}
|
C -- MISS --> E{"🔍 Which Source?"}
|
||||||
|
|
||||||
E -- Metadata --> F["📡 AniList GraphQL<br/>graphql.anilist.co"]
|
E -- Metadata --> F["📡 AniList GraphQL<br/>graphql.anilist.co"]
|
||||||
E -- Streaming --> G["📺 Miruro Pipe<br/>miruro.{to,ru,bz,tv}/api/secure/pipe"]
|
E -- Streaming (pipe) --> G["📺 Miruro Pipe<br/>miruro.{to,ru,bz,tv}/api/secure/pipe"]
|
||||||
|
E -- Streaming (embed) --> E2["🎬 Direct Embed<br/>megavid.buzz / anixo.buzz"]
|
||||||
|
|
||||||
F --> H["AniList Response<br/>JSON"]
|
F --> H["AniList Response<br/>JSON"]
|
||||||
G --> I["Pipe Response<br/>base64url + gzip"]
|
G --> I["Pipe Response<br/>base64url + gzip"]
|
||||||
@@ -158,6 +179,7 @@ flowchart TD
|
|||||||
style E fill:#1e1e2e,stroke:#a855f7,color:#f1f5f9
|
style E fill:#1e1e2e,stroke:#a855f7,color:#f1f5f9
|
||||||
style F fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9
|
style F fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9
|
||||||
style G fill:#1e1e2e,stroke:#eab308,color:#f1f5f9
|
style G fill:#1e1e2e,stroke:#eab308,color:#f1f5f9
|
||||||
|
style E2 fill:#1e1e2e,stroke:#22c55e,color:#f1f5f9
|
||||||
style H fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9
|
style H fill:#1e1e2e,stroke:#06b6d4,color:#f1f5f9
|
||||||
style I fill:#1e1e2e,stroke:#eab308,color:#f1f5f9
|
style I fill:#1e1e2e,stroke:#eab308,color:#f1f5f9
|
||||||
style J fill:#1e1e2e,stroke:#22c55e,color:#f1f5f9
|
style J fill:#1e1e2e,stroke:#22c55e,color:#f1f5f9
|
||||||
@@ -175,7 +197,7 @@ flowchart TD
|
|||||||
- **AniList GraphQL** for rich metadata
|
- **AniList GraphQL** for rich metadata
|
||||||
- **Miruro pipe** for streaming sources
|
- **Miruro pipe** for streaming sources
|
||||||
- **Smart caching** with configurable TTL
|
- **Smart caching** with configurable TTL
|
||||||
- **46 RESTful endpoints**
|
- **49 RESTful endpoints**
|
||||||
- **Gzip compression** — 30-70% smaller responses
|
- **Gzip compression** — 30-70% smaller responses
|
||||||
- **Request logging** — method, path, status, duration
|
- **Request logging** — method, path, status, duration
|
||||||
- **Graceful error handling** per endpoint
|
- **Graceful error handling** per endpoint
|
||||||
@@ -208,6 +230,7 @@ flowchart TD
|
|||||||
- **Sub/Dub** support per provider
|
- **Sub/Dub** support per provider
|
||||||
- **Codec and fansub** metadata
|
- **Codec and fansub** metadata
|
||||||
- **Batch episode fetching** — multiple sources in parallel
|
- **Batch episode fetching** — multiple sources in parallel
|
||||||
|
- **Direct embed providers** (megavid, anixo) — bypass Cloudflare
|
||||||
- **Quality fallback** — auto 1080p → 720p → 360p
|
- **Quality fallback** — auto 1080p → 720p → 360p
|
||||||
- **Subtitle extraction** — expose subtitle URLs
|
- **Subtitle extraction** — expose subtitle URLs
|
||||||
|
|
||||||
@@ -230,14 +253,14 @@ flowchart TD
|
|||||||
|
|
||||||
| Feature | Description | Status |
|
| Feature | Description | Status |
|
||||||
|:---|:---|:---:|
|
|:---|:---|:---:|
|
||||||
| 🎬 46 API Endpoints | Complete anime data coverage | ✅ |
|
| 🎬 49 API Endpoints | Complete anime data coverage | ✅ |
|
||||||
| 🔍 Full-Text Search | Keyword search with pagination | ✅ |
|
| 🔍 Full-Text Search | Keyword search with pagination | ✅ |
|
||||||
| 💡 Search Suggestions | Fast autocomplete | ✅ |
|
| 💡 Search Suggestions | Fast autocomplete | ✅ |
|
||||||
| 🎯 Advanced Filtering | Genre, year, season, format, sort | ✅ |
|
| 🎯 Advanced Filtering | Genre, year, season, format, sort | ✅ |
|
||||||
| 🎭 Characters + Voice Actors | Full character data from AniList | ✅ |
|
| 🎭 Characters + Voice Actors | Full character data from AniList | ✅ |
|
||||||
| 🔗 Relations & Recommendations | Related anime discovery | ✅ |
|
| 🔗 Relations & Recommendations | Related anime discovery | ✅ |
|
||||||
| ⏭️ Skip Timestamps | OP/ED skip data | ✅ |
|
| ⏭️ Skip Timestamps | OP/ED skip data | ✅ |
|
||||||
| 📡 12 Streaming Providers | M3U8 streaming sources | ✅ |
|
| 📡 14 Streaming Providers | M3U8 streaming sources | ✅ |
|
||||||
| 🔄 Smart Caching | In-memory Map with TTL | ✅ |
|
| 🔄 Smart Caching | In-memory Map with TTL | ✅ |
|
||||||
| 🚀 One-Click Deploy | Vercel button deployment | ✅ |
|
| 🚀 One-Click Deploy | Vercel button deployment | ✅ |
|
||||||
| 🏗️ Express Mode | Standalone server with `npm start` | ✅ |
|
| 🏗️ Express Mode | Standalone server with `npm start` | ✅ |
|
||||||
@@ -262,6 +285,13 @@ flowchart TD
|
|||||||
| 📺 **Miruro** | `miruro.bz` | Mirror domain |
|
| 📺 **Miruro** | `miruro.bz` | Mirror domain |
|
||||||
| 📺 **Miruro** | `miruro.tv` | Mirror domain |
|
| 📺 **Miruro** | `miruro.tv` | Mirror domain |
|
||||||
|
|
||||||
|
### Direct Embed Sources (no scraping)
|
||||||
|
|
||||||
|
| Source | Domain | Data |
|
||||||
|
|:---|:---|:---|
|
||||||
|
| 🎬 **Megavid** | `megavid.buzz` | Direct video embed URLs |
|
||||||
|
| 🎬 **AniXo** | `anixo.buzz` | Direct video embed URLs (CORS-friendly) |
|
||||||
|
|
||||||
### 🎬 Streaming Providers
|
### 🎬 Streaming Providers
|
||||||
|
|
||||||
| Provider | Provider | Provider | Provider |
|
| Provider | Provider | Provider | Provider |
|
||||||
@@ -362,7 +392,7 @@ MiruroAPI/
|
|||||||
│ │ └── 📄 cache.js # 💾 In-memory cache with TTL
|
│ │ └── 📄 cache.js # 💾 In-memory cache with TTL
|
||||||
│ │
|
│ │
|
||||||
│ └── 📂 routes/ # 🛤️ Express routes
|
│ └── 📂 routes/ # 🛤️ Express routes
|
||||||
│ └── 📄 apiRoutes.js # 🌐 Main API routes (46 endpoints)
|
│ └── 📄 apiRoutes.js # 🌐 Main API routes (49 endpoints)
|
||||||
│
|
│
|
||||||
├── 📄 server.js # 🚀 Express server entry point
|
├── 📄 server.js # 🚀 Express server entry point
|
||||||
├── 📄 package.json # 📦 Dependencies & scripts
|
├── 📄 package.json # 📦 Dependencies & scripts
|
||||||
@@ -436,12 +466,12 @@ bun dev
|
|||||||
| `ALLOWED_ORIGINS` | `*` | Comma-separated allowed origins (restricts CORS in production) |
|
| `ALLOWED_ORIGINS` | `*` | Comma-separated allowed origins (restricts CORS in production) |
|
||||||
| `PIPE_OBF_KEY` | `71951034...` | Pipe response XOR obfuscation key (hex, 32 chars) |
|
| `PIPE_OBF_KEY` | `71951034...` | Pipe response XOR obfuscation key (hex, 32 chars) |
|
||||||
| `PRU_PROXY_KEY` | `a54d389c...` | PRU proxy URL XOR key (hex, 32 chars) |
|
| `PRU_PROXY_KEY` | `a54d389c...` | PRU proxy URL XOR key (hex, 32 chars) |
|
||||||
| `SCRAPER_API_KEY` | — | ScraperAPI key for Cloudflare bypass (free 1K req/month) |
|
| `SCRAPER_API_KEY` | — | ScraperAPI key for Cloudflare bypass (requires premium plan, $49/mo) |
|
||||||
| `FLARESOLVERR_URL` | — | FlareSolverr URL for browser-based bypass (e.g., `http://127.0.0.1:8191`) |
|
| `FLARESOLVERR_URL` | — | FlareSolverr URL for browser-based bypass (e.g., `http://127.0.0.1:8191`) |
|
||||||
|
|
||||||
> ⚠️ **Required for production:** Set `ALLOWED_ORIGINS` to restrict CORS. Set `PIPE_OBF_KEY` and `PRU_PROXY_KEY` for pipe decoding.
|
> ⚠️ **Required for production:** Set `ALLOWED_ORIGINS` to restrict CORS. Set `PIPE_OBF_KEY` and `PRU_PROXY_KEY` for pipe decoding.
|
||||||
>
|
>
|
||||||
> 📺 **For streaming:** Set `SCRAPER_API_KEY` or `FLARESOLVERR_URL` to bypass Cloudflare bot protection. See [How to Fix Streaming Issues](#-how-to-fix-streaming-issues).
|
> 📺 **For streaming:** Set `SCRAPER_API_KEY` (requires ScraperAPI premium plan) or `FLARESOLVERR_URL` (free, self-hosted) to bypass Cloudflare bot protection. See [How to Fix Streaming Issues](#-how-to-fix-streaming-issues).
|
||||||
|
|
||||||
### Vercel Configuration
|
### Vercel Configuration
|
||||||
|
|
||||||
@@ -533,14 +563,15 @@ console.log(resp.data);
|
|||||||
"success": true,
|
"success": true,
|
||||||
"results": {
|
"results": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "2.3.0",
|
"version": "2.3.3",
|
||||||
"uptime": "0h 0m 34s",
|
"uptime": "0h 0m 34s",
|
||||||
"uptimeSeconds": 34,
|
"uptimeSeconds": 34,
|
||||||
"timestamp": "2026-06-09T09:55:00.884Z",
|
"timestamp": "2026-06-09T09:55:00.884Z",
|
||||||
"node": "v24.14.1",
|
"node": "v24.14.1",
|
||||||
"memory": { "used": "13MB", "total": "15MB" },
|
"memory": { "used": "13MB", "total": "15MB" },
|
||||||
"endpoints": 46,
|
"endpoints": 49,
|
||||||
"providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"]
|
"providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"],
|
||||||
|
"embedProviders": ["megavid", "anixo"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -580,7 +611,7 @@ console.log(resp.data);
|
|||||||
"uptime": "0h 0m 34s",
|
"uptime": "0h 0m 34s",
|
||||||
"requests": { "total": 156, "errors": 3, "successRate": "98.1%" },
|
"requests": { "total": 156, "errors": 3, "successRate": "98.1%" },
|
||||||
"cache": { "size": 12, "maxSize": 100, "ttl": "1 min" },
|
"cache": { "size": 12, "maxSize": 100, "ttl": "1 min" },
|
||||||
"endpoints": 46,
|
"endpoints": 49,
|
||||||
"timestamp": "2026-06-09T09:55:00.884Z"
|
"timestamp": "2026-06-09T09:55:00.884Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1414,6 +1445,32 @@ docker build -t miruroapi .
|
|||||||
docker run -p 3000:3000 miruroapi
|
docker run -p 3000:3000 miruroapi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ⚡ Cloudflare Worker (Experimental)
|
||||||
|
|
||||||
|
> **Cloudflare Worker implementation** — runs the API on Cloudflare's edge network.
|
||||||
|
> Streaming endpoints *may* work via edge-to-edge requests, but this is **untested and not guaranteed**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install wrangler CLI
|
||||||
|
npm install -g wrangler
|
||||||
|
|
||||||
|
# Login to Cloudflare
|
||||||
|
wrangler login
|
||||||
|
|
||||||
|
# Deploy worker
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Features:** Zero cold starts, 100K free requests/day, no npm dependencies, 15 API endpoints.
|
||||||
|
|
||||||
|
**Optional env vars:**
|
||||||
|
```bash
|
||||||
|
wrangler secret put SCRAPER_API_KEY # ScraperAPI premium key ($49/mo)
|
||||||
|
wrangler secret put FLARESOLVERR_URL # FlareSolverr Docker URL
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📜 Available Scripts
|
## 📜 Available Scripts
|
||||||
@@ -1451,7 +1508,8 @@ docker run -p 3000:3000 miruroapi
|
|||||||
|
|
||||||
| Version | Date | Key Changes |
|
| Version | Date | Key Changes |
|
||||||
|:---|:---|:---|
|
|:---|:---|:---|
|
||||||
| **2.3.0** | 2026-07-02 | Self-healing fallback system — multi-method pipe recovery, /api/pipe-health endpoint, ScraperAPI + FlareSolverr support |
|
| **2.3.2** | 2026-08-05 | Cloudflare Worker implementation — edge-to-edge streaming bypass, wrangler.toml, 15 Worker endpoints |
|
||||||
|
| **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 |
|
||||||
@@ -1492,22 +1550,9 @@ Cloudflare detects:
|
|||||||
- Missing browser fingerprint headers
|
- Missing browser fingerprint headers
|
||||||
- Non-residential traffic patterns
|
- Non-residential traffic patterns
|
||||||
|
|
||||||
#### Fix 1: ScraperAPI (Easiest — Free Tier)
|
#### Fix 1: FlareSolverr (Best — Free, Works!)
|
||||||
|
|
||||||
ScraperAPI proxies your requests through residential IPs, bypassing Cloudflare.
|
FlareSolverr runs a headless browser that bypasses Cloudflare automatically. This is the most reliable fix.
|
||||||
|
|
||||||
```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:** Free for 1K requests/month. Paid plans from $49/month for 250K requests.
|
|
||||||
|
|
||||||
#### Fix 2: 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
|
||||||
@@ -1524,9 +1569,22 @@ FLARESOLVERR_URL=http://your-server-ip:8191
|
|||||||
- Public IP address
|
- Public IP address
|
||||||
- Docker installed
|
- Docker installed
|
||||||
|
|
||||||
|
#### Fix 2: ScraperAPI (Paid — $49/month)
|
||||||
|
|
||||||
|
ScraperAPI proxies your requests through residential IPs, bypassing Cloudflare.
|
||||||
|
|
||||||
|
```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
|
#### Fix 3: Self-Host the Entire API
|
||||||
|
|
||||||
If you run the API on your own VPS (not Vercel), requests come from your residential/datacenter IP which may not be blocked.
|
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
|
||||||
@@ -1565,6 +1623,49 @@ 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
|
||||||
@@ -1620,13 +1721,19 @@ Yes! Use <code>npm start</code> to run the Express server on any VPS, Docker con
|
|||||||
<details>
|
<details>
|
||||||
<summary><b>🎬 Which streaming providers are available?</b></summary>
|
<summary><b>🎬 Which streaming providers are available?</b></summary>
|
||||||
<br/>
|
<br/>
|
||||||
12 providers: kiwi, pewe, bee, bonk, bun, ally, nun, twin, cog, moo, hop, telli. Not all anime are available on every provider.
|
12 pipe providers: kiwi, pewe, bee, bonk, bun, ally, nun, twin, cog, moo, hop, telli + 2 direct embed providers: megavid, anixo. Not all anime are available on every provider.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>🎬 How do I use the direct embed providers?</b></summary>
|
||||||
|
<br/>
|
||||||
|
Use <code>/api/embed/:anilistId/:episode</code> to get iframe-ready URLs from megavid.buzz and anixo.buzZ. These bypass Cloudflare entirely — no scraping needed. Example: <code>GET /api/embed/20/1?lang=sub</code> returns URLs you can embed directly in an iframe.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>❌ Why are streaming endpoints returning 500 errors?</b></summary>
|
<summary><b>❌ Why are streaming endpoints returning 500 errors?</b></summary>
|
||||||
<br/>
|
<br/>
|
||||||
Cloudflare's bot protection blocks requests from Vercel's datacenter IPs. The pipe endpoint (<code>miruro.to/api/secure/pipe</code>) returns HTTP 403. To fix this, set <code>SCRAPER_API_KEY</code> (free 1K req/month from ScraperAPI) or <code>FLARESOLVERR_URL</code> (self-hosted browser proxy) in your Vercel environment variables. See <a href="#-how-to-fix-streaming-issues">How to Fix Streaming Issues</a> above.
|
Cloudflare's bot protection blocks requests from Vercel's datacenter IPs. The pipe endpoint (<code>miruro.to/api/secure/pipe</code>) returns HTTP 403. To fix this, set <code>SCRAPER_API_KEY</code> (requires ScraperAPI premium plan, $49/mo) or <code>FLARESOLVERR_URL</code> (self-hosted, free) in your Vercel environment variables. See <a href="#-how-to-fix-streaming-issues">How to Fix Streaming Issues</a> above.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -1658,14 +1765,14 @@ Use <code>/api/pipe-health</code> to check which pipe methods are working. It sh
|
|||||||
|
|
||||||
### ✅ Completed
|
### ✅ Completed
|
||||||
|
|
||||||
- [x] 🎬 46 API endpoints covering all data
|
- [x] 🎬 49 API endpoints covering all data
|
||||||
- [x] 🔍 Full-text search with pagination
|
- [x] 🔍 Full-text search with pagination
|
||||||
- [x] 💡 Search suggestions for autocomplete
|
- [x] 💡 Search suggestions for autocomplete
|
||||||
- [x] 🎯 Advanced filtering (genre, year, season, format, sort)
|
- [x] 🎯 Advanced filtering (genre, year, season, format, sort)
|
||||||
- [x] 🎭 Characters + voice actors from AniList
|
- [x] 🎭 Characters + voice actors from AniList
|
||||||
- [x] 🔗 Relations and recommendations
|
- [x] 🔗 Relations and recommendations
|
||||||
- [x] ⏭️ Skip timestamps (OP/ED)
|
- [x] ⏭️ Skip timestamps (OP/ED)
|
||||||
- [x] 📡 12 streaming providers with M3U8 URLs
|
- [x] 📡 14 streaming providers with M3U8 URLs
|
||||||
- [x] 🔄 Smart caching with configurable TTL
|
- [x] 🔄 Smart caching with configurable TTL
|
||||||
- [x] 🚀 One-click Vercel deployment
|
- [x] 🚀 One-click Vercel deployment
|
||||||
- [x] 🐳 Docker support
|
- [x] 🐳 Docker support
|
||||||
@@ -1673,6 +1780,8 @@ Use <code>/api/pipe-health</code> to check which pipe methods are working. It sh
|
|||||||
- [x] 📖 Comprehensive documentation with real API data
|
- [x] 📖 Comprehensive documentation with real API data
|
||||||
- [x] 🔒 Security hardening (XOR keys in env vars, CORS restriction, input sanitization)
|
- [x] 🔒 Security hardening (XOR keys in env vars, CORS restriction, input sanitization)
|
||||||
- [x] 🛡️ Security headers at CDN level
|
- [x] 🛡️ Security headers at CDN level
|
||||||
|
- [x] 🎬 49 API endpoints with direct embed providers
|
||||||
|
- [x] 🎬 Direct embed providers (megavid, anixo) bypass Cloudflare
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -198,3 +198,5 @@ Error responses:
|
|||||||
| axios | ^1.8.0 | HTTP requests |
|
| axios | ^1.8.0 | HTTP requests |
|
||||||
| cors | ^2.8.5 | CORS headers |
|
| cors | ^2.8.5 | CORS headers |
|
||||||
| dotenv | ^16.4.0 | Environment variables |
|
| dotenv | ^16.4.0 | Environment variables |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+50
-1
@@ -1,6 +1,6 @@
|
|||||||
# API Endpoints Reference
|
# API Endpoints Reference
|
||||||
|
|
||||||
Complete documentation for all 45 MiruroAPI v2.3.0 endpoints.
|
Complete documentation for all 49 MiruroAPI v2.3.3 endpoints.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -57,6 +57,11 @@ Complete documentation for all 45 MiruroAPI v2.3.0 endpoints.
|
|||||||
- [Download URL](#download-url)
|
- [Download URL](#download-url)
|
||||||
- [Watch (Streaming)](#watch-streaming)
|
- [Watch (Streaming)](#watch-streaming)
|
||||||
|
|
||||||
|
### Direct Embed (no scraping)
|
||||||
|
- [Embed All Providers](#embed-all-providers)
|
||||||
|
- [Embed Specific Provider](#embed-specific-provider)
|
||||||
|
- [Embed Providers List](#embed-providers-list)
|
||||||
|
|
||||||
### Utility
|
### Utility
|
||||||
- [Compare](#compare)
|
- [Compare](#compare)
|
||||||
- [Genre Stats](#genre-stats)
|
- [Genre Stats](#genre-stats)
|
||||||
@@ -576,6 +581,50 @@ Anime release timeline with prequels, sequels, spin-offs.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Embed All Providers
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/embed/:anilistId/:episode
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns iframe-ready embed URLs for all available providers. No scraping required.
|
||||||
|
|
||||||
|
| Param | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| anilistId | int | required | AniList anime ID |
|
||||||
|
| episode | int | required | Episode number |
|
||||||
|
| lang | string | sub | Language: `sub` or `dub` |
|
||||||
|
| malId | int | optional | MAL ID (uses MAL-based URLs if provided) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Embed Specific Provider
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/embed/:anilistId/:episode/:provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns embed URL for a specific provider.
|
||||||
|
|
||||||
|
| Param | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| anilistId | int | required | AniList anime ID |
|
||||||
|
| episode | int | required | Episode number |
|
||||||
|
| provider | string | required | Provider ID: `megavid` or `anixo` |
|
||||||
|
| lang | string | sub | Language: `sub` or `dub` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Embed Providers List
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/embed-providers
|
||||||
|
```
|
||||||
|
|
||||||
|
Lists all available embed providers with example URLs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Health Check
|
## Health Check
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+29
-3
@@ -5,13 +5,39 @@ This guide explains how to get streaming URLs from MiruroAPI in 3 steps.
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
```
|
```
|
||||||
Step 1: Get Episodes → /api/episodes/:anilistId
|
Option A (Pipe-based — may be Cloudflare-blocked):
|
||||||
Step 2: Get Stream URLs → /api/watch/:provider/:anilistId/:category/:slug
|
Step 1: Get Episodes → /api/episodes/:anilistId
|
||||||
Step 3: Play M3U8 → Use HLS player
|
Step 2: Get Stream URLs → /api/watch/:provider/:anilistId/:category/:slug
|
||||||
|
Step 3: Play M3U8 → Use HLS player
|
||||||
|
|
||||||
|
Option B (Direct embed — no Cloudflare blocking):
|
||||||
|
Step 1: Get Embed URLs → /api/embed/:anilistId/:episode
|
||||||
|
Step 2: Embed in iframe → <iframe src="..."></iframe>
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Alternative: Direct Embed (Recommended)
|
||||||
|
|
||||||
|
If pipe-based streaming is blocked by Cloudflare, use direct embed providers instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Get embed URLs for all providers
|
||||||
|
curl "https://mirurotvapi.vercel.app/api/embed/20/1?lang=sub"
|
||||||
|
|
||||||
|
# Step 2: Embed the URL in an iframe
|
||||||
|
<iframe src="https://anixo.buzz/embed/ani/20/1/sub" allow="autoplay; fullscreen"></iframe>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Provider | URL Pattern | CORS |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| Megavid | `megavid.buzz/ani/{id}/{ep}/{lang}` | No |
|
||||||
|
| AniXo | `anixo.buzz/embed/ani/{id}/{ep}/{lang}` | Yes |
|
||||||
|
|
||||||
|
This approach bypasses Cloudflare entirely — no scraping, no proxy, works from any environment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 1: Get Episodes
|
## Step 1: Get Episodes
|
||||||
|
|
||||||
Each anime has episodes from multiple providers. Fetch the episode list with the AniList ID.
|
Each anime has episodes from multiple providers. Fetch the episode list with the AniList ID.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "miruro-api",
|
"name": "miruro-api",
|
||||||
"version": "1.4.0",
|
"version": "2.3.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "miruro-api",
|
"name": "miruro-api",
|
||||||
"version": "1.4.0",
|
"version": "2.3.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.0",
|
"axios": "^1.8.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "miruro-api",
|
"name": "miruro-api",
|
||||||
"version": "2.3.0",
|
"version": "2.3.3",
|
||||||
"description": "RESTful API for Miruro - Anime streaming data powered by AniList GraphQL and Miruro streaming providers",
|
"description": "RESTful API for Miruro - Anime streaming data powered by AniList GraphQL and Miruro streaming providers",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -101,10 +101,12 @@ app.use((req, res, next) => {
|
|||||||
return res.status(403).json({ success: false, message: "Origin not allowed" });
|
return res.status(403).json({ success: false, message: "Origin not allowed" });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Default: allow all origins for public API usage by anime frontends
|
||||||
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
||||||
}
|
}
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression");
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression, Authorization");
|
||||||
|
res.setHeader("Access-Control-Expose-Headers", "X-RateLimit-Limit, X-RateLimit-Remaining");
|
||||||
res.setHeader("Access-Control-Max-Age", "86400");
|
res.setHeader("Access-Control-Max-Age", "86400");
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
@@ -301,7 +303,7 @@ app.use((req, res) => {
|
|||||||
|
|
||||||
if (!process.env.VERCEL) {
|
if (!process.env.VERCEL) {
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.0 listening at ${PORT}`);
|
console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.3 listening at ${PORT}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+66
-6
@@ -113,7 +113,7 @@ const methodDirect = async (encodedReq) => {
|
|||||||
try {
|
try {
|
||||||
const res = await axios.get(`${origin}${PIPE_PATH}?e=${encodedReq}`, {
|
const res = await axios.get(`${origin}${PIPE_PATH}?e=${encodedReq}`, {
|
||||||
headers: HEADERS,
|
headers: HEADERS,
|
||||||
timeout: 20000,
|
timeout: 15000,
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,6 +126,10 @@ const methodDirect = async (encodedReq) => {
|
|||||||
lastError = e;
|
lastError = e;
|
||||||
const status = e.response?.status;
|
const status = e.response?.status;
|
||||||
|
|
||||||
|
if (status === 403 || status === 444) {
|
||||||
|
throw new Error(`Cloudflare is blocking requests to ${origin} (HTTP ${status}). Configure SCRAPER_API_KEY or FLARESOLVERR_URL for fallback.`);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
status &&
|
status &&
|
||||||
status >= 400 &&
|
status >= 400 &&
|
||||||
@@ -136,13 +140,17 @@ const methodDirect = async (encodedReq) => {
|
|||||||
throw new Error(`Pipe request failed with status ${status}`);
|
throw new Error(`Pipe request failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.code === "ECONNABORTED" || e.code === "ETIMEDOUT") {
|
||||||
|
lastError = new Error(`Request to ${origin} timed out after 15s`);
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt < maxRetries - 1) {
|
if (attempt < maxRetries - 1) {
|
||||||
const delay = Math.pow(2, attempt) * 1000;
|
const delay = Math.pow(2, attempt) * 1000;
|
||||||
await new Promise((r) => setTimeout(r, delay));
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw lastError || new Error("Direct request failed after all retries");
|
throw lastError || new Error("Direct request failed after all retries. The streaming provider may be temporarily unavailable.");
|
||||||
};
|
};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -153,7 +161,7 @@ const methodScraperAPI = async (encodedReq) => {
|
|||||||
if (!SCRAPER_API_KEY) throw new Error("SCRAPER_API_KEY not configured");
|
if (!SCRAPER_API_KEY) throw new Error("SCRAPER_API_KEY not configured");
|
||||||
|
|
||||||
const targetUrl = `${MIRURO_ORIGINS[0]}${PIPE_PATH}?e=${encodedReq}`;
|
const targetUrl = `${MIRURO_ORIGINS[0]}${PIPE_PATH}?e=${encodedReq}`;
|
||||||
const scraperUrl = `https://api.scraperapi.com/?api_key=${SCRAPER_API_KEY}&url=${encodeURIComponent(targetUrl)}`;
|
const scraperUrl = `https://api.scraperapi.com/?api_key=${SCRAPER_API_KEY}&premium=true&url=${encodeURIComponent(targetUrl)}`;
|
||||||
|
|
||||||
const res = await axios.get(scraperUrl, {
|
const res = await axios.get(scraperUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -220,10 +228,13 @@ const pipeRequest = async (path, query) => {
|
|||||||
const encodedReq = encodePipeRequest(payload);
|
const encodedReq = encodePipeRequest(payload);
|
||||||
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
const availableMethods = METHODS.filter((m) => !m.requires || m.requires());
|
||||||
|
|
||||||
for (const method of METHODS) {
|
if (availableMethods.length === 0) {
|
||||||
if (method.requires && !method.requires()) continue;
|
throw new Error("No streaming methods configured. Set SCRAPER_API_KEY or FLARESOLVERR_URL in your .env file to enable fallback methods.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const method of availableMethods) {
|
||||||
try {
|
try {
|
||||||
const result = await method.fn(encodedReq);
|
const result = await method.fn(encodedReq);
|
||||||
return result.data;
|
return result.data;
|
||||||
@@ -232,8 +243,14 @@ const pipeRequest = async (path, query) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCloudflare = errors.some((e) => e.error.includes("Cloudflare") || e.error.includes("403"));
|
||||||
|
const baseMessage = isCloudflare
|
||||||
|
? "Cloudflare is blocking all streaming requests"
|
||||||
|
: "All streaming methods failed";
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`All pipe methods failed: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}`
|
`${baseMessage}: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}. ` +
|
||||||
|
"Configure SCRAPER_API_KEY or FLARESOLVERR_URL for additional fallback methods."
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -575,6 +592,46 @@ const getBatchSources = async (provider, anilistId, category, slugs) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// DIRECT EMBED PROVIDERS (bypass Cloudflare — no scraping needed)
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const EMBED_PROVIDERS = [
|
||||||
|
{
|
||||||
|
id: "megavid",
|
||||||
|
name: "Megavid",
|
||||||
|
cors: false,
|
||||||
|
makeUrl(anilistId, episode, lang = "sub", malId = null) {
|
||||||
|
const idType = malId ? "mal" : "ani";
|
||||||
|
const id = malId || anilistId;
|
||||||
|
return `https://megavid.buzz/${idType}/${id}/${episode}/${lang}?autoplay=true`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "anixo",
|
||||||
|
name: "AniXo",
|
||||||
|
cors: true,
|
||||||
|
makeUrl(anilistId, episode, lang = "sub") {
|
||||||
|
return `https://anixo.buzz/embed/ani/${anilistId}/${episode}/${lang}?autoplay=true`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getEmbedUrl = (providerId, anilistId, episode, lang = "sub", malId = null) => {
|
||||||
|
const provider = EMBED_PROVIDERS.find((p) => p.id === providerId);
|
||||||
|
if (!provider) return null;
|
||||||
|
return provider.makeUrl(anilistId, episode, lang, malId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmbedUrls = (anilistId, episode, lang = "sub", malId = null) => {
|
||||||
|
return EMBED_PROVIDERS.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
cors: p.cors,
|
||||||
|
url: p.makeUrl(anilistId, episode, lang, malId),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getEpisodes,
|
getEpisodes,
|
||||||
getSources,
|
getSources,
|
||||||
@@ -590,6 +647,9 @@ module.exports = {
|
|||||||
deepTranslate,
|
deepTranslate,
|
||||||
injectSourceSlugs,
|
injectSourceSlugs,
|
||||||
pipeHealthCheck,
|
pipeHealthCheck,
|
||||||
|
EMBED_PROVIDERS,
|
||||||
|
getEmbedUrl,
|
||||||
|
getEmbedUrls,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════ END: pipe.js
|
// ══════════════════════════════════════════════════════════════ END: pipe.js
|
||||||
|
|||||||
+142
-13
@@ -678,7 +678,12 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
|
|
||||||
jsonResponse(res, result);
|
jsonResponse(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
|
||||||
|
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
|
||||||
|
const message = isBlocked
|
||||||
|
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
|
||||||
|
: err.message;
|
||||||
|
jsonError(res, message, status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -699,7 +704,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- FEATURE: Streaming sources (detailed/manual endpoint) ----
|
// ---- FEATURE: Streaming sources (query-based endpoint) ----
|
||||||
router.get("/sources", async (req, res) => {
|
router.get("/sources", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { episodeId, provider, anilistId, category = "sub" } = req.query;
|
const { episodeId, provider, anilistId, category = "sub" } = req.query;
|
||||||
@@ -710,7 +715,27 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
const result = await pipe.getSources(episodeId, provider, parseInt(anilistId), category);
|
const result = await pipe.getSources(episodeId, provider, parseInt(anilistId), category);
|
||||||
jsonResponse(res, result);
|
jsonResponse(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
const status = err.message.includes("Cloudflare") || err.message.includes("blocked")
|
||||||
|
? 503 : err.message.includes("not found") ? 404 : 500;
|
||||||
|
jsonError(res, err.message, status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Streaming sources by episode ID (path-based endpoint) ----
|
||||||
|
router.get("/sources/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { provider, anilistId, category = "sub" } = req.query;
|
||||||
|
if (!provider || !anilistId) {
|
||||||
|
return jsonError(res, "provider and anilistId query params are required", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pipe.getSources(id, provider, parseInt(anilistId), category);
|
||||||
|
jsonResponse(res, result);
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message.includes("Cloudflare") || err.message.includes("blocked")
|
||||||
|
? 503 : err.message.includes("not found") ? 404 : 500;
|
||||||
|
jsonError(res, err.message, status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -735,7 +760,12 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
skipTimes,
|
skipTimes,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
|
||||||
|
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
|
||||||
|
const message = isBlocked
|
||||||
|
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
|
||||||
|
: err.message;
|
||||||
|
jsonError(res, message, status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -750,7 +780,12 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
const result = await pipe.getDownloadUrl(provider, parseInt(anilistId), category, slug);
|
const result = await pipe.getDownloadUrl(provider, parseInt(anilistId), category, slug);
|
||||||
jsonResponse(res, result);
|
jsonResponse(res, result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
|
||||||
|
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
|
||||||
|
const message = isBlocked
|
||||||
|
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
|
||||||
|
: err.message;
|
||||||
|
jsonError(res, message, status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -768,10 +803,94 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
skipTimes,
|
skipTimes,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
const isBlocked = err.message.includes("Cloudflare") || err.message.includes("blocked") || err.message.includes("403");
|
||||||
|
const status = isBlocked ? 503 : err.message.includes("not found") ? 404 : 500;
|
||||||
|
const message = isBlocked
|
||||||
|
? "Streaming provider is blocking requests (Cloudflare protection). Try again later or configure SCRAPER_API_KEY / FLARESOLVERR_URL."
|
||||||
|
: err.message;
|
||||||
|
jsonError(res, message, status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// DIRECT EMBED PROVIDERS (no scraping — bypasses Cloudflare)
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Direct embed URLs for all providers ----
|
||||||
|
router.get("/embed/:anilistId/:episode", (req, res) => {
|
||||||
|
try {
|
||||||
|
const anilistId = parseInt(req.params.anilistId);
|
||||||
|
const episode = parseInt(req.params.episode);
|
||||||
|
if (isNaN(anilistId) || anilistId <= 0) return jsonError(res, "Invalid AniList ID", 400);
|
||||||
|
if (isNaN(episode) || episode <= 0) return jsonError(res, "Invalid episode number", 400);
|
||||||
|
|
||||||
|
const { lang = "sub", malId } = req.query;
|
||||||
|
const validLangs = ["sub", "dub"];
|
||||||
|
if (!validLangs.includes(lang)) return jsonError(res, "lang must be 'sub' or 'dub'", 400);
|
||||||
|
|
||||||
|
const embeds = pipe.getEmbedUrls(anilistId, episode, lang, malId ? parseInt(malId) : null);
|
||||||
|
|
||||||
|
jsonResponse(res, {
|
||||||
|
anilistId,
|
||||||
|
episode,
|
||||||
|
lang,
|
||||||
|
providers: embeds,
|
||||||
|
total: embeds.length,
|
||||||
|
description: "Direct embed URLs — embed in an iframe to watch. No scraping required.",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonError(res, err.message, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: Direct embed URL for specific provider ----
|
||||||
|
router.get("/embed/:anilistId/:episode/:provider", (req, res) => {
|
||||||
|
try {
|
||||||
|
const anilistId = parseInt(req.params.anilistId);
|
||||||
|
const episode = parseInt(req.params.episode);
|
||||||
|
const providerId = req.params.provider;
|
||||||
|
if (isNaN(anilistId) || anilistId <= 0) return jsonError(res, "Invalid AniList ID", 400);
|
||||||
|
if (isNaN(episode) || episode <= 0) return jsonError(res, "Invalid episode number", 400);
|
||||||
|
|
||||||
|
const { lang = "sub", malId } = req.query;
|
||||||
|
const validLangs = ["sub", "dub"];
|
||||||
|
if (!validLangs.includes(lang)) return jsonError(res, "lang must be 'sub' or 'dub'", 400);
|
||||||
|
|
||||||
|
const url = pipe.getEmbedUrl(providerId, anilistId, episode, lang, malId ? parseInt(malId) : null);
|
||||||
|
if (!url) return jsonError(res, `Unknown provider: ${providerId}. Available: megavid, anixo`, 404);
|
||||||
|
|
||||||
|
const provider = pipe.EMBED_PROVIDERS.find((p) => p.id === providerId);
|
||||||
|
|
||||||
|
jsonResponse(res, {
|
||||||
|
anilistId,
|
||||||
|
episode,
|
||||||
|
lang,
|
||||||
|
provider: providerId,
|
||||||
|
name: provider.name,
|
||||||
|
url,
|
||||||
|
cors: provider.cors,
|
||||||
|
description: "Embed this URL in an iframe to watch. No scraping required.",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonError(res, err.message, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- FEATURE: List all available embed providers ----
|
||||||
|
router.get("/embed-providers", (req, res) => {
|
||||||
|
jsonResponse(res, {
|
||||||
|
providers: pipe.EMBED_PROVIDERS.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
cors: p.cors,
|
||||||
|
example: p.makeUrl(20, 1, "sub"),
|
||||||
|
})),
|
||||||
|
total: pipe.EMBED_PROVIDERS.length,
|
||||||
|
usage: "GET /api/embed/:anilistId/:episode?lang=sub",
|
||||||
|
description: "Direct embed providers — bypass Cloudflare by embedding video URLs directly in an iframe.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
// UTILITY FEATURES
|
// UTILITY FEATURES
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -1124,7 +1243,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
success: true,
|
success: true,
|
||||||
results: {
|
results: {
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
version: "2.3.0",
|
version: "2.3.3",
|
||||||
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
||||||
uptimeSeconds: uptime,
|
uptimeSeconds: uptime,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -1138,8 +1257,9 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
size: require("../helpers/cache").getCacheSize(),
|
size: require("../helpers/cache").getCacheSize(),
|
||||||
maxSize: 100,
|
maxSize: 100,
|
||||||
},
|
},
|
||||||
endpoints: 46,
|
endpoints: 49,
|
||||||
providers: ["kiwi", "pewe", "bee", "bonk", "bun", "ally", "nun", "twin", "cog", "moo", "hop", "telli"],
|
providers: ["kiwi", "pewe", "bee", "bonk", "bun", "ally", "nun", "twin", "cog", "moo", "hop", "telli"],
|
||||||
|
embedProviders: ["megavid", "anixo"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1154,6 +1274,9 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
const failed = Object.entries(results).filter(
|
const failed = Object.entries(results).filter(
|
||||||
([, v]) => v.status === "failed"
|
([, v]) => v.status === "failed"
|
||||||
);
|
);
|
||||||
|
const skipped = Object.entries(results).filter(
|
||||||
|
([, v]) => v.status === "skipped"
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1161,10 +1284,11 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
methods: results,
|
methods: results,
|
||||||
working: working.length,
|
working: working.length,
|
||||||
failed: failed.length,
|
failed: failed.length,
|
||||||
|
skipped: skipped.length,
|
||||||
total: Object.keys(results).length,
|
total: Object.keys(results).length,
|
||||||
recommendation:
|
recommendation:
|
||||||
working.length === 0
|
working.length === 0
|
||||||
? "All methods blocked. Check Cloudflare status or add SCRAPER_API_KEY / FLARESOLVERR_URL."
|
? "All methods blocked or unconfigured. Set SCRAPER_API_KEY or FLARESOLVERR_URL in .env to enable fallback methods. See .env.example for details."
|
||||||
: working[0]
|
: working[0]
|
||||||
? `Preferred: ${working[0][0]} (${working[0][1].latency})`
|
? `Preferred: ${working[0][0]} (${working[0][1].latency})`
|
||||||
: "No methods available",
|
: "No methods available",
|
||||||
@@ -1172,7 +1296,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
jsonError(res, err.message, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1198,7 +1322,8 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
currentSize: require("../helpers/cache").getCacheSize(),
|
currentSize: require("../helpers/cache").getCacheSize(),
|
||||||
description: "Map-based cache with TTL expiration and FIFO eviction",
|
description: "Map-based cache with TTL expiration and FIFO eviction",
|
||||||
},
|
},
|
||||||
endpoints: 46,
|
endpoints: 49,
|
||||||
|
embedProviders: 2,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1211,7 +1336,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
info: {
|
info: {
|
||||||
title: "MiruroAPI",
|
title: "MiruroAPI",
|
||||||
description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers",
|
description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers",
|
||||||
version: "2.3.0",
|
version: "2.3.3",
|
||||||
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
@@ -1250,7 +1375,8 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
"/anime/{id}/recommendations": { get: { summary: "Recommendations", tags: ["Anime Details"] } },
|
"/anime/{id}/recommendations": { get: { summary: "Recommendations", tags: ["Anime Details"] } },
|
||||||
"/episodes/{id}": { get: { summary: "Episode list", tags: ["Streaming & Downloading"] } },
|
"/episodes/{id}": { get: { summary: "Episode list", tags: ["Streaming & Downloading"] } },
|
||||||
"/episodes/{id}/batch": { get: { summary: "Batch episode sources", tags: ["Streaming & Downloading"] } },
|
"/episodes/{id}/batch": { get: { summary: "Batch episode sources", tags: ["Streaming & Downloading"] } },
|
||||||
"/sources": { get: { summary: "Streaming sources (detailed)", tags: ["Streaming & Downloading"] } },
|
"/sources": { get: { summary: "Streaming sources (query-based)", tags: ["Streaming & Downloading"] } },
|
||||||
|
"/sources/{id}": { get: { summary: "Streaming sources by episode ID", tags: ["Streaming & Downloading"] } },
|
||||||
"/stream": { get: { summary: "Stream with quality fallback", tags: ["Streaming & Downloading"] } },
|
"/stream": { get: { summary: "Stream with quality fallback", tags: ["Streaming & Downloading"] } },
|
||||||
"/download": { get: { summary: "Download URL", tags: ["Streaming & Downloading"] } },
|
"/download": { get: { summary: "Download URL", tags: ["Streaming & Downloading"] } },
|
||||||
"/watch/{provider}/{anilistId}/{category}/{slug}": { get: { summary: "Streaming sources (simple)", tags: ["Streaming & Downloading"] } },
|
"/watch/{provider}/{anilistId}/{category}/{slug}": { get: { summary: "Streaming sources (simple)", tags: ["Streaming & Downloading"] } },
|
||||||
@@ -1259,6 +1385,9 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
"/calendar": { get: { summary: "Monthly calendar", tags: ["Utility"] } },
|
"/calendar": { get: { summary: "Monthly calendar", tags: ["Utility"] } },
|
||||||
"/timeline/{id}": { get: { summary: "Anime timeline", tags: ["Utility"] } },
|
"/timeline/{id}": { get: { summary: "Anime timeline", tags: ["Utility"] } },
|
||||||
"/providers": { get: { summary: "Provider capabilities", tags: ["System"] } },
|
"/providers": { get: { summary: "Provider capabilities", tags: ["System"] } },
|
||||||
|
"/embed/{anilistId}/{episode}": { get: { summary: "Direct embed URLs (all providers)", tags: ["Streaming & Downloading"] } },
|
||||||
|
"/embed/{anilistId}/{episode}/{provider}": { get: { summary: "Direct embed URL (specific provider)", tags: ["Streaming & Downloading"] } },
|
||||||
|
"/embed-providers": { get: { summary: "List embed providers", tags: ["System"] } },
|
||||||
"/health": { get: { summary: "Health check", tags: ["System"] } },
|
"/health": { get: { summary: "Health check", tags: ["System"] } },
|
||||||
"/stats": { get: { summary: "API statistics", tags: ["System"] } },
|
"/stats": { get: { summary: "API statistics", tags: ["System"] } },
|
||||||
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
|
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ const tests = [
|
|||||||
// ---- FEATURE: Streaming endpoints ----
|
// ---- FEATURE: Streaming endpoints ----
|
||||||
{ name: "Episodes", url: "/episodes/20" },
|
{ name: "Episodes", url: "/episodes/20" },
|
||||||
{ name: "Batch Episodes", url: "/episodes/20/batch?provider=kiwi&category=sub&slugs=animepahe-1,animepahe-2" },
|
{ name: "Batch Episodes", url: "/episodes/20/batch?provider=kiwi&category=sub&slugs=animepahe-1,animepahe-2" },
|
||||||
|
{ name: "Sources (query)", url: "/sources?episodeId=test&provider=kiwi&anilistId=20" },
|
||||||
|
{ name: "Sources (path)", url: "/sources/test?provider=kiwi&anilistId=20" },
|
||||||
{ name: "Stream", url: "/stream?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" },
|
{ name: "Stream", url: "/stream?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" },
|
||||||
{ name: "Download", url: "/download?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" },
|
{ name: "Download", url: "/download?provider=kiwi&anilistId=20&category=sub&slug=animepahe-1" },
|
||||||
{ name: "Watch", url: "/watch/kiwi/20/sub/animepahe-1" },
|
{ name: "Watch", url: "/watch/kiwi/20/sub/animepahe-1" },
|
||||||
@@ -91,6 +93,12 @@ const tests = [
|
|||||||
{ name: "Genre Stats", url: "/stats/genre" },
|
{ name: "Genre Stats", url: "/stats/genre" },
|
||||||
{ name: "Calendar", url: "/calendar" },
|
{ name: "Calendar", url: "/calendar" },
|
||||||
{ name: "Timeline", url: "/timeline/20" },
|
{ name: "Timeline", url: "/timeline/20" },
|
||||||
|
|
||||||
|
// ---- FEATURE: Direct embed endpoints (no scraping) ----
|
||||||
|
{ name: "Embed All Providers", url: "/embed/20/1?lang=sub" },
|
||||||
|
{ name: "Embed Megavid", url: "/embed/20/1/megavid?lang=sub" },
|
||||||
|
{ name: "Embed AniXo", url: "/embed/20/1/anixo?lang=sub" },
|
||||||
|
{ name: "Embed Providers List", url: "/embed-providers" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -152,7 +160,7 @@ async function runTest(test) {
|
|||||||
* API_URL=http://localhost:3000/api node test.js # Run against local
|
* API_URL=http://localhost:3000/api node test.js # Run against local
|
||||||
*/
|
*/
|
||||||
async function runAll() {
|
async function runAll() {
|
||||||
console.log(`\n🧪 MiruroAPI v2.3.0 — Running ${tests.length} tests...\n`);
|
console.log(`\n🧪 MiruroAPI v2.3.3 — Running ${tests.length} tests...\n`);
|
||||||
|
|
||||||
// NOTE: Sequential execution — parallel requests would hit rate limiter
|
// NOTE: Sequential execution — parallel requests would hit rate limiter
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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"]
|
||||||
Reference in New Issue
Block a user