mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 02:05:10 +00:00
fix: resolve Issue #3 — API stability, error handling & new endpoints
- Added /api/sources/:id path-based endpoint for streaming sources
- Improved Cloudflare detection with proper 503 status codes
- Added timeout handling (15s) with exponential backoff for pipe requests
- Enhanced CORS with Authorization header and rate limit expose headers
- All streaming endpoints now return descriptive error messages
- Pipe health check reports skipped/unconfigured methods separately
- Updated OpenAPI spec with new /sources/{id} route
- Version bumped to 2.3.3
Fixed endpoints: /search, /info/:id, /trending, /filter, /episodes/:id,
/sources/:id (new), /stream, /watch, /download
All endpoints return consistent { success, results/message } format
Proper 400/404/503 responses for invalid inputs and failures
This commit is contained in:
@@ -1,5 +1,59 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.3.3
|
||||||
|
### Issue #3 Fix — API Stability, Error Handling & New Endpoints
|
||||||
|
|
||||||
|
#### 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`
|
||||||
|
|
||||||
|
#### Files Changed
|
||||||
|
- `src/routes/apiRoutes.js` — Added `/sources/:id` route, improved error handling for all streaming endpoints, 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] 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
|
||||||
|
- Streaming endpoints depend on miruro.to pipe availability — Cloudflare may still block from datacenters
|
||||||
|
- For reliable streaming, deploy on Cloudflare Workers (worker.js) or configure SCRAPER_API_KEY/FLARESOLVERR_URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.3.2
|
## v2.3.2
|
||||||
### Cloudflare Worker Implementation — Issue #2 Fix
|
### Cloudflare Worker Implementation — Issue #2 Fix
|
||||||
|
|
||||||
|
|||||||
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.2",
|
"version": "2.3.3",
|
||||||
"description": "RESTful API for Miruro - Anime streaming data powered by AniList GraphQL and Miruro streaming providers",
|
"description": "RESTful API for Miruro - Anime streaming data powered by AniList GraphQL and Miruro streaming providers",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -101,10 +101,12 @@ app.use((req, res, next) => {
|
|||||||
return res.status(403).json({ success: false, message: "Origin not allowed" });
|
return res.status(403).json({ success: false, message: "Origin not allowed" });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Default: allow all origins for public API usage by anime frontends
|
||||||
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
||||||
}
|
}
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression");
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression, Authorization");
|
||||||
|
res.setHeader("Access-Control-Expose-Headers", "X-RateLimit-Limit, X-RateLimit-Remaining");
|
||||||
res.setHeader("Access-Control-Max-Age", "86400");
|
res.setHeader("Access-Control-Max-Age", "86400");
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
@@ -301,7 +303,7 @@ app.use((req, res) => {
|
|||||||
|
|
||||||
if (!process.env.VERCEL) {
|
if (!process.env.VERCEL) {
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.2 listening at ${PORT}`);
|
console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.3 listening at ${PORT}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-5
@@ -113,7 +113,7 @@ const methodDirect = async (encodedReq) => {
|
|||||||
try {
|
try {
|
||||||
const res = await axios.get(`${origin}${PIPE_PATH}?e=${encodedReq}`, {
|
const res = await axios.get(`${origin}${PIPE_PATH}?e=${encodedReq}`, {
|
||||||
headers: HEADERS,
|
headers: HEADERS,
|
||||||
timeout: 20000,
|
timeout: 15000,
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,6 +126,10 @@ const methodDirect = async (encodedReq) => {
|
|||||||
lastError = e;
|
lastError = e;
|
||||||
const status = e.response?.status;
|
const status = e.response?.status;
|
||||||
|
|
||||||
|
if (status === 403 || status === 444) {
|
||||||
|
throw new Error(`Cloudflare is blocking requests to ${origin} (HTTP ${status}). Configure SCRAPER_API_KEY or FLARESOLVERR_URL for fallback.`);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
status &&
|
status &&
|
||||||
status >= 400 &&
|
status >= 400 &&
|
||||||
@@ -136,13 +140,17 @@ const methodDirect = async (encodedReq) => {
|
|||||||
throw new Error(`Pipe request failed with status ${status}`);
|
throw new Error(`Pipe request failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.code === "ECONNABORTED" || e.code === "ETIMEDOUT") {
|
||||||
|
lastError = new Error(`Request to ${origin} timed out after 15s`);
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt < maxRetries - 1) {
|
if (attempt < maxRetries - 1) {
|
||||||
const delay = Math.pow(2, attempt) * 1000;
|
const delay = Math.pow(2, attempt) * 1000;
|
||||||
await new Promise((r) => setTimeout(r, delay));
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw lastError || new Error("Direct request failed after all retries");
|
throw lastError || new Error("Direct request failed after all retries. The streaming provider may be temporarily unavailable.");
|
||||||
};
|
};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -220,10 +228,13 @@ const pipeRequest = async (path, query) => {
|
|||||||
const encodedReq = encodePipeRequest(payload);
|
const encodedReq = encodePipeRequest(payload);
|
||||||
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
const availableMethods = METHODS.filter((m) => !m.requires || m.requires());
|
||||||
|
|
||||||
for (const method of METHODS) {
|
if (availableMethods.length === 0) {
|
||||||
if (method.requires && !method.requires()) continue;
|
throw new Error("No streaming methods configured. Set SCRAPER_API_KEY or FLARESOLVERR_URL in your .env file to enable fallback methods.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const method of availableMethods) {
|
||||||
try {
|
try {
|
||||||
const result = await method.fn(encodedReq);
|
const result = await method.fn(encodedReq);
|
||||||
return result.data;
|
return result.data;
|
||||||
@@ -232,8 +243,14 @@ const pipeRequest = async (path, query) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCloudflare = errors.some((e) => e.error.includes("Cloudflare") || e.error.includes("403"));
|
||||||
|
const baseMessage = isCloudflare
|
||||||
|
? "Cloudflare is blocking all streaming requests"
|
||||||
|
: "All streaming methods failed";
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`All pipe methods failed: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}`
|
`${baseMessage}: ${errors.map((e) => `${e.method}(${e.error})`).join(", ")}. ` +
|
||||||
|
"Configure SCRAPER_API_KEY or FLARESOLVERR_URL for additional fallback methods."
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+56
-11
@@ -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,7 +803,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1124,7 +1164,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
success: true,
|
success: true,
|
||||||
results: {
|
results: {
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
version: "2.3.2",
|
version: "2.3.3",
|
||||||
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
||||||
uptimeSeconds: uptime,
|
uptimeSeconds: uptime,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -1154,6 +1194,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 +1204,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 +1216,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
jsonError(res, err.message, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1211,7 +1255,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
info: {
|
info: {
|
||||||
title: "MiruroAPI",
|
title: "MiruroAPI",
|
||||||
description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers",
|
description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers",
|
||||||
version: "2.3.2",
|
version: "2.3.3",
|
||||||
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
@@ -1250,7 +1294,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"] } },
|
||||||
|
|||||||
@@ -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" },
|
||||||
@@ -152,7 +154,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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user