mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 10:15:10 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec1ff69f4 | ||
|
|
56e3500d3e | ||
|
|
9b1ac11ff0 |
@@ -1,5 +1,63 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.1.0
|
||||||
|
### Major Feature Update — Provider capabilities, enriched metadata, skip times
|
||||||
|
|
||||||
|
#### New Endpoints
|
||||||
|
- GET /api/providers — Provider capabilities and configuration (12 providers with full metadata)
|
||||||
|
|
||||||
|
#### Anime Info Enrichment
|
||||||
|
- Added `alternateName` — multiple language titles from synonyms
|
||||||
|
- Added `aggregateRating` — rating with count, mean score
|
||||||
|
- Added `sameAs` — external links (AniList, MAL, official sites)
|
||||||
|
- Added `externalLinks` — structured external links with site/type
|
||||||
|
- Added `productionCompany` — non-animation studios
|
||||||
|
- Added `animationStudio` — animation studios
|
||||||
|
- Added `trailer` — anime trailer info
|
||||||
|
- Added `nextAiringEpisode` — next episode air time
|
||||||
|
- Added `tags` — simplified tag names array
|
||||||
|
|
||||||
|
#### Streaming Improvements
|
||||||
|
- Added `extractSkipTimes` — extract OP/ED skip timestamps from sources
|
||||||
|
- Added `skipTimes` field to /stream and /watch responses
|
||||||
|
- Episode thumbnails already included in pipe response
|
||||||
|
|
||||||
|
#### Provider Capabilities (from miruro.tv __SSR_CONFIG__)
|
||||||
|
- kiwi: sub, download
|
||||||
|
- pewe: sub
|
||||||
|
- bonk: sub, ssub, download, skip_times
|
||||||
|
- bee: ssub
|
||||||
|
- ally: sub, download, thumbnails
|
||||||
|
- moo: sub, download
|
||||||
|
- hop: ssub, thumbnails
|
||||||
|
- nun: sub (embed of ally)
|
||||||
|
- bun: ssub (embed of bee)
|
||||||
|
- twin: sub, ssub (embed of bonk)
|
||||||
|
- cog: sub (embed of moo)
|
||||||
|
- telli: sub (embed of kiwi, hidden)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.0.1
|
||||||
|
### Streaming Improvements — Better subtitle extraction, quality fallback
|
||||||
|
|
||||||
|
#### Streaming Enhancements
|
||||||
|
- Improved `extractSubtitles` — now handles multiple provider formats (bonk: file/label/kind/language, others: url/name/lang)
|
||||||
|
- Updated `getBestStream` — falls back to first active HLS stream when quality metadata is missing
|
||||||
|
- Updated `getDownloadUrl` — now returns subtitles and bestStream in response
|
||||||
|
- Added subtitle fields: url, label, language, kind, format, encoding, isDefault
|
||||||
|
|
||||||
|
#### Provider Improvements
|
||||||
|
- Bonk provider confirmed to return subtitles (English, VTT format, captions kind)
|
||||||
|
- Better handling of providers without quality metadata (bonk, ally, bee return undefined quality)
|
||||||
|
|
||||||
|
#### Verified
|
||||||
|
- Subtitle extraction working: bonk provider returns 1 English subtitle per episode
|
||||||
|
- Download URL working: bonk provider returns direct download link
|
||||||
|
- Batch sources: subtitles included in batch responses
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.0.0
|
## v2.0.0
|
||||||
### Major Release — 35 endpoints, streaming improvements, new features
|
### Major Release — 35 endpoints, streaming improvements, new features
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
<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.0.0-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
<img src="https://img.shields.io/badge/Version-2.1.0-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
||||||
<img src="https://img.shields.io/badge/Endpoints-35-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
<img src="https://img.shields.io/badge/Endpoints-36-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-12-a855f7?style=flat-square&logoColor=white" alt="Providers"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -1408,6 +1408,7 @@ docker run -p 3000:3000 miruroapi
|
|||||||
|
|
||||||
| Version | Date | Key Changes |
|
| Version | Date | Key Changes |
|
||||||
|:---|:---|:---|
|
|:---|:---|:---|
|
||||||
|
| **2.0.1** | 2026-06-24 | Streaming improvements — better subtitle extraction, quality fallback for providers without metadata |
|
||||||
| **2.0.0** | 2026-06-25 | Major upgrade — 35 endpoints, compression, streaming improvements, new features |
|
| **2.0.0** | 2026-06-25 | Major upgrade — 35 endpoints, compression, streaming improvements, new features |
|
||||||
| **1.4.0** | 2026-06-25 | Creator attribution middleware, Vercel deployment fix, security headers |
|
| **1.4.0** | 2026-06-25 | Creator attribution middleware, Vercel deployment fix, security headers |
|
||||||
| **1.3.0** | 2026-06-25 | Full landing page upgrade with premium design |
|
| **1.3.0** | 2026-06-25 | Full landing page upgrade with premium design |
|
||||||
|
|||||||
+27
-1
@@ -1,6 +1,6 @@
|
|||||||
# API Endpoints Reference
|
# API Endpoints Reference
|
||||||
|
|
||||||
Complete documentation for all 35 MiruroAPI v2.0.0 endpoints.
|
Complete documentation for all 35 MiruroAPI v2.0.1 endpoints.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -498,6 +498,32 @@ Streaming sources via URL path (recommended).
|
|||||||
| category | string | sub or dub |
|
| category | string | sub or dub |
|
||||||
| slug | string | Episode slug |
|
| slug | string | Episode slug |
|
||||||
|
|
||||||
|
### Response Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| streams | array | Array of stream objects (hls/embed) |
|
||||||
|
| download | string | Direct download URL (if available) |
|
||||||
|
| subtitles | array | Subtitle objects (if available) |
|
||||||
|
| bestStream | object | Best available HLS stream with quality fallback |
|
||||||
|
|
||||||
|
### Subtitle Object
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| url | string | VTT subtitle file URL |
|
||||||
|
| label | string | Display name (e.g., "English") |
|
||||||
|
| language | string | Language code (e.g., "en") |
|
||||||
|
| kind | string | "subtitles" or "captions" |
|
||||||
|
| format | string | "vtt" format |
|
||||||
|
| encoding | string | "utf-8" |
|
||||||
|
| isDefault | boolean | Is default subtitle |
|
||||||
|
|
||||||
|
### Note
|
||||||
|
- Bonk provider returns subtitles (English, VTT format)
|
||||||
|
- Other providers may not return subtitles
|
||||||
|
- Quality fallback: 1080p → 720p → 480p → 360p
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Compare
|
## Compare
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "miruro-api",
|
"name": "miruro-api",
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"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": {
|
||||||
|
|||||||
+42
-3
@@ -3,7 +3,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "MiruroAPI",
|
"title": "MiruroAPI",
|
||||||
"description": "Free RESTful API for anime streaming data. Powered by AniList GraphQL and Miruro streaming providers.",
|
"description": "Free RESTful API for anime streaming data. Powered by AniList GraphQL and Miruro streaming providers.",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "Shineii86",
|
"name": "Shineii86",
|
||||||
"url": "https://github.com/Shineii86/MiruroAPI"
|
"url": "https://github.com/Shineii86/MiruroAPI"
|
||||||
@@ -339,13 +339,52 @@
|
|||||||
"/api/watch/{provider}/{anilistId}/{category}/{slug}": {
|
"/api/watch/{provider}/{anilistId}/{category}/{slug}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["Streaming"],
|
"tags": ["Streaming"],
|
||||||
"summary": "Streaming sources (simple)",
|
"summary": "Streaming sources (simple) with subtitles",
|
||||||
|
"description": "Returns streaming sources, subtitles (if available), and best stream with quality fallback",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{ "name": "provider", "in": "path", "required": true, "schema": { "type": "string" } },
|
{ "name": "provider", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||||
{ "name": "anilistId", "in": "path", "required": true, "schema": { "type": "integer" } },
|
{ "name": "anilistId", "in": "path", "required": true, "schema": { "type": "integer" } },
|
||||||
{ "name": "category", "in": "path", "required": true, "schema": { "type": "string" } },
|
{ "name": "category", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||||
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
|
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||||
]
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": { "type": "boolean" },
|
||||||
|
"results": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"streams": { "type": "array" },
|
||||||
|
"download": { "type": "string" },
|
||||||
|
"subtitles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": { "type": "string", "description": "VTT subtitle file URL" },
|
||||||
|
"label": { "type": "string", "description": "Display name" },
|
||||||
|
"language": { "type": "string", "description": "Language code" },
|
||||||
|
"kind": { "type": "string", "description": "subtitles or captions" },
|
||||||
|
"format": { "type": "string", "description": "vtt format" },
|
||||||
|
"encoding": { "type": "string", "description": "utf-8" },
|
||||||
|
"isDefault": { "type": "boolean", "description": "Is default subtitle" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bestStream": { "type": "object" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/compare/{id1}/{id2}": {
|
"/api/compare/{id1}/{id2}": {
|
||||||
|
|||||||
+52
-12
@@ -327,19 +327,48 @@ const getWatchSources = async (provider, anilistId, category, slug) => {
|
|||||||
// ---- FEATURE: Extract subtitle URLs from streaming sources ----
|
// ---- FEATURE: Extract subtitle URLs from streaming sources ----
|
||||||
/**
|
/**
|
||||||
* Extracts and formats subtitle URLs from the pipe sources response.
|
* Extracts and formats subtitle URLs from the pipe sources response.
|
||||||
* Subtitles are usually in WebVTT format with language tags.
|
* Handles multiple subtitle formats from different providers.
|
||||||
|
*
|
||||||
|
* Provider formats:
|
||||||
|
* bonk: { file, label, kind, default, language, format, encoding }
|
||||||
|
* others: { url, name, lang, format }
|
||||||
*
|
*
|
||||||
* @param {object} sources - The sources response from getSources/getWatchSources
|
* @param {object} sources - The sources response from getSources/getWatchSources
|
||||||
* @returns {Array} Array of subtitle objects { url, label, language, format }
|
* @returns {Array} Array of subtitle objects { url, label, language, kind, format, isDefault }
|
||||||
*/
|
*/
|
||||||
const extractSubtitles = (sources) => {
|
const extractSubtitles = (sources) => {
|
||||||
const subtitles = sources.subtitles || sources.captions || [];
|
const raw = sources.subtitles || sources.captions || [];
|
||||||
return subtitles.map((sub) => ({
|
|
||||||
url: sub.url || sub.file,
|
if (!Array.isArray(raw)) return [];
|
||||||
label: sub.label || sub.name || "Unknown",
|
|
||||||
language: sub.lang || sub.language || sub.label || "en",
|
return raw.map((sub) => ({
|
||||||
|
url: sub.file || sub.url || null,
|
||||||
|
label: sub.label || sub.name || sub.language || "Unknown",
|
||||||
|
language: sub.language || sub.lang || sub.label || "en",
|
||||||
|
kind: sub.kind || "subtitles",
|
||||||
format: sub.format || "vtt",
|
format: sub.format || "vtt",
|
||||||
}));
|
encoding: sub.encoding || "utf-8",
|
||||||
|
isDefault: sub.default || false,
|
||||||
|
})).filter((sub) => sub.url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- FEATURE: Extract skip timestamps (OP/ED) from sources ----
|
||||||
|
/**
|
||||||
|
* Extracts skip timestamps (OP/ED) from the pipe sources response.
|
||||||
|
* Only available for providers with skip_times capability (e.g., bonk).
|
||||||
|
*
|
||||||
|
* @param {object} sources - The sources response from getSources/getWatchSources
|
||||||
|
* @returns {object|null} Skip timestamps object or null
|
||||||
|
*/
|
||||||
|
const extractSkipTimes = (sources) => {
|
||||||
|
const skipTimes = sources.skipTimes || sources.skip || null;
|
||||||
|
if (!skipTimes || typeof skipTimes !== "object") return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
intro: skipTimes.intro || skipTimes.op || null,
|
||||||
|
outro: skipTimes.outro || skipTimes.ed || null,
|
||||||
|
preview: skipTimes.preview || null,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@@ -349,7 +378,8 @@ const extractSubtitles = (sources) => {
|
|||||||
// ---- FEATURE: Get best available stream with quality fallback ----
|
// ---- FEATURE: Get best available stream with quality fallback ----
|
||||||
/**
|
/**
|
||||||
* Returns the best available HLS stream with automatic quality fallback.
|
* Returns the best available HLS stream with automatic quality fallback.
|
||||||
* Tries 1080p → 720p → 360p in order.
|
* Tries 1080p → 720p → 480p → 360p in order.
|
||||||
|
* Falls back to first HLS stream if no quality match.
|
||||||
*
|
*
|
||||||
* @param {object} sources - The sources response
|
* @param {object} sources - The sources response
|
||||||
* @param {string} [preferredQuality="1080p"] - Preferred quality
|
* @param {string} [preferredQuality="1080p"] - Preferred quality
|
||||||
@@ -371,7 +401,10 @@ const getBestStream = (sources, preferredQuality = "1080p") => {
|
|||||||
if (match) return match;
|
if (match) return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Return first available if no quality match
|
// NOTE: Some providers don't include quality — return first active HLS stream, or first HLS stream
|
||||||
|
const active = streams.find((s) => s.isActive);
|
||||||
|
if (active) return active;
|
||||||
|
|
||||||
return streams[0];
|
return streams[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -382,23 +415,29 @@ const getBestStream = (sources, preferredQuality = "1080p") => {
|
|||||||
// ---- FEATURE: Get download URL for an episode ----
|
// ---- FEATURE: Get download URL for an episode ----
|
||||||
/**
|
/**
|
||||||
* Fetches the download URL for a specific episode from the pipe.
|
* Fetches the download URL for a specific episode from the pipe.
|
||||||
* Returns the direct download link if available.
|
* Returns the direct download link, subtitles, and best stream if available.
|
||||||
*
|
*
|
||||||
* @param {string} provider - Provider name
|
* @param {string} provider - Provider name
|
||||||
* @param {number} anilistId - The AniList anime ID
|
* @param {number} anilistId - The AniList anime ID
|
||||||
* @param {string} category - Audio category ("sub" or "dub")
|
* @param {string} category - Audio category ("sub" or "dub")
|
||||||
* @param {string} slug - Episode slug (e.g., "animepahe-1")
|
* @param {string} slug - Episode slug (e.g., "animepahe-1")
|
||||||
* @returns {Promise<object>} Object with download URL and metadata
|
* @returns {Promise<object>} Object with download URL, subtitles, best stream, and metadata
|
||||||
*/
|
*/
|
||||||
const getDownloadUrl = async (provider, anilistId, category, slug) => {
|
const getDownloadUrl = async (provider, anilistId, category, slug) => {
|
||||||
const sources = await getWatchSources(provider, anilistId, category, slug);
|
const sources = await getWatchSources(provider, anilistId, category, slug);
|
||||||
|
const subtitles = extractSubtitles(sources);
|
||||||
|
const bestStream = getBestStream(sources);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
download: sources.download || null,
|
download: sources.download || null,
|
||||||
|
subtitles,
|
||||||
|
bestStream,
|
||||||
provider,
|
provider,
|
||||||
anilistId,
|
anilistId,
|
||||||
category,
|
category,
|
||||||
slug,
|
slug,
|
||||||
hasDownload: !!sources.download,
|
hasDownload: !!sources.download,
|
||||||
|
hasSubtitles: subtitles.length > 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -465,6 +504,7 @@ module.exports = {
|
|||||||
getDownloadUrl,
|
getDownloadUrl,
|
||||||
getBatchSources,
|
getBatchSources,
|
||||||
extractSubtitles,
|
extractSubtitles,
|
||||||
|
extractSkipTimes,
|
||||||
getBestStream,
|
getBestStream,
|
||||||
encodePipeRequest,
|
encodePipeRequest,
|
||||||
decodePipeResponse,
|
decodePipeResponse,
|
||||||
|
|||||||
+192
-7
@@ -559,6 +559,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// ---- FEATURE: Complete anime info by AniList ID ----
|
// ---- FEATURE: Complete anime info by AniList ID ----
|
||||||
|
// ---- FEATURE: Full anime info with metadata enrichment ----
|
||||||
router.get("/info/:id", async (req, res) => {
|
router.get("/info/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(req.params.id);
|
||||||
@@ -571,7 +572,35 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
setCache(cacheKey, result, 300 * 1000);
|
setCache(cacheKey, result, 300 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResponse(res, result);
|
// NOTE: Enrich with miruro-style metadata fields
|
||||||
|
const enriched = {
|
||||||
|
...result,
|
||||||
|
alternateName: result.synonyms || [],
|
||||||
|
aggregateRating: result.averageScore ? {
|
||||||
|
ratingValue: result.averageScore,
|
||||||
|
bestRating: 100,
|
||||||
|
ratingCount: result.favourites || 0,
|
||||||
|
meanScore: result.meanScore || null,
|
||||||
|
} : null,
|
||||||
|
sameAs: (result.externalLinks || []).map((l) => l.url),
|
||||||
|
externalLinks: (result.externalLinks || []).map((l) => ({
|
||||||
|
url: l.url,
|
||||||
|
site: l.site,
|
||||||
|
type: l.type,
|
||||||
|
})),
|
||||||
|
productionCompany: (result.studios?.nodes || [])
|
||||||
|
.filter((s) => !s.isAnimationStudio)
|
||||||
|
.map((s) => ({ name: s.name, siteUrl: s.siteUrl })),
|
||||||
|
animationStudio: (result.studios?.nodes || [])
|
||||||
|
.filter((s) => s.isAnimationStudio)
|
||||||
|
.map((s) => ({ name: s.name, siteUrl: s.siteUrl })),
|
||||||
|
streamingEpisodes: result.streamingEpisodes || [],
|
||||||
|
trailer: result.trailer || null,
|
||||||
|
nextAiringEpisode: result.nextAiringEpisode || null,
|
||||||
|
tags: (result.tags || []).map((t) => t.name),
|
||||||
|
};
|
||||||
|
|
||||||
|
jsonResponse(res, enriched);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
jsonError(res, err.message);
|
||||||
}
|
}
|
||||||
@@ -668,6 +697,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ---- FEATURE: Streaming sources with quality fallback ----
|
// ---- FEATURE: Streaming sources with quality fallback ----
|
||||||
|
// ---- FEATURE: Stream with quality fallback, subtitles, and skip times ----
|
||||||
router.get("/stream", async (req, res) => {
|
router.get("/stream", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { provider, anilistId, category = "sub", slug, quality = "1080p" } = req.query;
|
const { provider, anilistId, category = "sub", slug, quality = "1080p" } = req.query;
|
||||||
@@ -678,11 +708,13 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
const sources = await pipe.getWatchSources(provider, parseInt(anilistId), category, slug);
|
const sources = await pipe.getWatchSources(provider, parseInt(anilistId), category, slug);
|
||||||
const bestStream = pipe.getBestStream(sources, quality);
|
const bestStream = pipe.getBestStream(sources, quality);
|
||||||
const subtitles = pipe.extractSubtitles(sources);
|
const subtitles = pipe.extractSubtitles(sources);
|
||||||
|
const skipTimes = pipe.extractSkipTimes(sources);
|
||||||
|
|
||||||
jsonResponse(res, {
|
jsonResponse(res, {
|
||||||
...sources,
|
...sources,
|
||||||
bestStream,
|
bestStream,
|
||||||
subtitles,
|
subtitles,
|
||||||
|
skipTimes,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
jsonError(res, err.message);
|
||||||
@@ -708,8 +740,15 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
router.get("/watch/:provider/:anilistId/:category/:slug", async (req, res) => {
|
router.get("/watch/:provider/:anilistId/:category/:slug", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { provider, anilistId, category, slug } = req.params;
|
const { provider, anilistId, category, slug } = req.params;
|
||||||
const result = await pipe.getWatchSources(provider, parseInt(anilistId), category, slug);
|
const sources = await pipe.getWatchSources(provider, parseInt(anilistId), category, slug);
|
||||||
jsonResponse(res, result);
|
const subtitles = pipe.extractSubtitles(sources);
|
||||||
|
const skipTimes = pipe.extractSkipTimes(sources);
|
||||||
|
|
||||||
|
jsonResponse(res, {
|
||||||
|
...sources,
|
||||||
|
subtitles,
|
||||||
|
skipTimes,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, err.message);
|
jsonError(res, err.message);
|
||||||
}
|
}
|
||||||
@@ -907,6 +946,151 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
// HEALTH & STATS
|
// HEALTH & STATS
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Provider capabilities and configuration ----
|
||||||
|
router.get("/providers", (req, res) => {
|
||||||
|
const providers = {
|
||||||
|
kiwi: {
|
||||||
|
name: "kiwi",
|
||||||
|
visible: true,
|
||||||
|
player: "native",
|
||||||
|
parent: null,
|
||||||
|
relationship: null,
|
||||||
|
capabilities: { sub: true, ssub: false, download: true, skip_times: false, thumbnails: false },
|
||||||
|
proxy: { rotate: false, segments: true },
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
pewe: {
|
||||||
|
name: "pewe",
|
||||||
|
visible: true,
|
||||||
|
player: "native",
|
||||||
|
parent: null,
|
||||||
|
relationship: null,
|
||||||
|
capabilities: { sub: true, ssub: false, download: false, skip_times: false, thumbnails: false },
|
||||||
|
proxy: { rotate: false, segments: true },
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
bonk: {
|
||||||
|
name: "bonk",
|
||||||
|
visible: true,
|
||||||
|
player: "native",
|
||||||
|
parent: null,
|
||||||
|
relationship: null,
|
||||||
|
variantOrder: ["ssub", "sub"],
|
||||||
|
capabilities: { sub: true, ssub: true, download: true, skip_times: true, thumbnails: false },
|
||||||
|
proxy: { rotate: false, segments: false },
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
bee: {
|
||||||
|
name: "bee",
|
||||||
|
visible: true,
|
||||||
|
player: "native",
|
||||||
|
parent: null,
|
||||||
|
relationship: null,
|
||||||
|
capabilities: { sub: false, ssub: true, download: false, skip_times: false, thumbnails: false },
|
||||||
|
proxy: { rotate: false, segments: true },
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
ally: {
|
||||||
|
name: "ally",
|
||||||
|
visible: true,
|
||||||
|
player: "native",
|
||||||
|
parent: null,
|
||||||
|
relationship: null,
|
||||||
|
capabilities: { sub: true, ssub: false, download: true, skip_times: false, thumbnails: true },
|
||||||
|
proxy: false,
|
||||||
|
cors: true,
|
||||||
|
fallback: 2,
|
||||||
|
},
|
||||||
|
moo: {
|
||||||
|
name: "moo",
|
||||||
|
visible: true,
|
||||||
|
player: "native",
|
||||||
|
parent: null,
|
||||||
|
relationship: null,
|
||||||
|
capabilities: { sub: true, ssub: false, download: true, skip_times: false, thumbnails: false },
|
||||||
|
proxy: { rotate: false, segments: true },
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
hop: {
|
||||||
|
name: "hop",
|
||||||
|
visible: true,
|
||||||
|
player: "native",
|
||||||
|
parent: null,
|
||||||
|
relationship: null,
|
||||||
|
capabilities: { sub: false, ssub: true, download: false, skip_times: false, thumbnails: true },
|
||||||
|
proxy: { rotate: false, segments: true },
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
nun: {
|
||||||
|
name: "nun",
|
||||||
|
visible: true,
|
||||||
|
player: "iframe",
|
||||||
|
parent: "ally",
|
||||||
|
relationship: "embed",
|
||||||
|
capabilities: { sub: true, ssub: false, download: false, skip_times: false, thumbnails: false },
|
||||||
|
proxy: false,
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
bun: {
|
||||||
|
name: "bun",
|
||||||
|
visible: true,
|
||||||
|
player: "iframe",
|
||||||
|
parent: "bee",
|
||||||
|
relationship: "embed",
|
||||||
|
capabilities: { sub: false, ssub: true, download: false, skip_times: false, thumbnails: false },
|
||||||
|
proxy: false,
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
twin: {
|
||||||
|
name: "twin",
|
||||||
|
visible: true,
|
||||||
|
player: "iframe",
|
||||||
|
parent: "bonk",
|
||||||
|
relationship: "embed",
|
||||||
|
variantOrder: ["sub", "ssub"],
|
||||||
|
capabilities: { sub: true, ssub: true, download: false, skip_times: false, thumbnails: false },
|
||||||
|
proxy: false,
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
cog: {
|
||||||
|
name: "cog",
|
||||||
|
visible: true,
|
||||||
|
player: "iframe",
|
||||||
|
parent: "moo",
|
||||||
|
relationship: "embed",
|
||||||
|
capabilities: { sub: true, ssub: false, download: false, skip_times: false, thumbnails: false },
|
||||||
|
proxy: false,
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
telli: {
|
||||||
|
name: "telli",
|
||||||
|
visible: false,
|
||||||
|
player: "iframe",
|
||||||
|
parent: "kiwi",
|
||||||
|
relationship: "embed",
|
||||||
|
capabilities: { sub: true, ssub: false, download: false, skip_times: false, thumbnails: false },
|
||||||
|
proxy: false,
|
||||||
|
cors: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const order = ["kiwi", "pewe", "bonk", "bee", "ally", "moo", "hop", "nun", "bun", "twin", "cog", "telli"];
|
||||||
|
|
||||||
|
jsonResponse(res, {
|
||||||
|
providers,
|
||||||
|
order,
|
||||||
|
total: order.length,
|
||||||
|
description: "Provider capabilities and configuration",
|
||||||
|
capabilities: {
|
||||||
|
sub: "External subtitles (WebVTT)",
|
||||||
|
ssub: "Soft subtitles (embedded in video stream)",
|
||||||
|
download: "Direct download URL available",
|
||||||
|
skip_times: "OP/ED skip timestamps available",
|
||||||
|
thumbnails: "Episode thumbnail images available",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---- FEATURE: Health check endpoint ----
|
// ---- FEATURE: Health check endpoint ----
|
||||||
router.get("/health", (req, res) => {
|
router.get("/health", (req, res) => {
|
||||||
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
||||||
@@ -918,7 +1102,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
success: true,
|
success: true,
|
||||||
results: {
|
results: {
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
version: "2.0.0",
|
version: "2.1.0",
|
||||||
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(),
|
||||||
@@ -932,7 +1116,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
size: require("../helpers/cache").getCacheSize(),
|
size: require("../helpers/cache").getCacheSize(),
|
||||||
maxSize: 100,
|
maxSize: 100,
|
||||||
},
|
},
|
||||||
endpoints: 35,
|
endpoints: 36,
|
||||||
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"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -960,7 +1144,7 @@ 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: 35,
|
endpoints: 36,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -973,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.0.0",
|
version: "2.1.0",
|
||||||
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" },
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
@@ -1020,6 +1204,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => {
|
|||||||
"/stats/genre": { get: { summary: "Genre statistics", tags: ["Utility"] } },
|
"/stats/genre": { get: { summary: "Genre statistics", tags: ["Utility"] } },
|
||||||
"/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"] } },
|
||||||
"/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"] } },
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* Repository: https://github.com/Shineii86/MiruroAPI
|
* Repository: https://github.com/Shineii86/MiruroAPI
|
||||||
*
|
*
|
||||||
* @description
|
* @description
|
||||||
* Integration test suite for MiruroAPI v2.0.0 endpoints.
|
* Integration test suite for MiruroAPI v2.1.0 endpoints.
|
||||||
* Tests all 35 endpoints for correct response format.
|
* Tests all 36 endpoints for correct response format.
|
||||||
*
|
*
|
||||||
* @author Shinei Nouzen
|
* @author Shinei Nouzen
|
||||||
* @license MIT
|
* @license MIT
|
||||||
@@ -19,6 +19,7 @@ const tests = [
|
|||||||
{ name: "Health", url: "/health" },
|
{ name: "Health", url: "/health" },
|
||||||
{ name: "Stats", url: "/stats" },
|
{ name: "Stats", url: "/stats" },
|
||||||
{ name: "OpenAPI", url: "/openapi" },
|
{ name: "OpenAPI", url: "/openapi" },
|
||||||
|
{ name: "Providers", url: "/providers" },
|
||||||
|
|
||||||
// Search & Discovery
|
// Search & Discovery
|
||||||
{ name: "Search", url: "/search?query=naruto" },
|
{ name: "Search", url: "/search?query=naruto" },
|
||||||
@@ -103,7 +104,7 @@ async function runTest(test) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runAll() {
|
async function runAll() {
|
||||||
console.log(`\n🧪 MiruroAPI v2.0.0 — Running ${tests.length} tests...\n`);
|
console.log(`\n🧪 MiruroAPI v2.1.0 — Running ${tests.length} tests...\n`);
|
||||||
|
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
await runTest(test);
|
await runTest(test);
|
||||||
|
|||||||
Reference in New Issue
Block a user