diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ffd31a..1487999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # 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 diff --git a/README.md b/README.md index f0a4f07..16c1273 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ AniList GraphQL Vercel License - Version - Endpoints + Version + Endpoints Providers

diff --git a/package.json b/package.json index 2f2e8e1..47ff178 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "miruro-api", - "version": "2.0.1", + "version": "2.1.0", "description": "RESTful API for miruro.tv - Anime streaming data powered by AniList GraphQL and Miruro streaming providers", "main": "server.js", "scripts": { diff --git a/src/helpers/pipe.js b/src/helpers/pipe.js index ca86380..fe15793 100644 --- a/src/helpers/pipe.js +++ b/src/helpers/pipe.js @@ -352,6 +352,25 @@ const extractSubtitles = (sources) => { })).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, + }; +}; + // ══════════════════════════════════════════════════════════════ // QUALITY FALLBACK // ══════════════════════════════════════════════════════════════ @@ -485,6 +504,7 @@ module.exports = { getDownloadUrl, getBatchSources, extractSubtitles, + extractSkipTimes, getBestStream, encodePipeRequest, decodePipeResponse, diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js index f66cafb..d69a260 100644 --- a/src/routes/apiRoutes.js +++ b/src/routes/apiRoutes.js @@ -559,6 +559,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { // ══════════════════════════════════════════════════════════════ // ---- FEATURE: Complete anime info by AniList ID ---- + // ---- FEATURE: Full anime info with metadata enrichment ---- router.get("/info/:id", async (req, res) => { try { const id = parseInt(req.params.id); @@ -571,7 +572,35 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { 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) { jsonError(res, err.message); } @@ -668,6 +697,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { }); // ---- FEATURE: Streaming sources with quality fallback ---- + // ---- FEATURE: Stream with quality fallback, subtitles, and skip times ---- router.get("/stream", async (req, res) => { try { 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 bestStream = pipe.getBestStream(sources, quality); const subtitles = pipe.extractSubtitles(sources); + const skipTimes = pipe.extractSkipTimes(sources); jsonResponse(res, { ...sources, bestStream, subtitles, + skipTimes, }); } catch (err) { jsonError(res, err.message); @@ -708,8 +740,15 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { router.get("/watch/:provider/:anilistId/:category/:slug", async (req, res) => { try { const { provider, anilistId, category, slug } = req.params; - const result = await pipe.getWatchSources(provider, parseInt(anilistId), category, slug); - jsonResponse(res, result); + const sources = await pipe.getWatchSources(provider, parseInt(anilistId), category, slug); + const subtitles = pipe.extractSubtitles(sources); + const skipTimes = pipe.extractSkipTimes(sources); + + jsonResponse(res, { + ...sources, + subtitles, + skipTimes, + }); } catch (err) { jsonError(res, err.message); } @@ -907,6 +946,151 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { // 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 ---- router.get("/health", (req, res) => { const uptime = Math.floor((Date.now() - startTime) / 1000); @@ -918,7 +1102,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { success: true, results: { status: "healthy", - version: "2.0.0", + version: "2.1.0", uptime: `${hours}h ${minutes}m ${seconds}s`, uptimeSeconds: uptime, timestamp: new Date().toISOString(), @@ -932,7 +1116,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { size: require("../helpers/cache").getCacheSize(), maxSize: 100, }, - endpoints: 35, + endpoints: 36, 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(), description: "Map-based cache with TTL expiration and FIFO eviction", }, - endpoints: 35, + endpoints: 36, timestamp: new Date().toISOString(), }, }); @@ -973,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.0.0", + version: "2.1.0", contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" }, }, servers: [ @@ -1020,6 +1204,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { "/stats/genre": { get: { summary: "Genre statistics", tags: ["Utility"] } }, "/calendar": { get: { summary: "Monthly calendar", tags: ["Utility"] } }, "/timeline/{id}": { get: { summary: "Anime timeline", tags: ["Utility"] } }, + "/providers": { get: { summary: "Provider capabilities", tags: ["System"] } }, "/health": { get: { summary: "Health check", tags: ["System"] } }, "/stats": { get: { summary: "API statistics", tags: ["System"] } }, "/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } }, diff --git a/test.js b/test.js index cae57d5..007b9cf 100644 --- a/test.js +++ b/test.js @@ -4,8 +4,8 @@ * Repository: https://github.com/Shineii86/MiruroAPI * * @description - * Integration test suite for MiruroAPI v2.0.1 endpoints. - * Tests all 35 endpoints for correct response format. + * Integration test suite for MiruroAPI v2.1.0 endpoints. + * Tests all 36 endpoints for correct response format. * * @author Shinei Nouzen * @license MIT @@ -19,6 +19,7 @@ const tests = [ { name: "Health", url: "/health" }, { name: "Stats", url: "/stats" }, { name: "OpenAPI", url: "/openapi" }, + { name: "Providers", url: "/providers" }, // Search & Discovery { name: "Search", url: "/search?query=naruto" }, @@ -103,7 +104,7 @@ async function runTest(test) { } async function runAll() { - console.log(`\n🧪 MiruroAPI v2.0.1 — Running ${tests.length} tests...\n`); + console.log(`\n🧪 MiruroAPI v2.1.0 — Running ${tests.length} tests...\n`); for (const test of tests) { await runTest(test);