mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 02:05:10 +00:00
feat: add direct embed providers (megavid, anixo) to bypass Cloudflare
- Added GET /api/embed/:anilistId/:episode — embed URLs for all providers - Added GET /api/embed/:anilistId/:episode/:provider — specific provider embed - Added GET /api/embed-providers — list all available embed providers - Embed providers return iframe-ready URLs, no scraping required - Supports sub/dub language selection and optional malId param - Updated OpenAPI spec with new embed endpoints - Updated health/stats to show 49 endpoints and embed providers - Added embed provider tests to test.js Cloudflare-protected pipe endpoints now have working alternatives: use /api/embed to get direct video URLs from megavid.buzz and anixo.buzz
This commit is contained in:
+17
-4
@@ -1,7 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v2.3.3
|
## v2.3.3
|
||||||
### Issue #3 Fix — API Stability, Error Handling & New Endpoints
|
### 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
|
#### 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
|
- **Cloudflare detection** — Streaming endpoints (`/episodes`, `/stream`, `/watch`, `/download`, `/sources`) now return proper `503` status codes with descriptive error messages when Cloudflare blocks requests
|
||||||
@@ -25,9 +34,11 @@
|
|||||||
|
|
||||||
#### Changed: Version Bump
|
#### Changed: Version Bump
|
||||||
- All version references updated from `2.3.2` to `2.3.3`
|
- 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
|
#### Files Changed
|
||||||
- `src/routes/apiRoutes.js` — Added `/sources/:id` route, improved error handling for all streaming endpoints, updated OpenAPI spec
|
- `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
|
- `src/helpers/pipe.js` — Better timeout handling, Cloudflare detection, informative error messages
|
||||||
- `server.js` — Updated CORS headers, version bump
|
- `server.js` — Updated CORS headers, version bump
|
||||||
- `package.json` — Version bump to 2.3.3
|
- `package.json` — Version bump to 2.3.3
|
||||||
@@ -42,6 +53,7 @@
|
|||||||
- [x] Fix `/api/episodes/:id` — Improved error handling, proper 503 on Cloudflare blocks
|
- [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/sources` — Added `/api/sources/:id` path-based endpoint
|
||||||
- [x] Fix `/api/watch` — Improved error handling with Cloudflare detection
|
- [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] Consistent JSON responses — All endpoints return `{ success, results/message }`
|
||||||
- [x] Proper 400/404 responses — Input validation for invalid IDs and missing params
|
- [x] Proper 400/404 responses — Input validation for invalid IDs and missing params
|
||||||
- [x] Error handling — Timeouts, Cloudflare blocks, upstream failures handled gracefully
|
- [x] Error handling — Timeouts, Cloudflare blocks, upstream failures handled gracefully
|
||||||
@@ -49,8 +61,9 @@
|
|||||||
|
|
||||||
#### Notes
|
#### Notes
|
||||||
- AniList-based endpoints (search, info, trending, filter, etc.) are fully functional
|
- AniList-based endpoints (search, info, trending, filter, etc.) are fully functional
|
||||||
- Streaming endpoints depend on miruro.to pipe availability — Cloudflare may still block from datacenters
|
- **Direct embed providers (`/api/embed`) work from anywhere** — no Cloudflare blocking
|
||||||
- For reliable streaming, deploy on Cloudflare Workers (worker.js) or configure SCRAPER_API_KEY/FLARESOLVERR_URL
|
- 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -592,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,
|
||||||
@@ -607,6 +647,9 @@ module.exports = {
|
|||||||
deepTranslate,
|
deepTranslate,
|
||||||
injectSourceSlugs,
|
injectSourceSlugs,
|
||||||
pipeHealthCheck,
|
pipeHealthCheck,
|
||||||
|
EMBED_PROVIDERS,
|
||||||
|
getEmbedUrl,
|
||||||
|
getEmbedUrls,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════ END: pipe.js
|
// ══════════════════════════════════════════════════════════════ END: pipe.js
|
||||||
|
|||||||
+86
-2
@@ -812,6 +812,85 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// 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
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -1178,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"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1242,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(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1304,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"] } },
|
||||||
|
|||||||
@@ -93,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" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user