mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 02:05:10 +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
|
||||
|
||||
## 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
|
||||
### Streaming Architecture — pru proxy + raw CDN URLs
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
+67
-33
@@ -20,6 +20,7 @@
|
||||
const axios = require("axios");
|
||||
const { Buffer } = require("buffer");
|
||||
const zlib = require("zlib");
|
||||
const { getCached, setCache } = require("./cache");
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// 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 ----
|
||||
/**
|
||||
* 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 ----
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Decoded episode data with providers
|
||||
* @throws {Error} If the pipe request fails
|
||||
*/
|
||||
const fetchRawEpisodes = async (anilistId) => {
|
||||
const payload = {
|
||||
path: "episodes",
|
||||
method: "GET",
|
||||
query: { anilistId },
|
||||
body: null,
|
||||
version: "0.2.0",
|
||||
};
|
||||
const cacheKey = `pipe:episodes:${anilistId}`;
|
||||
const cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const encodedReq = encodePipeRequest(payload);
|
||||
const res = await axios.get(`${MIRURO_PIPE_URL}?e=${encodedReq}`, {
|
||||
headers: HEADERS,
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
if (res.status !== 200) throw new Error(`Pipe request failed: ${res.status}`);
|
||||
const data = decodePipeResponse(res.text || res.data);
|
||||
return deepTranslate(data);
|
||||
const data = await pipeRequest("episodes", { anilistId });
|
||||
const result = deepTranslate(data);
|
||||
setCache(cacheKey, result, 300 * 1000);
|
||||
return result;
|
||||
};
|
||||
|
||||
// ---- FEATURE: Get episodes with slug-based IDs ----
|
||||
@@ -316,7 +352,8 @@ const getEpisodes = async (anilistId) => {
|
||||
// ---- FEATURE: Get streaming sources (detailed endpoint) ----
|
||||
/**
|
||||
* 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} 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"
|
||||
*/
|
||||
const getSources = async (episodeId, provider, anilistId, category = "sub") => {
|
||||
const encId = Buffer.from(episodeId).toString("base64url");
|
||||
const payload = {
|
||||
path: "sources",
|
||||
method: "GET",
|
||||
query: { episodeId: encId, provider, category, anilistId },
|
||||
body: null,
|
||||
version: "0.2.0",
|
||||
};
|
||||
const cacheKey = `pipe:sources:${episodeId}:${provider}:${category}`;
|
||||
const cached = getCached(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const encodedReq = encodePipeRequest(payload);
|
||||
const res = await axios.get(`${MIRURO_PIPE_URL}?e=${encodedReq}`, {
|
||||
headers: HEADERS,
|
||||
timeout: 15000,
|
||||
const encId = Buffer.from(episodeId).toString("base64url");
|
||||
const sources = await pipeRequest("sources", {
|
||||
episodeId: encId,
|
||||
provider,
|
||||
category,
|
||||
anilistId,
|
||||
});
|
||||
|
||||
if (res.status !== 200) throw new Error(`Pipe sources request failed: ${res.status}`);
|
||||
const sources = deepTranslate(decodePipeResponse(res.text || res.data));
|
||||
return proxySubtitles(proxyStreams(sources));
|
||||
const result = proxySubtitles(proxyStreams(deepTranslate(sources)));
|
||||
setCache(cacheKey, result, 600 * 1000);
|
||||
return result;
|
||||
};
|
||||
|
||||
// ---- FEATURE: Get streaming sources (simple slug-based endpoint) ----
|
||||
|
||||
@@ -1102,7 +1102,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
||||
success: true,
|
||||
results: {
|
||||
status: "healthy",
|
||||
version: "2.1.0",
|
||||
version: "2.1.3",
|
||||
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
||||
uptimeSeconds: uptime,
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -1157,7 +1157,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
||||
info: {
|
||||
title: "MiruroAPI",
|
||||
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" },
|
||||
},
|
||||
servers: [
|
||||
|
||||
Reference in New Issue
Block a user