mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 18:25:11 +00:00
v2.1.3: Pipe resilience — retry logic, caching, optimized streaming
- Added pipeRequest() with retry + exponential backoff (3 attempts) - Cached episodes (5min) and sources (10min) responses - Confirmed kiwi/bonk CDNs serve real content, bee/ally serve decoys - Updated version to 2.1.3 across package.json, health, OpenAPI
This commit is contained in:
@@ -1,5 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.1.3
|
||||||
|
### Pipe Resilience — Retry logic, caching, optimized streaming
|
||||||
|
|
||||||
|
#### Pipe Request Improvements
|
||||||
|
- Added `pipeRequest()` helper with retry logic and exponential backoff (3 attempts: 1s, 2s, 4s delays)
|
||||||
|
- Retries on 444 (Cloudflare rate limit) and connection reset errors
|
||||||
|
- Skips retry on non-retryable 4xx errors (except 444)
|
||||||
|
- Increased pipe request timeout from 15s to 20s
|
||||||
|
|
||||||
|
#### Caching
|
||||||
|
- `fetchRawEpisodes` now caches episode data for 5 minutes (was uncached)
|
||||||
|
- `getSources` now caches streaming sources for 10 minutes (was uncached)
|
||||||
|
- Subsequent calls for the same episode are instant (no pipe requests)
|
||||||
|
|
||||||
|
#### Streaming Pipeline
|
||||||
|
- Confirmed kiwi provider CDN (owocdn.top) serves real video content with CORS `*`
|
||||||
|
- Confirmed bonk provider CDN (vibeplayer.site) returns streams
|
||||||
|
- bee/ally CDNs serve decoy PNGs to server IPs (confirmed)
|
||||||
|
- Stream URLs returned as raw CDN URLs (browser loads directly)
|
||||||
|
- Subtitles proxied through `/api/proxy` for CORS bypass
|
||||||
|
|
||||||
|
#### Version Bump
|
||||||
|
- MiruroAPI v2.1.3 (package.json, health endpoint, OpenAPI spec)
|
||||||
|
|
||||||
## v2.1.2
|
## v2.1.2
|
||||||
### Streaming Architecture — pru proxy + raw CDN URLs
|
### Streaming Architecture — pru proxy + raw CDN URLs
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "miruro-api",
|
"name": "miruro-api",
|
||||||
"version": "2.1.0",
|
"version": "2.1.3",
|
||||||
"description": "RESTful API for miruro.tv - Anime streaming data powered by AniList GraphQL and Miruro streaming providers",
|
"description": "RESTful API for miruro.tv - Anime streaming data powered by AniList GraphQL and Miruro streaming providers",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+67
-33
@@ -20,6 +20,7 @@
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Buffer } = require("buffer");
|
const { Buffer } = require("buffer");
|
||||||
const zlib = require("zlib");
|
const zlib = require("zlib");
|
||||||
|
const { getCached, setCache } = require("./cache");
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
// MIRURO PIPE CONFIGURATION
|
// MIRURO PIPE CONFIGURATION
|
||||||
@@ -95,6 +96,50 @@ const decodePipeResponse = (encodedStr) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- FEATURE: Pipe request with retry and exponential backoff ----
|
||||||
|
/**
|
||||||
|
* Makes a pipe API request with retry logic and exponential backoff.
|
||||||
|
* The pipe endpoint intermittently rate-limits (444/connection reset) from datacenter IPs.
|
||||||
|
* Retries up to 3 times with increasing delays (1s, 2s, 4s).
|
||||||
|
*
|
||||||
|
* @param {string} path - Pipe path ("episodes" or "sources")
|
||||||
|
* @param {object} query - Query parameters for the pipe
|
||||||
|
* @param {number} [maxRetries=3] - Maximum number of retries
|
||||||
|
* @returns {Promise<object>} Decoded pipe response
|
||||||
|
* @throws {Error} If all retries fail
|
||||||
|
*/
|
||||||
|
const pipeRequest = async (path, query, maxRetries = 3) => {
|
||||||
|
const payload = { path, method: "GET", query, body: null, version: "0.2.0" };
|
||||||
|
const encodedReq = encodePipeRequest(payload);
|
||||||
|
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`${MIRURO_PIPE_URL}?e=${encodedReq}`, {
|
||||||
|
headers: HEADERS,
|
||||||
|
timeout: 20000,
|
||||||
|
maxRedirects: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) throw new Error(`Pipe request failed: ${res.status}`);
|
||||||
|
return decodePipeResponse(res.text || res.data);
|
||||||
|
} catch (e) {
|
||||||
|
lastError = e;
|
||||||
|
const status = e.response?.status;
|
||||||
|
// NOTE: Don't retry on non-retryable errors (4xx except 444)
|
||||||
|
if (status && status >= 400 && status < 500 && status !== 444) {
|
||||||
|
throw new Error(`Pipe request failed with status ${status}`);
|
||||||
|
}
|
||||||
|
// NOTE: Exponential backoff: 1s, 2s, 4s
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
const delay = Math.pow(2, attempt) * 1000;
|
||||||
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError || new Error("Pipe request failed after all retries");
|
||||||
|
};
|
||||||
|
|
||||||
// ---- FEATURE: Subtitle proxy for CORS-safe browser access ----
|
// ---- FEATURE: Subtitle proxy for CORS-safe browser access ----
|
||||||
/**
|
/**
|
||||||
* CDN streams (mt.nekostream.site) have CORS * — browsers load them directly.
|
* CDN streams (mt.nekostream.site) have CORS * — browsers load them directly.
|
||||||
@@ -261,30 +306,21 @@ const injectSourceSlugs = (data, anilistId) => {
|
|||||||
// ---- FEATURE: Fetch raw decoded episode data from Miruro pipe ----
|
// ---- FEATURE: Fetch raw decoded episode data from Miruro pipe ----
|
||||||
/**
|
/**
|
||||||
* Internal helper to fetch and decode raw episode data from the pipe.
|
* Internal helper to fetch and decode raw episode data from the pipe.
|
||||||
* Sends a base64-encoded request and decodes the gzipped response.
|
* Uses pipeRequest with retry logic and caches results for 5 minutes.
|
||||||
*
|
*
|
||||||
* @param {number} anilistId - The AniList anime ID
|
* @param {number} anilistId - The AniList anime ID
|
||||||
* @returns {Promise<object>} Decoded episode data with providers
|
* @returns {Promise<object>} Decoded episode data with providers
|
||||||
* @throws {Error} If the pipe request fails
|
* @throws {Error} If the pipe request fails
|
||||||
*/
|
*/
|
||||||
const fetchRawEpisodes = async (anilistId) => {
|
const fetchRawEpisodes = async (anilistId) => {
|
||||||
const payload = {
|
const cacheKey = `pipe:episodes:${anilistId}`;
|
||||||
path: "episodes",
|
const cached = getCached(cacheKey);
|
||||||
method: "GET",
|
if (cached) return cached;
|
||||||
query: { anilistId },
|
|
||||||
body: null,
|
|
||||||
version: "0.2.0",
|
|
||||||
};
|
|
||||||
|
|
||||||
const encodedReq = encodePipeRequest(payload);
|
const data = await pipeRequest("episodes", { anilistId });
|
||||||
const res = await axios.get(`${MIRURO_PIPE_URL}?e=${encodedReq}`, {
|
const result = deepTranslate(data);
|
||||||
headers: HEADERS,
|
setCache(cacheKey, result, 300 * 1000);
|
||||||
timeout: 15000,
|
return result;
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 200) throw new Error(`Pipe request failed: ${res.status}`);
|
|
||||||
const data = decodePipeResponse(res.text || res.data);
|
|
||||||
return deepTranslate(data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- FEATURE: Get episodes with slug-based IDs ----
|
// ---- FEATURE: Get episodes with slug-based IDs ----
|
||||||
@@ -316,7 +352,8 @@ const getEpisodes = async (anilistId) => {
|
|||||||
// ---- FEATURE: Get streaming sources (detailed endpoint) ----
|
// ---- FEATURE: Get streaming sources (detailed endpoint) ----
|
||||||
/**
|
/**
|
||||||
* Fetches M3U8 streaming sources for a specific episode.
|
* Fetches M3U8 streaming sources for a specific episode.
|
||||||
* This is the detailed/manual endpoint — use /watch for simpler access.
|
* Uses pipeRequest with retry logic and caches results for 10 minutes.
|
||||||
|
* Stream URLs are ephemeral but CDNs serve the same content for hours.
|
||||||
*
|
*
|
||||||
* @param {string} episodeId - The raw episode ID from the pipe
|
* @param {string} episodeId - The raw episode ID from the pipe
|
||||||
* @param {string} provider - Provider name (e.g., "kiwi", "bee", "bonk")
|
* @param {string} provider - Provider name (e.g., "kiwi", "bee", "bonk")
|
||||||
@@ -329,24 +366,21 @@ const getEpisodes = async (anilistId) => {
|
|||||||
* console.log(sources.streams[0].url); // "https://.../master.m3u8"
|
* console.log(sources.streams[0].url); // "https://.../master.m3u8"
|
||||||
*/
|
*/
|
||||||
const getSources = async (episodeId, provider, anilistId, category = "sub") => {
|
const getSources = async (episodeId, provider, anilistId, category = "sub") => {
|
||||||
const encId = Buffer.from(episodeId).toString("base64url");
|
const cacheKey = `pipe:sources:${episodeId}:${provider}:${category}`;
|
||||||
const payload = {
|
const cached = getCached(cacheKey);
|
||||||
path: "sources",
|
if (cached) return cached;
|
||||||
method: "GET",
|
|
||||||
query: { episodeId: encId, provider, category, anilistId },
|
|
||||||
body: null,
|
|
||||||
version: "0.2.0",
|
|
||||||
};
|
|
||||||
|
|
||||||
const encodedReq = encodePipeRequest(payload);
|
const encId = Buffer.from(episodeId).toString("base64url");
|
||||||
const res = await axios.get(`${MIRURO_PIPE_URL}?e=${encodedReq}`, {
|
const sources = await pipeRequest("sources", {
|
||||||
headers: HEADERS,
|
episodeId: encId,
|
||||||
timeout: 15000,
|
provider,
|
||||||
|
category,
|
||||||
|
anilistId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) throw new Error(`Pipe sources request failed: ${res.status}`);
|
const result = proxySubtitles(proxyStreams(deepTranslate(sources)));
|
||||||
const sources = deepTranslate(decodePipeResponse(res.text || res.data));
|
setCache(cacheKey, result, 600 * 1000);
|
||||||
return proxySubtitles(proxyStreams(sources));
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- FEATURE: Get streaming sources (simple slug-based endpoint) ----
|
// ---- FEATURE: Get streaming sources (simple slug-based endpoint) ----
|
||||||
|
|||||||
@@ -1102,7 +1102,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
success: true,
|
success: true,
|
||||||
results: {
|
results: {
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
version: "2.1.0",
|
version: "2.1.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(),
|
||||||
@@ -1157,7 +1157,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.1.0",
|
version: "2.1.3",
|
||||||
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
|
|||||||
Reference in New Issue
Block a user