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 | ||
|
|
3ab0bae18d | ||
|
|
33e5e8e136 | ||
|
|
3675bbba77 | ||
|
|
9ada6e1016 | ||
|
|
3a28c4d182 | ||
|
|
68f7ea7cf4 | ||
|
|
37e70c3bda | ||
|
|
af08c91624 |
+138
-6
@@ -1,5 +1,137 @@
|
||||
# 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
|
||||
### Major Release — 35 endpoints, streaming improvements, new features
|
||||
|
||||
#### Technical Foundation
|
||||
- Added gzip compression middleware (30-70% response size reduction)
|
||||
- Added request logging with method, path, status, duration, IP
|
||||
- Added Cache-Control headers for API responses (varies by endpoint type)
|
||||
- Improved CORS with preflight caching (24h max-age)
|
||||
- Health endpoint now includes memory stats (heap, RSS), cache size, node version
|
||||
|
||||
#### New AniList Helpers (11 new functions)
|
||||
- getAllGenres — complete genre list from AniList
|
||||
- getAllTags — complete tag list with metadata
|
||||
- getTopAnime — top-rated anime of all time (SCORE_DESC)
|
||||
- getTrendingDaily — trending anime (last 7 days)
|
||||
- getTrendingWeekly — trending anime (last 30 days)
|
||||
- getSeasonalAnime — anime by year + season (e.g., 2025/Spring)
|
||||
- getAnimeByStudio — anime produced by a specific studio
|
||||
- getCharacterInfo — character detail with anime list
|
||||
- getStaffInfo — staff detail with anime list
|
||||
- searchCharacters — search characters by name
|
||||
- searchStaff — search staff by name
|
||||
|
||||
#### New Streaming & Download Features
|
||||
- extractSubtitles — extract subtitle URLs from sources
|
||||
- getBestStream — automatic quality fallback (1080p → 720p → 360p)
|
||||
- getDownloadUrl — dedicated download URL endpoint
|
||||
- getBatchSources — fetch multiple episode sources in parallel
|
||||
|
||||
#### New API Endpoints (17 new)
|
||||
- GET /api/trending/daily — daily trending anime
|
||||
- GET /api/trending/weekly — weekly trending anime
|
||||
- GET /api/top — top anime by score
|
||||
- GET /api/random — random anime of the day
|
||||
- GET /api/multi-search — combined query + filters
|
||||
- GET /api/season/:year/:season — seasonal anime
|
||||
- GET /api/studio/:name — anime by studio
|
||||
- GET /api/genres — all genres list
|
||||
- GET /api/tags — all tags list
|
||||
- GET /api/genre/:name — anime by genre
|
||||
- GET /api/year/:year — anime by year
|
||||
- GET /api/status/:status — anime by status
|
||||
- GET /api/format/:format — anime by format
|
||||
- GET /api/character/:id — character detail
|
||||
- GET /api/staff/:id — staff detail
|
||||
- GET /api/characters — search characters
|
||||
- GET /api/staff — search staff
|
||||
- GET /api/episodes/:id/batch — batch episode sources
|
||||
- GET /api/stream — streaming with quality fallback
|
||||
- GET /api/download — download URL
|
||||
- GET /api/compare/:id1/:id2 — compare two anime
|
||||
- GET /api/stats/genre — genre statistics
|
||||
- GET /api/calendar — monthly airing calendar
|
||||
- GET /api/timeline/:id — anime release timeline
|
||||
|
||||
#### Streaming Improvements
|
||||
- Subtitle URL extraction from pipe responses
|
||||
- Quality fallback: tries 1080p → 720p → 480p → 360p automatically
|
||||
- Batch episode fetching (up to 5 parallel requests)
|
||||
- Dedicated download endpoint with provider metadata
|
||||
- Cache headers: episodes/sources (2min), info (5min), genres/tags (24h)
|
||||
|
||||
#### Version synced to 2.0.0 across all files
|
||||
|
||||
## v1.4.0
|
||||
- Added creator attribution middleware — injects author info into every response
|
||||
- Fixed Vercel deployment: replaced legacy `builds`/`routes` config with modern `functions` config
|
||||
- Added `api/index.js` serverless entry point
|
||||
- Security headers: removed deprecated `X-XSS-Protection`, added `Strict-Transport-Security` (HSTS)
|
||||
- Rate limiter: uses `x-forwarded-for` header for correct IP detection on Vercel
|
||||
- Rate limiter: added periodic IP entry pruning when >200 IPs tracked
|
||||
- Cache: added periodic expired entry cleanup when cache reaches 80% capacity
|
||||
- Version synced to 1.4.0
|
||||
|
||||
## v1.3.0
|
||||
- Full landing page upgrade to match AniKotoAPI style
|
||||
- Added canvas particle effects with cyan-colored floating particles
|
||||
@@ -7,18 +139,18 @@
|
||||
- Added animated stat counters that count up on scroll
|
||||
- Added infinite scrolling marquee with 8 feature badges
|
||||
- Added live interactive API console with 5 preset endpoint buttons
|
||||
- Added sidebar endpoint explorer with 16 endpoints and multi-language code tabs (cURL, JavaScript, Python)
|
||||
- Added sidebar endpoint explorer with 16 endpoints and multi-language code tabs
|
||||
- Added 4 interactive playground cards (Search, Info, Episodes, Trending)
|
||||
- Added custom scrollbar styling
|
||||
- Added JetBrains Mono font for code blocks
|
||||
- Added 4-column grid footer with API, Resources, Legal sections
|
||||
- Added 4-column grid footer
|
||||
- Added fixed header with scroll-aware blur/glass effect
|
||||
- Added ambient orb gradient animations
|
||||
- Added CTA section with gradient overlay
|
||||
- Color scheme updated to match MiruroAPI brand (cyan #38bdf8 + indigo #818cf8)
|
||||
- All branding updated: AniKotoAPI references → MiruroAPI
|
||||
- OG/Twitter meta tags updated for mirurotvapi.vercel.app
|
||||
- JSON-LD structured data updated with image, category, programmingModel
|
||||
- JSON-LD structured data updated
|
||||
- Real Miruro SVG favicons used in header and footer
|
||||
|
||||
## v1.2.0
|
||||
@@ -26,14 +158,14 @@
|
||||
- Root cause: createApiRoutes() had (app, jsonResponse, jsonError) but was called with (jsonResponse, jsonError), shifting all params
|
||||
- Every jsonResponse() call was actually calling jsonError(), returning {success: false, message: data}
|
||||
- Health endpoint worked because it used res.json() directly, bypassing the wrapper
|
||||
- All 18 endpoints verified working: health, stats, search, suggestions, filter, trending, popular, upcoming, recent, spotlight, schedule, info, characters, relations, recommendations, episodes, watch
|
||||
- All 18 endpoints verified working
|
||||
|
||||
## v1.1.0
|
||||
- Added Swagger UI interactive docs at /docs
|
||||
- Added OpenAPI 3.0 spec at /openapi.json
|
||||
- Added mappings field in episodes response (anilistId, malId, kitsuId)
|
||||
- Added mappings field in episodes response
|
||||
- Added Docker support (Dockerfile + .dockerignore)
|
||||
- Improved landing page with premium design (Walter-style)
|
||||
- Improved landing page with premium design
|
||||
- Added "Try it" buttons on all endpoints
|
||||
- Health endpoint now includes version, providers list, and endpoint count
|
||||
- Version bump to 1.1.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="https://capsule-render.vercel.app/api?type=waving&height=300&color=gradient&text=MiruroAPI&fontAlignY=30&fontSize=90&desc=Free%20Anime%20Streaming%20API&descSize=30" />
|
||||
<img src="https://capsule-render.vercel.app/api?type=waving&height=300&color=gradient&text=𝙈𝙞𝙧𝙪𝙧𝙤𝘼𝙋𝙄&fontAlignY=30&fontSize=100&desc=𝖠%20𝖢𝗈𝗆𝗉𝗅𝖾𝗍𝖾%20𝖱𝖤𝖲𝖳𝖿𝗎𝗅%20𝖠𝖯𝖨%20𝖥𝗈𝗋%20𝖠𝗇𝗂𝗆𝖾%20𝖲𝗍𝗋𝖾𝖺𝗆𝗂𝗇𝗀&descSize=25" />
|
||||
|
||||
</div>
|
||||
|
||||
@@ -19,15 +19,15 @@
|
||||
<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/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
||||
<img src="https://img.shields.io/badge/Version-1.2.0-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
||||
<img src="https://img.shields.io/badge/Endpoints-18-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
||||
<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-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"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>A complete RESTful API for anime streaming data powered by AniList GraphQL and Miruro providers</b><br/>
|
||||
Search, browse, filter, watch — every endpoint returns fresh data with smart caching.<br/>
|
||||
18 endpoints, 12 streaming providers, M3U8 URLs with subtitles and skip timestamps.
|
||||
35 endpoints, 12 streaming providers, M3U8 URLs with subtitles, skip timestamps, and batch operations.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -41,8 +41,7 @@
|
||||
|
||||
---
|
||||
|
||||
> ## ⚠️ Disclaimer
|
||||
>
|
||||
> [!WARNING]
|
||||
> 1. This `API` does not store any files — it only links to media hosted on 3rd party services.
|
||||
> 2. This `API` is explicitly made for **educational purposes only** and not for commercial usage. This repo will not be responsible for any misuse of it.
|
||||
> 3. All anime data, images, and content belong to their respective owners (AniList, Miruro). This project is not affiliated with miruro.tv.
|
||||
@@ -85,7 +84,7 @@
|
||||
|
||||
### Why MiruroAPI?
|
||||
|
||||
- 🎬 **18 Endpoints** — Complete anime data coverage
|
||||
- 🎬 **35 Endpoints** — Complete anime data coverage
|
||||
- 🔍 **Full-Text Search** — Search anime by keyword with suggestions
|
||||
- 🎭 **Characters & Voice Actors** — Full character data from AniList
|
||||
- 🎯 **Advanced Filtering** — Genre, year, season, format, sort
|
||||
@@ -93,6 +92,8 @@
|
||||
- 📅 **Airing Schedule** — See what's airing on any date
|
||||
- 📡 **12 Streaming Providers** — M3U8 streaming sources
|
||||
- ⏭️ **Skip Timestamps** — OP/ED skip data
|
||||
- 🎭 **Character & Staff Search** — Search characters and staff by name
|
||||
- 🎬 **Batch Episodes** — Fetch multiple episode sources in parallel
|
||||
- ⚡ **Smart Caching** — In-memory Map with configurable TTL
|
||||
- 🔒 **CORS Enabled** — Works from any frontend, no proxy needed
|
||||
- 🚀 **Zero-Config Deploy** — One click to Vercel, or run standalone with Express
|
||||
@@ -139,7 +140,9 @@ flowchart TD
|
||||
- **AniList GraphQL** for rich metadata
|
||||
- **Miruro pipe** for streaming sources
|
||||
- **Smart caching** with configurable TTL
|
||||
- **18 RESTful endpoints**
|
||||
- **35 RESTful endpoints**
|
||||
- **Gzip compression** — 30-70% smaller responses
|
||||
- **Request logging** — method, path, status, duration
|
||||
- **Graceful error handling** per endpoint
|
||||
- **Rate limiting** (100 req/min per IP)
|
||||
|
||||
@@ -153,6 +156,9 @@ flowchart TD
|
||||
- **Characters** with voice actors
|
||||
- **Relations** and **Recommendations**
|
||||
- **Airing schedule** by date
|
||||
- **Seasonal anime** by year + season
|
||||
- **Studio filter** — anime by studio name
|
||||
- **Character & Staff search**
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -166,6 +172,9 @@ flowchart TD
|
||||
- **Download links** when available
|
||||
- **Sub/Dub** support per provider
|
||||
- **Codec and fansub** metadata
|
||||
- **Batch episode fetching** — multiple sources in parallel
|
||||
- **Quality fallback** — auto 1080p → 720p → 360p
|
||||
- **Subtitle extraction** — expose subtitle URLs
|
||||
|
||||
</td>
|
||||
<td>
|
||||
@@ -186,7 +195,7 @@ flowchart TD
|
||||
|
||||
| Feature | Description | Status |
|
||||
|:---|:---|:---:|
|
||||
| 🎬 18 API Endpoints | Complete anime data coverage | ✅ |
|
||||
| 🎬 35 API Endpoints | Complete anime data coverage | ✅ |
|
||||
| 🔍 Full-Text Search | Keyword search with pagination | ✅ |
|
||||
| 💡 Search Suggestions | Fast autocomplete | ✅ |
|
||||
| 🎯 Advanced Filtering | Genre, year, season, format, sort | ✅ |
|
||||
@@ -318,7 +327,7 @@ MiruroAPI/
|
||||
│ │ └── 📄 cache.js # 💾 In-memory cache with TTL
|
||||
│ │
|
||||
│ └── 📂 routes/ # 🛤️ Express routes
|
||||
│ └── 📄 apiRoutes.js # 🌐 Main API routes (18 endpoints)
|
||||
│ └── 📄 apiRoutes.js # 🌐 Main API routes (35 endpoints)
|
||||
│
|
||||
├── 📄 server.js # 🚀 Express server entry point
|
||||
├── 📄 package.json # 📦 Dependencies & scripts
|
||||
@@ -481,13 +490,13 @@ console.log(resp.data);
|
||||
"success": true,
|
||||
"results": {
|
||||
"status": "healthy",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.0",
|
||||
"uptime": "0h 0m 34s",
|
||||
"uptimeSeconds": 34,
|
||||
"timestamp": "2026-06-09T09:55:00.884Z",
|
||||
"node": "v24.14.1",
|
||||
"memory": { "used": "13MB", "total": "15MB" },
|
||||
"endpoints": 16,
|
||||
"endpoints": 35,
|
||||
"providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"]
|
||||
}
|
||||
}
|
||||
@@ -528,7 +537,7 @@ console.log(resp.data);
|
||||
"uptime": "0h 0m 34s",
|
||||
"requests": { "total": 156, "errors": 3, "successRate": "98.1%" },
|
||||
"cache": { "size": 12, "maxSize": 100, "ttl": "1 min" },
|
||||
"endpoints": 16,
|
||||
"endpoints": 35,
|
||||
"timestamp": "2026-06-09T09:55:00.884Z"
|
||||
}
|
||||
}
|
||||
@@ -1399,6 +1408,10 @@ docker run -p 3000:3000 miruroapi
|
||||
|
||||
| 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 |
|
||||
| **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.2.0** | 2026-06-09 | Critical response format fix, full endpoint diagnostic, 18/18 passing |
|
||||
| **1.1.0** | 2026-06-09 | Swagger UI docs, OpenAPI spec, mappings field, Docker, landing page |
|
||||
| **1.0.0** | 2026-06-09 | Initial release — 16 endpoints, AniList GraphQL + Miruro pipe, caching |
|
||||
@@ -1494,7 +1507,7 @@ Yes! Use <code>npm start</code> to run the Express server on any VPS, Docker con
|
||||
|
||||
### ✅ Completed
|
||||
|
||||
- [x] 🎬 18 API endpoints covering all data
|
||||
- [x] 🎬 35 API endpoints covering all data
|
||||
- [x] 🔍 Full-text search with pagination
|
||||
- [x] 💡 Search suggestions for autocomplete
|
||||
- [x] 🎯 Advanced filtering (genre, year, season, format, sort)
|
||||
@@ -1576,8 +1589,8 @@ git push origin feature/amazing-feature
|
||||
- ✅ Update CHANGELOG.md with your changes
|
||||
- ✅ Keep PRs focused — one feature or fix per PR
|
||||
- ✅ Add JSDoc comments for new functions
|
||||
- ❌ Don't commit `node_modules` or cache files
|
||||
- ❌ Don't add unrelated changes to a single PR
|
||||
- ❎ Don't commit `node_modules` or cache files
|
||||
- ❎ Don't add unrelated changes to a single PR
|
||||
|
||||
---
|
||||
|
||||
@@ -1657,8 +1670,10 @@ Free to use, modify, and distribute — see the [LICENSE](LICENSE) file for deta
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<div align="center">
|
||||
<p align="center">
|
||||
<b>Made With ❤️ For The Anime Community</b>
|
||||
<br/><br/>
|
||||
<sub>© 2026 Shineii86. All Rights Reserved.</sub>
|
||||
</p>
|
||||
<sub>© Shinei Nouzen. All Rights Reserved.</sub>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("../server");
|
||||
+565
-311
@@ -1,29 +1,579 @@
|
||||
# API Endpoints Reference
|
||||
|
||||
Complete documentation for all 18 MiruroAPI endpoints.
|
||||
Complete documentation for all 35 MiruroAPI v2.0.1 endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Health Check](#health-check)
|
||||
- [Stats](#stats)
|
||||
### Search & Discovery
|
||||
- [Search](#search)
|
||||
- [Suggestions](#suggestions)
|
||||
- [Filter](#filter)
|
||||
- [Multi-Search](#multi-search)
|
||||
|
||||
### Collections
|
||||
- [Trending](#trending)
|
||||
- [Trending Daily](#trending-daily)
|
||||
- [Trending Weekly](#trending-weekly)
|
||||
- [Popular](#popular)
|
||||
- [Top](#top)
|
||||
- [Upcoming](#upcoming)
|
||||
- [Recent](#recent)
|
||||
- [Spotlight](#spotlight)
|
||||
- [Schedule](#schedule)
|
||||
- [Random](#random)
|
||||
- [Upcoming Countdown](#upcoming-countdown)
|
||||
|
||||
### Seasonal & Studio
|
||||
- [Seasonal Anime](#seasonal-anime)
|
||||
- [Anime by Studio](#anime-by-studio)
|
||||
|
||||
### Genres & Tags
|
||||
- [Genres](#genres)
|
||||
- [Tags](#tags)
|
||||
- [Anime by Genre](#anime-by-genre)
|
||||
- [Anime by Year](#anime-by-year)
|
||||
- [Anime by Status](#anime-by-status)
|
||||
- [Anime by Format](#anime-by-format)
|
||||
|
||||
### Character & Staff
|
||||
- [Character Detail](#character-detail)
|
||||
- [Staff Detail](#staff-detail)
|
||||
- [Search Characters](#search-characters)
|
||||
- [Search Staff](#search-staff)
|
||||
|
||||
### Anime Details
|
||||
- [Anime Info](#anime-info)
|
||||
- [Characters](#characters)
|
||||
- [Relations](#relations)
|
||||
- [Recommendations](#recommendations)
|
||||
|
||||
### Streaming & Downloading
|
||||
- [Episodes](#episodes)
|
||||
- [Batch Episodes](#batch-episodes)
|
||||
- [Sources (Detailed)](#sources-detailed)
|
||||
- [Stream (Quality Fallback)](#stream-quality-fallback)
|
||||
- [Download URL](#download-url)
|
||||
- [Watch (Streaming)](#watch-streaming)
|
||||
|
||||
### Utility
|
||||
- [Compare](#compare)
|
||||
- [Genre Stats](#genre-stats)
|
||||
- [Calendar](#calendar)
|
||||
- [Timeline](#timeline)
|
||||
|
||||
### System
|
||||
- [Health Check](#health-check)
|
||||
- [Stats](#stats)
|
||||
- [OpenAPI](#openapi)
|
||||
|
||||
---
|
||||
|
||||
## Search
|
||||
|
||||
```
|
||||
GET /api/search?query=naruto
|
||||
```
|
||||
|
||||
Full-text anime search with pagination.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| query | string | required | Search query |
|
||||
| page | int | 1 | Page number |
|
||||
| per_page | int | 20 | Results per page (max 50) |
|
||||
|
||||
---
|
||||
|
||||
## Suggestions
|
||||
|
||||
```
|
||||
GET /api/suggestions?query=naruto
|
||||
```
|
||||
|
||||
Lightweight autocomplete suggestions (max 8 results).
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| query | string | required | Search query |
|
||||
|
||||
---
|
||||
|
||||
## Filter
|
||||
|
||||
```
|
||||
GET /api/filter?genre=Action&year=2024&season=SPRING
|
||||
```
|
||||
|
||||
Advanced anime filter with multiple parameters.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| genre | string | — | Genre name |
|
||||
| tag | string | — | Tag name |
|
||||
| year | int | — | Season year |
|
||||
| season | string | — | WINTER, SPRING, SUMMER, FALL |
|
||||
| format | string | — | TV, MOVIE, OVA, ONA, SPECIAL |
|
||||
| status | string | — | RELEASING, FINISHED, NOT_YET_RELEASED |
|
||||
| sort | string | POPULARITY_DESC | Sort order |
|
||||
| page | int | 1 | Page number |
|
||||
| per_page | int | 20 | Results per page |
|
||||
|
||||
---
|
||||
|
||||
## Multi-Search
|
||||
|
||||
```
|
||||
GET /api/multi-search?q=naruto&genre=Action&year=2024
|
||||
```
|
||||
|
||||
Combined query + filters search.
|
||||
|
||||
---
|
||||
|
||||
## Trending
|
||||
|
||||
```
|
||||
GET /api/trending?page=1&per_page=20
|
||||
```
|
||||
|
||||
Currently trending anime.
|
||||
|
||||
---
|
||||
|
||||
## Trending Daily
|
||||
|
||||
```
|
||||
GET /api/trending/daily
|
||||
```
|
||||
|
||||
Trending anime from the last 7 days.
|
||||
|
||||
---
|
||||
|
||||
## Trending Weekly
|
||||
|
||||
```
|
||||
GET /api/trending/weekly
|
||||
```
|
||||
|
||||
Trending anime from the last 30 days.
|
||||
|
||||
---
|
||||
|
||||
## Popular
|
||||
|
||||
```
|
||||
GET /api/popular
|
||||
```
|
||||
|
||||
Most popular anime of all time.
|
||||
|
||||
---
|
||||
|
||||
## Top
|
||||
|
||||
```
|
||||
GET /api/top
|
||||
```
|
||||
|
||||
Top anime by score (all time).
|
||||
|
||||
---
|
||||
|
||||
## Upcoming
|
||||
|
||||
```
|
||||
GET /api/upcoming
|
||||
```
|
||||
|
||||
Most anticipated upcoming anime.
|
||||
|
||||
---
|
||||
|
||||
## Recent
|
||||
|
||||
```
|
||||
GET /api/recent
|
||||
```
|
||||
|
||||
Currently airing anime.
|
||||
|
||||
---
|
||||
|
||||
## Spotlight
|
||||
|
||||
```
|
||||
GET /api/spotlight
|
||||
```
|
||||
|
||||
Top 10 trending + popular anime for hero banners.
|
||||
|
||||
---
|
||||
|
||||
## Schedule
|
||||
|
||||
```
|
||||
GET /api/schedule?page=1&per_page=20
|
||||
```
|
||||
|
||||
Airing schedule with timestamps.
|
||||
|
||||
---
|
||||
|
||||
## Random
|
||||
|
||||
```
|
||||
GET /api/random
|
||||
```
|
||||
|
||||
Returns a random anime.
|
||||
|
||||
---
|
||||
|
||||
## Upcoming Countdown
|
||||
|
||||
```
|
||||
GET /api/upcoming/countdown
|
||||
```
|
||||
|
||||
Next 10 episodes airing soon.
|
||||
|
||||
---
|
||||
|
||||
## Seasonal Anime
|
||||
|
||||
```
|
||||
GET /api/season/2025/spring
|
||||
```
|
||||
|
||||
Anime for a specific year and season.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| year | int | required | Year (e.g., 2025) |
|
||||
| season | string | required | WINTER, SPRING, SUMMER, FALL |
|
||||
|
||||
---
|
||||
|
||||
## Anime by Studio
|
||||
|
||||
```
|
||||
GET /api/studio/MAPPA
|
||||
```
|
||||
|
||||
All anime produced by a specific studio.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| name | string | required | Studio name |
|
||||
|
||||
---
|
||||
|
||||
## Genres
|
||||
|
||||
```
|
||||
GET /api/genres
|
||||
```
|
||||
|
||||
Returns all available genres from AniList.
|
||||
|
||||
---
|
||||
|
||||
## Tags
|
||||
|
||||
```
|
||||
GET /api/tags
|
||||
```
|
||||
|
||||
Returns all available tags from AniList.
|
||||
|
||||
---
|
||||
|
||||
## Anime by Genre
|
||||
|
||||
```
|
||||
GET /api/genre/Action
|
||||
```
|
||||
|
||||
All anime in a specific genre.
|
||||
|
||||
---
|
||||
|
||||
## Anime by Year
|
||||
|
||||
```
|
||||
GET /api/year/2024
|
||||
```
|
||||
|
||||
All anime from a specific year.
|
||||
|
||||
---
|
||||
|
||||
## Anime by Status
|
||||
|
||||
```
|
||||
GET /api/status/RELEASING
|
||||
```
|
||||
|
||||
All anime with a specific status (RELEASING, FINISHED, NOT_YET_RELEASED).
|
||||
|
||||
---
|
||||
|
||||
## Anime by Format
|
||||
|
||||
```
|
||||
GET /api/format/MOVIE
|
||||
```
|
||||
|
||||
All anime with a specific format (TV, MOVIE, OVA, ONA, SPECIAL).
|
||||
|
||||
---
|
||||
|
||||
## Character Detail
|
||||
|
||||
```
|
||||
GET /api/character/1
|
||||
```
|
||||
|
||||
Detailed character info with anime list.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| id | int | required | AniList character ID |
|
||||
|
||||
---
|
||||
|
||||
## Staff Detail
|
||||
|
||||
```
|
||||
GET /api/staff/1
|
||||
```
|
||||
|
||||
Detailed staff info with anime list.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| id | int | required | AniList staff ID |
|
||||
|
||||
---
|
||||
|
||||
## Search Characters
|
||||
|
||||
```
|
||||
GET /api/characters?query=kaneki
|
||||
```
|
||||
|
||||
Search characters by name.
|
||||
|
||||
---
|
||||
|
||||
## Search Staff
|
||||
|
||||
```
|
||||
GET /api/staff?query=oda
|
||||
```
|
||||
|
||||
Search staff by name.
|
||||
|
||||
---
|
||||
|
||||
## Anime Info
|
||||
|
||||
```
|
||||
GET /api/info/20605
|
||||
```
|
||||
|
||||
Complete anime info including characters, relations, recommendations, trailer, stats.
|
||||
|
||||
---
|
||||
|
||||
## Characters
|
||||
|
||||
```
|
||||
GET /api/anime/20605/characters?page=1&per_page=25
|
||||
```
|
||||
|
||||
Paginated character list with voice actors.
|
||||
|
||||
---
|
||||
|
||||
## Relations
|
||||
|
||||
```
|
||||
GET /api/anime/20605/relations
|
||||
```
|
||||
|
||||
Related anime (sequels, prequels, spin-offs).
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
```
|
||||
GET /api/anime/20605/recommendations?page=1&per_page=10
|
||||
```
|
||||
|
||||
Community recommendations sorted by rating.
|
||||
|
||||
---
|
||||
|
||||
## Episodes
|
||||
|
||||
```
|
||||
GET /api/episodes/20605
|
||||
```
|
||||
|
||||
Episode list from all providers with sub/dub support.
|
||||
|
||||
---
|
||||
|
||||
## Batch Episodes
|
||||
|
||||
```
|
||||
GET /api/episodes/20605/batch?provider=kiwi&category=sub&slugs=animepahe-1,animepahe-2
|
||||
```
|
||||
|
||||
Fetch multiple episode sources in parallel.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| provider | string | kiwi | Provider name |
|
||||
| category | string | sub | sub or dub |
|
||||
| slugs | string | required | Comma-separated episode slugs |
|
||||
|
||||
---
|
||||
|
||||
## Sources (Detailed)
|
||||
|
||||
```
|
||||
GET /api/sources?episodeId=xxx&provider=kiwi&anilistId=20605&category=sub
|
||||
```
|
||||
|
||||
Streaming sources with raw episode ID.
|
||||
|
||||
---
|
||||
|
||||
## Stream (Quality Fallback)
|
||||
|
||||
```
|
||||
GET /api/stream?provider=kiwi&anilistId=20605&category=sub&slug=animepahe-1&quality=1080p
|
||||
```
|
||||
|
||||
Streaming sources with automatic quality fallback.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| provider | string | required | Provider name |
|
||||
| anilistId | int | required | AniList anime ID |
|
||||
| category | string | sub | sub or dub |
|
||||
| slug | string | required | Episode slug |
|
||||
| quality | string | 1080p | Preferred quality |
|
||||
|
||||
---
|
||||
|
||||
## Download URL
|
||||
|
||||
```
|
||||
GET /api/download?provider=kiwi&anilistId=20605&category=sub&slug=animepahe-1
|
||||
```
|
||||
|
||||
Returns the download URL for an episode.
|
||||
|
||||
---
|
||||
|
||||
## Watch (Streaming)
|
||||
|
||||
```
|
||||
GET /api/watch/kiwi/20605/sub/animepahe-1
|
||||
```
|
||||
|
||||
Streaming sources via URL path (recommended).
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| provider | string | Provider name |
|
||||
| anilistId | int | AniList anime ID |
|
||||
| category | string | sub or dub |
|
||||
| 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
|
||||
|
||||
```
|
||||
GET /api/compare/20605/20
|
||||
```
|
||||
|
||||
Compare two anime side by side.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| id1 | int | First anime ID |
|
||||
| id2 | int | Second anime ID |
|
||||
|
||||
---
|
||||
|
||||
## Genre Stats
|
||||
|
||||
```
|
||||
GET /api/stats/genre
|
||||
```
|
||||
|
||||
Genre statistics with top anime per genre.
|
||||
|
||||
---
|
||||
|
||||
## Calendar
|
||||
|
||||
```
|
||||
GET /api/calendar?year=2025&month=6
|
||||
```
|
||||
|
||||
Monthly airing calendar.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| year | int | current | Year |
|
||||
| month | int | current | Month (1-12) |
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
```
|
||||
GET /api/timeline/20605
|
||||
```
|
||||
|
||||
Anime release timeline with prequels, sequels, spin-offs.
|
||||
|
||||
---
|
||||
|
||||
## Health Check
|
||||
@@ -32,7 +582,7 @@ Complete documentation for all 18 MiruroAPI endpoints.
|
||||
GET /api/health
|
||||
```
|
||||
|
||||
Returns API health status, version, uptime, memory usage, and available providers.
|
||||
API health status, version, uptime, memory, cache stats.
|
||||
|
||||
**Response:**
|
||||
|
||||
@@ -41,11 +591,12 @@ Returns API health status, version, uptime, memory usage, and available provider
|
||||
"success": true,
|
||||
"results": {
|
||||
"status": "healthy",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.0",
|
||||
"uptime": "0h 0m 34s",
|
||||
"node": "v24.14.1",
|
||||
"memory": { "used": "13MB", "total": "15MB" },
|
||||
"endpoints": 16,
|
||||
"memory": { "used": "13MB", "total": "15MB", "rss": "25MB" },
|
||||
"cache": { "size": 12, "maxSize": 100 },
|
||||
"endpoints": 35,
|
||||
"providers": ["kiwi","pewe","bee","bonk","bun","ally","nun","twin","cog","moo","hop","telli"]
|
||||
}
|
||||
}
|
||||
@@ -59,317 +610,20 @@ Returns API health status, version, uptime, memory usage, and available provider
|
||||
GET /api/stats
|
||||
```
|
||||
|
||||
Returns request statistics, cache info, and memory usage.
|
||||
Cache & API statistics including request count and success rate.
|
||||
|
||||
---
|
||||
|
||||
## Search
|
||||
## OpenAPI
|
||||
|
||||
```
|
||||
GET /api/search
|
||||
GET /api/openapi
|
||||
```
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `query` | string | Yes | — | Search keyword |
|
||||
| `page` | number | No | 1 | Page number |
|
||||
| `per_page` | number | No | 20 | Results per page |
|
||||
|
||||
**Example:** `GET /api/search?query=naruto&per_page=2`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"results": {
|
||||
"page": 1,
|
||||
"perPage": 2,
|
||||
"total": 5000,
|
||||
"hasNextPage": true,
|
||||
"results": [
|
||||
{
|
||||
"id": 20,
|
||||
"title": { "romaji": "NARUTO", "english": "Naruto", "native": "NARUTO -ナルト-" },
|
||||
"coverImage": { "large": "https://s4.anilist.co/file/..." },
|
||||
"format": "TV",
|
||||
"episodes": 220,
|
||||
"status": "FINISHED",
|
||||
"averageScore": 80,
|
||||
"genres": ["Action","Adventure","Comedy","Drama","Fantasy","Supernatural"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
Returns the OpenAPI 3.0.3 specification in JSON format.
|
||||
|
||||
---
|
||||
|
||||
## Suggestions
|
||||
|
||||
```
|
||||
GET /api/suggestions
|
||||
```
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `query` | string | Yes | Search keyword |
|
||||
|
||||
**Example:** `GET /api/suggestions?query=naruto`
|
||||
|
||||
Returns fast autocomplete suggestions with title, poster, format, status, year, and episode count.
|
||||
|
||||
---
|
||||
|
||||
## Filter
|
||||
|
||||
```
|
||||
GET /api/filter
|
||||
```
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `genre` | string | No | — | Genre name (e.g. "Action") |
|
||||
| `tag` | string | No | — | Tag name |
|
||||
| `year` | number | No | — | Release year |
|
||||
| `season` | string | No | — | FALL, WINTER, SPRING, SUMMER |
|
||||
| `format` | string | No | — | TV, MOVIE, OVA, ONA, SPECIAL, MUSIC |
|
||||
| `status` | string | No | — | RELEASING, FINISHED, NOT_YET_RELEASED, CANCELLED |
|
||||
| `sort` | string | No | POPULARITY_DESC | Sort order |
|
||||
| `page` | number | No | 1 | Page number |
|
||||
| `per_page` | number | No | 20 | Results per page |
|
||||
|
||||
**Example:** `GET /api/filter?genre=Action&year=2024&season=WINTER&per_page=3`
|
||||
|
||||
---
|
||||
|
||||
## Trending
|
||||
|
||||
```
|
||||
GET /api/trending
|
||||
```
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `per_page` | number | No | 20 | Results per page |
|
||||
|
||||
**Example:** `GET /api/trending?per_page=5`
|
||||
|
||||
---
|
||||
|
||||
## Popular
|
||||
|
||||
```
|
||||
GET /api/popular
|
||||
```
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `per_page` | number | No | 20 | Results per page |
|
||||
|
||||
---
|
||||
|
||||
## Upcoming
|
||||
|
||||
```
|
||||
GET /api/upcoming
|
||||
```
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `per_page` | number | No | 20 | Results per page |
|
||||
|
||||
---
|
||||
|
||||
## Recent
|
||||
|
||||
```
|
||||
GET /api/recent
|
||||
```
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `per_page` | number | No | 20 | Results per page |
|
||||
|
||||
---
|
||||
|
||||
## Spotlight
|
||||
|
||||
```
|
||||
GET /api/spotlight
|
||||
```
|
||||
|
||||
Returns featured/spotlight anime for the hero carousel.
|
||||
|
||||
---
|
||||
|
||||
## Schedule
|
||||
|
||||
```
|
||||
GET /api/schedule
|
||||
```
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `date` | string | No | today | Date in YYYY-MM-DD format |
|
||||
|
||||
---
|
||||
|
||||
## Anime Info
|
||||
|
||||
```
|
||||
GET /api/info/:id
|
||||
```
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | number | Yes | AniList anime ID |
|
||||
|
||||
**Example:** `GET /api/info/20`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"results": {
|
||||
"id": 20,
|
||||
"idMal": 20,
|
||||
"title": { "romaji": "NARUTO", "english": "Naruto", "native": "NARUTO -ナルト-" },
|
||||
"description": "Naruto Uzumaki, a hyperactive and knuckle-headed ninja...",
|
||||
"coverImage": { "large": "https://s4.anilist.co/file/..." },
|
||||
"format": "TV",
|
||||
"episodes": 220,
|
||||
"duration": 23,
|
||||
"status": "FINISHED",
|
||||
"averageScore": 80,
|
||||
"genres": ["Action","Adventure","Comedy","Drama","Fantasy","Supernatural"],
|
||||
"studios": [{ "name": "Studio Pierrot", "isAnimationStudio": true }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Characters
|
||||
|
||||
```
|
||||
GET /api/anime/:id/characters
|
||||
```
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | number | Yes | AniList anime ID |
|
||||
|
||||
**Example:** `GET /api/anime/20/characters`
|
||||
|
||||
Returns characters with voice actors.
|
||||
|
||||
---
|
||||
|
||||
## Relations
|
||||
|
||||
```
|
||||
GET /api/anime/:id/relations
|
||||
```
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | number | Yes | AniList anime ID |
|
||||
|
||||
Returns related anime (sequels, prequels, side stories, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
```
|
||||
GET /api/anime/:id/recommendations
|
||||
```
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | number | Yes | AniList anime ID |
|
||||
|
||||
Returns recommended anime based on the given ID.
|
||||
|
||||
---
|
||||
|
||||
## Episodes
|
||||
|
||||
```
|
||||
GET /api/episodes/:id
|
||||
```
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | number | Yes | AniList anime ID |
|
||||
|
||||
**Example:** `GET /api/episodes/20`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"results": {
|
||||
"providers": {
|
||||
"kiwi": {
|
||||
"meta": { "id": "1571", "title": "Naruto", "type": "TV" },
|
||||
"episodes": {
|
||||
"sub": [
|
||||
{
|
||||
"id": "watch/kiwi/20/sub/animepahe-1",
|
||||
"number": 1,
|
||||
"title": "Enter: Naruto Uzumaki!",
|
||||
"airDate": "2002-10-03",
|
||||
"audio": "sub",
|
||||
"filler": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Watch (Streaming)
|
||||
|
||||
```
|
||||
GET /api/watch/:provider/:anilistId/:category/:slug
|
||||
```
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `provider` | string | Yes | Provider name (kiwi, pewe, etc.) |
|
||||
| `anilistId` | number | Yes | AniList anime ID |
|
||||
| `category` | string | Yes | sub or dub |
|
||||
| `slug` | string | Yes | Episode slug from episodes response |
|
||||
|
||||
**Example:** `GET /api/watch/kiwi/20/sub/animepahe-1`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"results": {
|
||||
"streams": [
|
||||
{
|
||||
"url": "https://vault-01.uwucdn.top/stream/.../uwu.m3u8",
|
||||
"type": "hls",
|
||||
"quality": "360p",
|
||||
"resolution": { "width": 640, "height": 360 },
|
||||
"codec": "h264",
|
||||
"audio": "sub",
|
||||
"fansub": "df68",
|
||||
"referer": "https://kwik.cx/e/..."
|
||||
}
|
||||
],
|
||||
"download": "https://pahe.win/LJmbA"
|
||||
}
|
||||
}
|
||||
```
|
||||
> 📝 All endpoints return `{ "success": true, "results": {...} }` on success and `{ "success": false, "message": "..." }` on error.
|
||||
>
|
||||
> 🎬 Creator attribution is injected into every response: `{ "creator": "Shinei Nouzen", "github": "...", "telegram": "...", "message": "...", "timestamp": "..." }`
|
||||
|
||||
Generated
+1480
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "miruro-api",
|
||||
"version": "1.3.0",
|
||||
"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": {
|
||||
@@ -9,10 +9,11 @@
|
||||
"test": "node test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
"axios": "^1.8.0",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.0"
|
||||
"dotenv": "^16.4.0",
|
||||
"express": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.0"
|
||||
|
||||
+6
-6
@@ -10,7 +10,7 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://mirurotvapi.vercel.app" />
|
||||
<meta property="og:image" content="https://mirurotvapi.vercel.app/og-image.png" />
|
||||
<meta property="og:description" content="Free RESTful API for anime streaming data. 18 endpoints, 12 providers, M3U8 streaming URLs. Powered by AniList GraphQL and Miruro." />
|
||||
<meta property="og:description" content="Free RESTful API for anime streaming data. 35 endpoints, 12 providers, M3U8 streaming URLs. Powered by AniList GraphQL and Miruro." />
|
||||
<meta property="og:site_name" content="MiruroAPI" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
<meta name="twitter:site" content="@Shineii86" />
|
||||
<meta name="twitter:creator" content="@Shineii86" />
|
||||
<meta name="twitter:title" content="MiruroAPI — Free Anime Streaming API" />
|
||||
<meta name="twitter:description" content="Free RESTful API for anime streaming data. 18 endpoints, 12 providers, M3U8 streaming URLs." />
|
||||
<meta name="twitter:description" content="Free RESTful API for anime streaming data. 35 endpoints, 12 providers, M3U8 streaming URLs." />
|
||||
<meta name="twitter:image" content="https://mirurotvapi.vercel.app/og-image.png" />
|
||||
<meta name="twitter:image:alt" content="MiruroAPI - Free Anime Streaming API" />
|
||||
<meta name="twitter:domain" content="mirurotvapi.vercel.app" />
|
||||
|
||||
<!-- ──────────────── Generic Meta ──────────────── -->
|
||||
<meta name="description" content="Free RESTful API for anime streaming data. 18 endpoints, 12 providers, M3U8 streaming URLs. Powered by AniList GraphQL and Miruro." />
|
||||
<meta name="description" content="Free RESTful API for anime streaming data. 35 endpoints, 12 providers, M3U8 streaming URLs. Powered by AniList GraphQL and Miruro." />
|
||||
<meta name="author" content="Shinei Nouzen" />
|
||||
<meta name="keywords" content="anime api, free api, rest api, anime streaming, m3u8, anime data, anilist, miruro, episode streaming, sub, dub" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
@@ -278,7 +278,7 @@
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebAPI",
|
||||
"name": "MiruroAPI",
|
||||
"description": "Free RESTful API for anime streaming data. 18 endpoints, 12 providers, M3U8 streaming URLs.",
|
||||
"description": "Free RESTful API for anime streaming data. 35 endpoints, 12 providers, M3U8 streaming URLs.",
|
||||
"url": "https://mirurotvapi.vercel.app",
|
||||
"image": "https://mirurotvapi.vercel.app/og-image.png",
|
||||
"documentation": "https://mirurotvapi.vercel.app/docs",
|
||||
@@ -326,7 +326,7 @@
|
||||
<section class="hero container">
|
||||
<div class="hero-badge"><span class="dot"></span> Free · No API key · Open source</div>
|
||||
<h1>The <span class="grad">Anime Streaming</span> API<br/>for Your Next Project</h1>
|
||||
<p class="hero-sub">Search, discover, and stream anime with M3U8 URLs. Powered by AniList GraphQL + Miruro providers. 18 endpoints, 12 providers, zero config.</p>
|
||||
<p class="hero-sub">Search, discover, and stream anime with M3U8 URLs. Powered by AniList GraphQL + Miruro providers. 35 endpoints, 12 providers, zero config.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="#playground" class="btn btn-primary">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
@@ -370,7 +370,7 @@ fetch("https://mirurotvapi.vercel.app/api/search?query=naruto")
|
||||
<section class="stats">
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="reveal"><div class="stat-value grad" data-count="18">0</div><div class="stat-label">API Endpoints</div></div>
|
||||
<div class="reveal"><div class="stat-value grad" data-count="35">0</div><div class="stat-label">API Endpoints</div></div>
|
||||
<div class="reveal reveal-delay-1"><div class="stat-value grad" data-count="12">0</div><div class="stat-label">Streaming Providers</div></div>
|
||||
<div class="reveal reveal-delay-2"><div class="stat-value grad" data-count="5000">0</div><div class="stat-label">Anime Indexed</div></div>
|
||||
<div class="reveal reveal-delay-3"><div class="stat-value grad">M3U8</div><div class="stat-label">Stream Format</div></div>
|
||||
|
||||
+315
-118
@@ -3,7 +3,7 @@
|
||||
"info": {
|
||||
"title": "MiruroAPI",
|
||||
"description": "Free RESTful API for anime streaming data. Powered by AniList GraphQL and Miruro streaming providers.",
|
||||
"version": "1.1.0",
|
||||
"version": "2.0.1",
|
||||
"contact": {
|
||||
"name": "Shineii86",
|
||||
"url": "https://github.com/Shineii86/MiruroAPI"
|
||||
@@ -18,230 +18,427 @@
|
||||
"description": "Production"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{ "name": "Search", "description": "Search, filter, suggestions" },
|
||||
{ "name": "Collections", "description": "Trending, popular, top, schedule" },
|
||||
{ "name": "Seasonal & Studio", "description": "Seasonal anime, studio filter" },
|
||||
{ "name": "Genres & Tags", "description": "Genre, year, status, format filters" },
|
||||
{ "name": "Character & Staff", "description": "Character/staff details and search" },
|
||||
{ "name": "Anime Details", "description": "Info, characters, relations, recommendations" },
|
||||
{ "name": "Streaming", "description": "Episodes, sources, stream, download" },
|
||||
{ "name": "Utility", "description": "Compare, stats, calendar, timeline" },
|
||||
{ "name": "System", "description": "Health, stats, OpenAPI" }
|
||||
],
|
||||
"paths": {
|
||||
"/api/health": {
|
||||
"get": {
|
||||
"tags": ["System"],
|
||||
"summary": "Health check",
|
||||
"operationId": "health",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "API is healthy",
|
||||
"content": { "application/json": { "schema": { "type": "object" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/search": {
|
||||
"get": {
|
||||
"tags": ["Search & Discovery"],
|
||||
"tags": ["Search"],
|
||||
"summary": "Search anime by name",
|
||||
"operationId": "searchAnime",
|
||||
"parameters": [
|
||||
{ "name": "query", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Search query" },
|
||||
{ "name": "query", "in": "query", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 50 } }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "Search results with pagination" }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/suggestions": {
|
||||
"get": {
|
||||
"tags": ["Search & Discovery"],
|
||||
"tags": ["Search"],
|
||||
"summary": "Autocomplete suggestions",
|
||||
"operationId": "getSuggestions",
|
||||
"parameters": [
|
||||
{ "name": "query", "in": "query", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "Lightweight suggestions (max 8)" }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/filter": {
|
||||
"get": {
|
||||
"tags": ["Search & Discovery"],
|
||||
"summary": "Advanced filter / browse",
|
||||
"operationId": "filterAnime",
|
||||
"tags": ["Search"],
|
||||
"summary": "Advanced filter",
|
||||
"parameters": [
|
||||
{ "name": "genre", "in": "query", "schema": { "type": "string" }, "description": "e.g. Action, Romance" },
|
||||
{ "name": "tag", "in": "query", "schema": { "type": "string" }, "description": "e.g. Isekai, Time Skip" },
|
||||
{ "name": "genre", "in": "query", "schema": { "type": "string" } },
|
||||
{ "name": "tag", "in": "query", "schema": { "type": "string" } },
|
||||
{ "name": "year", "in": "query", "schema": { "type": "integer" } },
|
||||
{ "name": "season", "in": "query", "schema": { "type": "string", "enum": ["WINTER", "SPRING", "SUMMER", "FALL"] } },
|
||||
{ "name": "format", "in": "query", "schema": { "type": "string", "enum": ["TV", "MOVIE", "OVA", "ONA", "SPECIAL", "MUSIC"] } },
|
||||
{ "name": "status", "in": "query", "schema": { "type": "string", "enum": ["RELEASING", "FINISHED", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"] } },
|
||||
{ "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["SCORE_DESC", "POPULARITY_DESC", "TRENDING_DESC", "START_DATE_DESC", "FAVOURITES_DESC", "UPDATED_AT_DESC"], "default": "POPULARITY_DESC" } },
|
||||
{ "name": "format", "in": "query", "schema": { "type": "string", "enum": ["TV", "MOVIE", "OVA", "ONA", "SPECIAL"] } },
|
||||
{ "name": "status", "in": "query", "schema": { "type": "string", "enum": ["RELEASING", "FINISHED", "NOT_YET_RELEASED"] } },
|
||||
{ "name": "sort", "in": "query", "schema": { "type": "string", "default": "POPULARITY_DESC" } },
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 50 } }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "Filtered results with pagination" }
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20 } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/multi-search": {
|
||||
"get": {
|
||||
"tags": ["Search"],
|
||||
"summary": "Multi-parameter search"
|
||||
}
|
||||
},
|
||||
"/api/trending": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Trending anime",
|
||||
"operationId": "getTrending",
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20 } }
|
||||
],
|
||||
"responses": { "200": { "description": "Trending anime list" } }
|
||||
"summary": "Trending anime"
|
||||
}
|
||||
},
|
||||
"/api/trending/daily": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Daily trending anime"
|
||||
}
|
||||
},
|
||||
"/api/trending/weekly": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Weekly trending anime"
|
||||
}
|
||||
},
|
||||
"/api/popular": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Most popular anime",
|
||||
"operationId": "getPopular",
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20 } }
|
||||
],
|
||||
"responses": { "200": { "description": "Popular anime list" } }
|
||||
"summary": "Most popular anime"
|
||||
}
|
||||
},
|
||||
"/api/top": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Top anime by score"
|
||||
}
|
||||
},
|
||||
"/api/upcoming": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Upcoming anime",
|
||||
"operationId": "getUpcoming",
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20 } }
|
||||
],
|
||||
"responses": { "200": { "description": "Upcoming anime list" } }
|
||||
"summary": "Upcoming anime"
|
||||
}
|
||||
},
|
||||
"/api/recent": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Recently updated anime",
|
||||
"operationId": "getRecent",
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20 } }
|
||||
],
|
||||
"responses": { "200": { "description": "Recent anime list" } }
|
||||
"summary": "Recently aired anime"
|
||||
}
|
||||
},
|
||||
"/api/spotlight": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Spotlight / featured anime",
|
||||
"operationId": "getSpotlight",
|
||||
"responses": { "200": { "description": "Top 10 trending + popular" } }
|
||||
"summary": "Spotlight/featured anime"
|
||||
}
|
||||
},
|
||||
"/api/schedule": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Airing schedule",
|
||||
"operationId": "getSchedule",
|
||||
"summary": "Airing schedule"
|
||||
}
|
||||
},
|
||||
"/api/random": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Random anime"
|
||||
}
|
||||
},
|
||||
"/api/upcoming/countdown": {
|
||||
"get": {
|
||||
"tags": ["Collections"],
|
||||
"summary": "Next 10 episodes airing soon"
|
||||
}
|
||||
},
|
||||
"/api/season/{year}/{season}": {
|
||||
"get": {
|
||||
"tags": ["Seasonal & Studio"],
|
||||
"summary": "Seasonal anime",
|
||||
"parameters": [
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 20 } }
|
||||
],
|
||||
"responses": { "200": { "description": "Upcoming episodes with timestamps" } }
|
||||
{ "name": "year", "in": "path", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "season", "in": "path", "required": true, "schema": { "type": "string", "enum": ["WINTER", "SPRING", "SUMMER", "FALL"] } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/studio/{name}": {
|
||||
"get": {
|
||||
"tags": ["Seasonal & Studio"],
|
||||
"summary": "Anime by studio",
|
||||
"parameters": [
|
||||
{ "name": "name", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/genres": {
|
||||
"get": {
|
||||
"tags": ["Genres & Tags"],
|
||||
"summary": "All genres"
|
||||
}
|
||||
},
|
||||
"/api/tags": {
|
||||
"get": {
|
||||
"tags": ["Genres & Tags"],
|
||||
"summary": "All tags"
|
||||
}
|
||||
},
|
||||
"/api/genre/{name}": {
|
||||
"get": {
|
||||
"tags": ["Genres & Tags"],
|
||||
"summary": "Anime by genre",
|
||||
"parameters": [
|
||||
{ "name": "name", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/year/{year}": {
|
||||
"get": {
|
||||
"tags": ["Genres & Tags"],
|
||||
"summary": "Anime by year",
|
||||
"parameters": [
|
||||
{ "name": "year", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/status/{status}": {
|
||||
"get": {
|
||||
"tags": ["Genres & Tags"],
|
||||
"summary": "Anime by status",
|
||||
"parameters": [
|
||||
{ "name": "status", "in": "path", "required": true, "schema": { "type": "string", "enum": ["RELEASING", "FINISHED", "NOT_YET_RELEASED"] } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/format/{format}": {
|
||||
"get": {
|
||||
"tags": ["Genres & Tags"],
|
||||
"summary": "Anime by format",
|
||||
"parameters": [
|
||||
{ "name": "format", "in": "path", "required": true, "schema": { "type": "string", "enum": ["TV", "MOVIE", "OVA", "ONA", "SPECIAL"] } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/character/{id}": {
|
||||
"get": {
|
||||
"tags": ["Character & Staff"],
|
||||
"summary": "Character detail",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/staff/{id}": {
|
||||
"get": {
|
||||
"tags": ["Character & Staff"],
|
||||
"summary": "Staff detail",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/characters": {
|
||||
"get": {
|
||||
"tags": ["Character & Staff"],
|
||||
"summary": "Search characters",
|
||||
"parameters": [
|
||||
{ "name": "query", "in": "query", "required": true, "schema": { "type": "string" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/staff": {
|
||||
"get": {
|
||||
"tags": ["Character & Staff"],
|
||||
"summary": "Search staff",
|
||||
"parameters": [
|
||||
{ "name": "query", "in": "query", "required": true, "schema": { "type": "string" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/info/{id}": {
|
||||
"get": {
|
||||
"tags": ["Anime Details"],
|
||||
"summary": "Full anime info",
|
||||
"operationId": "getAnimeInfo",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "AniList anime ID" }
|
||||
],
|
||||
"responses": { "200": { "description": "Complete anime metadata, characters, relations, recommendations" } }
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/anime/{id}/characters": {
|
||||
"get": {
|
||||
"tags": ["Anime Details"],
|
||||
"summary": "Characters with voice actors",
|
||||
"operationId": "getCharacters",
|
||||
"summary": "Characters",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 25 } }
|
||||
],
|
||||
"responses": { "200": { "description": "Paginated character list" } }
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/anime/{id}/relations": {
|
||||
"get": {
|
||||
"tags": ["Anime Details"],
|
||||
"summary": "Related anime",
|
||||
"operationId": "getRelations",
|
||||
"summary": "Relations",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
],
|
||||
"responses": { "200": { "description": "Sequels, prequels, side stories, etc." } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/anime/{id}/recommendations": {
|
||||
"get": {
|
||||
"tags": ["Anime Details"],
|
||||
"summary": "Community recommendations",
|
||||
"operationId": "getRecommendations",
|
||||
"summary": "Recommendations",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 10 } }
|
||||
],
|
||||
"responses": { "200": { "description": "Recommended anime sorted by rating" } }
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/episodes/{id}": {
|
||||
"get": {
|
||||
"tags": ["Streaming"],
|
||||
"summary": "Episode list from all providers",
|
||||
"operationId": "getEpisodes",
|
||||
"summary": "Episode list",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "AniList anime ID" }
|
||||
],
|
||||
"responses": { "200": { "description": "Episodes organized by provider and audio type (sub/dub)" } }
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/episodes/{id}/batch": {
|
||||
"get": {
|
||||
"tags": ["Streaming"],
|
||||
"summary": "Batch episode sources",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "provider", "in": "query", "schema": { "type": "string", "default": "kiwi" } },
|
||||
{ "name": "category", "in": "query", "schema": { "type": "string", "default": "sub" } },
|
||||
{ "name": "slugs", "in": "query", "schema": { "type": "string" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/sources": {
|
||||
"get": {
|
||||
"tags": ["Streaming"],
|
||||
"summary": "Streaming sources (detailed)",
|
||||
"operationId": "getSources",
|
||||
"parameters": [
|
||||
{ "name": "episodeId", "in": "query", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "provider", "in": "query", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "anilistId", "in": "query", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "category", "in": "query", "schema": { "type": "string", "enum": ["sub", "dub"], "default": "sub" } }
|
||||
],
|
||||
"responses": { "200": { "description": "M3U8 streams, subtitles, intro/outro timestamps" } }
|
||||
{ "name": "category", "in": "query", "schema": { "type": "string", "default": "sub" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/stream": {
|
||||
"get": {
|
||||
"tags": ["Streaming"],
|
||||
"summary": "Stream with quality fallback",
|
||||
"parameters": [
|
||||
{ "name": "provider", "in": "query", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "anilistId", "in": "query", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "category", "in": "query", "schema": { "type": "string", "default": "sub" } },
|
||||
{ "name": "slug", "in": "query", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "quality", "in": "query", "schema": { "type": "string", "default": "1080p" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/download": {
|
||||
"get": {
|
||||
"tags": ["Streaming"],
|
||||
"summary": "Download URL",
|
||||
"parameters": [
|
||||
{ "name": "provider", "in": "query", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "anilistId", "in": "query", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "category", "in": "query", "schema": { "type": "string", "default": "sub" } },
|
||||
{ "name": "slug", "in": "query", "required": true, "schema": { "type": "string" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/watch/{provider}/{anilistId}/{category}/{slug}": {
|
||||
"get": {
|
||||
"tags": ["Streaming"],
|
||||
"summary": "Streaming sources (simple slug-based)",
|
||||
"operationId": "getWatchSources",
|
||||
"summary": "Streaming sources (simple) with subtitles",
|
||||
"description": "Returns streaming sources, subtitles (if available), and best stream with quality fallback",
|
||||
"parameters": [
|
||||
{ "name": "provider", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "anilistId", "in": "path", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "category", "in": "path", "required": true, "schema": { "type": "string", "enum": ["sub", "dub"] } },
|
||||
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "e.g. animepahe-1" }
|
||||
{ "name": "category", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": { "200": { "description": "M3U8 streams, subtitles, intro/outro timestamps" } }
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{ "name": "System", "description": "Health & status" },
|
||||
{ "name": "Search & Discovery", "description": "Search, suggestions, and advanced filtering" },
|
||||
{ "name": "Collections", "description": "Trending, popular, upcoming, recent, spotlight, schedule" },
|
||||
{ "name": "Anime Details", "description": "Full anime info, characters, relations, recommendations" },
|
||||
{ "name": "Streaming", "description": "Episode lists and M3U8 streaming sources" }
|
||||
"bestStream": { "type": "object" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/compare/{id1}/{id2}": {
|
||||
"get": {
|
||||
"tags": ["Utility"],
|
||||
"summary": "Compare two anime",
|
||||
"parameters": [
|
||||
{ "name": "id1", "in": "path", "required": true, "schema": { "type": "integer" } },
|
||||
{ "name": "id2", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/stats/genre": {
|
||||
"get": {
|
||||
"tags": ["Utility"],
|
||||
"summary": "Genre statistics"
|
||||
}
|
||||
},
|
||||
"/api/calendar": {
|
||||
"get": {
|
||||
"tags": ["Utility"],
|
||||
"summary": "Monthly airing calendar",
|
||||
"parameters": [
|
||||
{ "name": "year", "in": "query", "schema": { "type": "integer" } },
|
||||
{ "name": "month", "in": "query", "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/timeline/{id}": {
|
||||
"get": {
|
||||
"tags": ["Utility"],
|
||||
"summary": "Anime timeline",
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/health": {
|
||||
"get": {
|
||||
"tags": ["System"],
|
||||
"summary": "Health check"
|
||||
}
|
||||
},
|
||||
"/api/stats": {
|
||||
"get": {
|
||||
"tags": ["System"],
|
||||
"summary": "API statistics"
|
||||
}
|
||||
},
|
||||
"/api/openapi": {
|
||||
"get": {
|
||||
"tags": ["System"],
|
||||
"summary": "OpenAPI spec"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
*
|
||||
* @description
|
||||
* Main entry point for the MiruroAPI Express server.
|
||||
* Configures CORS, middleware, static files, Swagger docs,
|
||||
* API routes, and 404 handling. Starts the server on the
|
||||
* configured port.
|
||||
* Configures CORS, compression, logging, security headers,
|
||||
* rate limiting, API routes, and 404 handling.
|
||||
*
|
||||
* @exports
|
||||
* None (side-effect: starts Express server)
|
||||
@@ -19,7 +18,9 @@
|
||||
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const compression = require("compression");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
require("dotenv").config();
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
@@ -28,15 +29,63 @@ require("dotenv").config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const startTime = Date.now();
|
||||
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
||||
? process.env.ALLOWED_ORIGINS.split(",")
|
||||
: null;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// COMPRESSION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Gzip compression — reduces response size by 30-70% ----
|
||||
app.use(compression({
|
||||
level: 6,
|
||||
threshold: 1024,
|
||||
filter: (req, res) => {
|
||||
if (req.headers["x-no-compression"]) return false;
|
||||
return compression.filter(req, res);
|
||||
},
|
||||
}));
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// REQUEST LOGGING
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Request logging — method, path, status, response time ----
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const originalEnd = res.end;
|
||||
|
||||
res.end = function (...args) {
|
||||
const duration = Date.now() - start;
|
||||
const status = res.statusCode;
|
||||
const ip =
|
||||
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
||||
req.ip ||
|
||||
req.connection.remoteAddress;
|
||||
|
||||
// NOTE: Only log API requests, skip static files
|
||||
if (req.path.startsWith("/api/")) {
|
||||
const log = `[${new Date().toISOString()}] ${req.method} ${req.path} ${status} ${duration}ms ${ip}`;
|
||||
if (status >= 400) {
|
||||
console.error(log);
|
||||
} else {
|
||||
console.log(log);
|
||||
}
|
||||
}
|
||||
|
||||
originalEnd.apply(this, args);
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CORS MIDDLEWARE
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// NOTE: Single unified CORS middleware — handles all origin validation
|
||||
// ---- FEATURE: Unified CORS middleware with preflight handling ----
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
if (
|
||||
@@ -46,36 +95,63 @@ app.use((req, res, next) => {
|
||||
) {
|
||||
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
||||
}
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-No-Compression");
|
||||
res.setHeader("Access-Control-Max-Age", "86400");
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return res.status(204).end();
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SECURITY HEADERS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Security headers ----
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader("X-Frame-Options", "DENY");
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
res.setHeader("X-XSS-Protection", "1; mode=block");
|
||||
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
||||
next();
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CACHE HEADERS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Cache-Control headers for API responses ----
|
||||
app.use("/api", (req, res, next) => {
|
||||
// NOTE: Different cache durations based on endpoint type
|
||||
if (req.path.includes("/health") || req.path.includes("/stats")) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
} else if (req.path.includes("/search") || req.path.includes("/suggestions")) {
|
||||
res.setHeader("Cache-Control", "public, max-age=60");
|
||||
} else if (req.path.includes("/episodes") || req.path.includes("/sources") || req.path.includes("/watch")) {
|
||||
res.setHeader("Cache-Control", "public, max-age=120");
|
||||
} else {
|
||||
res.setHeader("Cache-Control", "public, max-age=300");
|
||||
}
|
||||
res.setHeader("Vary", "Accept-Encoding");
|
||||
next();
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// STATIC FILES
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
app.use(express.static(path.join(__dirname, "public"), { redirect: false }));
|
||||
app.use(express.static(path.join(__dirname, "public"), {
|
||||
redirect: false,
|
||||
maxAge: "1d",
|
||||
}));
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CLEAN URL ROUTES
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Clean URL routes (no .html extension) ----
|
||||
const fs = require("fs");
|
||||
const publicDir = path.join(__dirname, "public");
|
||||
|
||||
app.get("/tos", (req, res) => {
|
||||
@@ -90,7 +166,6 @@ app.get("/privacy", (req, res) => {
|
||||
// SWAGGER UI DOCS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Interactive Swagger UI documentation at /docs ----
|
||||
app.get("/docs", (req, res) => {
|
||||
res.sendFile(path.join(publicDir, "docs.html"));
|
||||
});
|
||||
@@ -99,25 +174,9 @@ app.get("/docs", (req, res) => {
|
||||
// RESPONSE HELPERS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Standardized JSON response wrapper ----
|
||||
/**
|
||||
* Wraps data in a standardized success JSON response.
|
||||
*
|
||||
* @param {object} res - Express response object
|
||||
* @param {*} data - The data to return in the response
|
||||
* @param {number} status - HTTP status code (default: 200)
|
||||
*/
|
||||
const jsonResponse = (res, data, status = 200) =>
|
||||
res.status(status).json({ success: true, results: data });
|
||||
|
||||
// ---- FEATURE: Standardized error response wrapper ----
|
||||
/**
|
||||
* Returns a standardized error JSON response.
|
||||
*
|
||||
* @param {object} res - Express response object
|
||||
* @param {string} message - Error message to return
|
||||
* @param {number} status - HTTP status code (default: 500)
|
||||
*/
|
||||
const jsonError = (res, message = "Internal server error", status = 500) =>
|
||||
res.status(status).json({ success: false, message });
|
||||
|
||||
@@ -128,7 +187,10 @@ const jsonError = (res, message = "Internal server error", status = 500) =>
|
||||
// ---- FEATURE: Rate limiting (100 requests per minute per IP) ----
|
||||
const requestCounts = new Map();
|
||||
app.use((req, res, next) => {
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const ip =
|
||||
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
||||
req.ip ||
|
||||
req.connection.remoteAddress;
|
||||
const now = Date.now();
|
||||
const windowMs = 60000;
|
||||
const maxRequests = 100;
|
||||
@@ -148,6 +210,16 @@ app.use((req, res, next) => {
|
||||
}
|
||||
|
||||
timestamps.push(now);
|
||||
|
||||
// NOTE: Periodic cleanup — prune expired IP entries when map grows large
|
||||
if (requestCounts.size > 200) {
|
||||
for (const [key, val] of requestCounts) {
|
||||
if (val.filter((t) => now - t < windowMs).length === 0) {
|
||||
requestCounts.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -156,7 +228,7 @@ app.use((req, res, next) => {
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
const apiRoutes = require("./src/routes/apiRoutes");
|
||||
app.use("/api", apiRoutes(jsonResponse, jsonError));
|
||||
app.use("/api", apiRoutes(jsonResponse, jsonError, startTime));
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// 404 HANDLER
|
||||
@@ -164,7 +236,7 @@ app.use("/api", apiRoutes(jsonResponse, jsonError));
|
||||
|
||||
// ---- FEATURE: Global error handler ----
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
console.error(`[ERROR] ${err.message}`);
|
||||
res.status(err.status || 500).json({
|
||||
success: false,
|
||||
message: err.message || "Internal server error",
|
||||
@@ -191,7 +263,7 @@ app.use((req, res) => {
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.info(`MiruroAPI listening at ${PORT}`);
|
||||
console.log(`[${new Date().toISOString()}] MiruroAPI v2.0.0 listening at ${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -584,6 +584,341 @@ const filterAnime = async ({
|
||||
};
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// GENRES & CATEGORIES
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Get all available genres ----
|
||||
/**
|
||||
* Returns the complete list of AniList genres.
|
||||
*
|
||||
* @returns {Promise<Array>} Array of genre name strings
|
||||
*/
|
||||
const getAllGenres = async () => {
|
||||
const gql = `query { GenreCollection }`;
|
||||
const data = await anilistQuery(gql);
|
||||
return data.GenreCollection;
|
||||
};
|
||||
|
||||
// ---- FEATURE: Get all available tags ----
|
||||
/**
|
||||
* Returns the complete list of AniList tags with metadata.
|
||||
*
|
||||
* @returns {Promise<Array>} Array of tag objects { name, description, category, rank, isGeneralSpoiler, isMediaSpoiler, isAdult }
|
||||
*/
|
||||
const getAllTags = async () => {
|
||||
const gql = `query { TagCollection { name description category rank isGeneralSpoiler isMediaSpoiler isAdult } }`;
|
||||
const data = await anilistQuery(gql);
|
||||
return data.TagCollection;
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TOP & TRENDING
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Top anime by score (all time) ----
|
||||
/**
|
||||
* Fetches top-rated anime of all time sorted by score.
|
||||
*
|
||||
* @param {number} [page=1] - Page number
|
||||
* @param {number} [perPage=20] - Results per page
|
||||
* @returns {Promise<object>} Paginated top anime results
|
||||
*/
|
||||
const getTopAnime = async (page = 1, perPage = 20) => {
|
||||
const gql = `
|
||||
query ($page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
media(type: ANIME, sort: SCORE_DESC) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { page, perPage });
|
||||
const pageData = data.Page;
|
||||
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.media,
|
||||
};
|
||||
};
|
||||
|
||||
// ---- FEATURE: Daily trending anime ----
|
||||
/**
|
||||
* Fetches currently trending anime (last 7 days).
|
||||
*
|
||||
* @param {number} [page=1] - Page number
|
||||
* @param {number} [perPage=20] - Results per page
|
||||
* @returns {Promise<object>} Paginated trending results
|
||||
*/
|
||||
const getTrendingDaily = async (page = 1, perPage = 20) => {
|
||||
const gql = `
|
||||
query ($page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
media(type: ANIME, sort: TRENDING_DESC, trending: 7) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { page, perPage });
|
||||
const pageData = data.Page;
|
||||
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.media,
|
||||
};
|
||||
};
|
||||
|
||||
// ---- FEATURE: Weekly trending anime ----
|
||||
/**
|
||||
* Fetches currently trending anime (last 30 days).
|
||||
*
|
||||
* @param {number} [page=1] - Page number
|
||||
* @param {number} [perPage=20] - Results per page
|
||||
* @returns {Promise<object>} Paginated trending results
|
||||
*/
|
||||
const getTrendingWeekly = async (page = 1, perPage = 20) => {
|
||||
const gql = `
|
||||
query ($page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
media(type: ANIME, sort: TRENDING_DESC, trending: 30) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { page, perPage });
|
||||
const pageData = data.Page;
|
||||
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.media,
|
||||
};
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEASONAL & STUDIO
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Seasonal anime by year and season ----
|
||||
/**
|
||||
* Fetches anime for a specific season and year.
|
||||
*
|
||||
* @param {number} year - The year (e.g., 2025)
|
||||
* @param {string} season - The season (WINTER, SPRING, SUMMER, FALL)
|
||||
* @param {number} [page=1] - Page number
|
||||
* @param {number} [perPage=20] - Results per page
|
||||
* @returns {Promise<object>} Paginated seasonal anime results
|
||||
*/
|
||||
const getSeasonalAnime = async (year, season, page = 1, perPage = 20) => {
|
||||
const gql = `
|
||||
query ($year: Int, $season: MediaSeason, $page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
media(type: ANIME, season: $season, seasonYear: $year, sort: POPULARITY_DESC) {
|
||||
${MEDIA_LIST_FIELDS}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { year, season: season.toUpperCase(), page, perPage });
|
||||
const pageData = data.Page;
|
||||
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.media,
|
||||
};
|
||||
};
|
||||
|
||||
// ---- FEATURE: Anime by studio name ----
|
||||
/**
|
||||
* Fetches all anime produced by a specific studio.
|
||||
*
|
||||
* @param {string} studioName - The studio name to search for
|
||||
* @param {number} [page=1] - Page number
|
||||
* @param {number} [perPage=20] - Results per page
|
||||
* @returns {Promise<object>} Paginated studio anime results
|
||||
*/
|
||||
const getAnimeByStudio = async (studioName, page = 1, perPage = 20) => {
|
||||
const gql = `
|
||||
query ($name: String, $page: Int, $perPage: Int) {
|
||||
Studio(search: $name) {
|
||||
id name isAnimationStudio siteUrl
|
||||
media(type: ANIME, sort: POPULARITY_DESC, page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
nodes { ${MEDIA_LIST_FIELDS} }
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { name: studioName, page, perPage });
|
||||
const studio = data.Studio;
|
||||
|
||||
if (!studio) throw new Error("Studio not found");
|
||||
|
||||
return {
|
||||
studio: {
|
||||
id: studio.id,
|
||||
name: studio.name,
|
||||
isAnimationStudio: studio.isAnimationStudio,
|
||||
siteUrl: studio.siteUrl,
|
||||
},
|
||||
page: studio.media.pageInfo.currentPage,
|
||||
perPage: studio.media.pageInfo.perPage,
|
||||
total: studio.media.pageInfo.total,
|
||||
hasNextPage: studio.media.pageInfo.hasNextPage,
|
||||
results: studio.media.nodes,
|
||||
};
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CHARACTER & STAFF
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Character detail with anime list ----
|
||||
/**
|
||||
* Fetches detailed character info including all anime appearances.
|
||||
*
|
||||
* @param {number} id - AniList character ID
|
||||
* @returns {Promise<object>} Character details with anime list
|
||||
*/
|
||||
const getCharacterInfo = async (id) => {
|
||||
const gql = `
|
||||
query ($id: Int) {
|
||||
Character(id: $id) {
|
||||
id name { full native userPreferred }
|
||||
image { large medium }
|
||||
description gender favourites siteUrl
|
||||
dateOfBirth { year month day }
|
||||
dateOfDeath { year month day }
|
||||
age bloodType
|
||||
media(perPage: 25, sort: POPULARITY_DESC) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
edges {
|
||||
role
|
||||
node { id title { romaji english } coverImage { large } format episodes status meanScore }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { id });
|
||||
if (!data.Character) throw new Error("Character not found");
|
||||
return data.Character;
|
||||
};
|
||||
|
||||
// ---- FEATURE: Staff detail with anime list ----
|
||||
/**
|
||||
* Fetches detailed staff info including all anime works.
|
||||
*
|
||||
* @param {number} id - AniList staff ID
|
||||
* @returns {Promise<object>} Staff details with anime list
|
||||
*/
|
||||
const getStaffInfo = async (id) => {
|
||||
const gql = `
|
||||
query ($id: Int) {
|
||||
Staff(id: $id) {
|
||||
id name { full native userPreferred }
|
||||
image { large medium }
|
||||
description gender favourites siteUrl
|
||||
dateOfBirth { year month day }
|
||||
dateOfDeath { year month day }
|
||||
age homeTown yearsActive
|
||||
staffMedia(perPage: 25, sort: POPULARITY_DESC) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
edges {
|
||||
role
|
||||
node { id title { romaji english } coverImage { large } format episodes status meanScore }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { id });
|
||||
if (!data.Staff) throw new Error("Staff not found");
|
||||
return data.Staff;
|
||||
};
|
||||
|
||||
// ---- FEATURE: Search characters by name ----
|
||||
/**
|
||||
* Searches AniList characters by name.
|
||||
*
|
||||
* @param {string} query - Character name to search
|
||||
* @param {number} [page=1] - Page number
|
||||
* @param {number} [perPage=20] - Results per page
|
||||
* @returns {Promise<object>} Paginated character search results
|
||||
*/
|
||||
const searchCharacters = async (query, page = 1, perPage = 20) => {
|
||||
const gql = `
|
||||
query ($search: String, $page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
characters(search: $search, sort: SEARCH_MATCH) {
|
||||
id name { full native userPreferred } image { large medium } gender favourites siteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { search: query, page, perPage });
|
||||
const pageData = data.Page;
|
||||
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.characters,
|
||||
};
|
||||
};
|
||||
|
||||
// ---- FEATURE: Search staff by name ----
|
||||
/**
|
||||
* Searches AniList staff by name.
|
||||
*
|
||||
* @param {string} query - Staff name to search
|
||||
* @param {number} [page=1] - Page number
|
||||
* @param {number} [perPage=20] - Results per page
|
||||
* @returns {Promise<object>} Paginated staff search results
|
||||
*/
|
||||
const searchStaff = async (query, page = 1, perPage = 20) => {
|
||||
const gql = `
|
||||
query ($search: String, $page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo { total currentPage lastPage hasNextPage perPage }
|
||||
staff(search: $search, sort: SEARCH_MATCH) {
|
||||
id name { full native userPreferred } image { large medium } gender favourites siteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await anilistQuery(gql, { search: query, page, perPage });
|
||||
const pageData = data.Page;
|
||||
|
||||
return {
|
||||
page: pageData.pageInfo.currentPage,
|
||||
perPage: pageData.pageInfo.perPage,
|
||||
total: pageData.pageInfo.total,
|
||||
hasNextPage: pageData.pageInfo.hasNextPage,
|
||||
results: pageData.staff,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
anilistQuery,
|
||||
searchAnime,
|
||||
@@ -596,6 +931,17 @@ module.exports = {
|
||||
getSpotlight,
|
||||
getSchedule,
|
||||
filterAnime,
|
||||
getAllGenres,
|
||||
getAllTags,
|
||||
getTopAnime,
|
||||
getTrendingDaily,
|
||||
getTrendingWeekly,
|
||||
getSeasonalAnime,
|
||||
getAnimeByStudio,
|
||||
getCharacterInfo,
|
||||
getStaffInfo,
|
||||
searchCharacters,
|
||||
searchStaff,
|
||||
MEDIA_LIST_FIELDS,
|
||||
MEDIA_FULL_FIELDS,
|
||||
};
|
||||
|
||||
+18
-1
@@ -104,6 +104,15 @@ const setCache = (key, data, ttl = DEFAULT_TTL) => {
|
||||
cache.delete(firstKey);
|
||||
}
|
||||
|
||||
// NOTE: Periodic cleanup — prune all expired entries when cache reaches 80% capacity
|
||||
// Prevents unbounded memory growth from stale entries that were never accessed
|
||||
if (cache.size >= MAX_CACHE_SIZE * 0.8) {
|
||||
const now = Date.now();
|
||||
for (const [k, v] of cache) {
|
||||
if (now > v.expiry) cache.delete(k);
|
||||
}
|
||||
}
|
||||
|
||||
cache.set(key, { data, expiry: Date.now() + ttl });
|
||||
};
|
||||
|
||||
@@ -120,6 +129,14 @@ const clearCache = () => {
|
||||
cache.clear();
|
||||
};
|
||||
|
||||
module.exports = { getCached, setCache, clearCache };
|
||||
// ---- FEATURE: Get current cache size ----
|
||||
/**
|
||||
* Returns the current number of entries in the cache.
|
||||
*
|
||||
* @returns {number} Current cache size
|
||||
*/
|
||||
const getCacheSize = () => cache.size;
|
||||
|
||||
module.exports = { getCached, setCache, clearCache, getCacheSize };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: cache.js
|
||||
|
||||
@@ -320,10 +320,192 @@ const getWatchSources = async (provider, anilistId, category, slug) => {
|
||||
return getSources(targetId, provider, anilistId, category);
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SUBTITLE EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract subtitle URLs from streaming sources ----
|
||||
/**
|
||||
* Extracts and formats subtitle URLs from the pipe sources response.
|
||||
* 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
|
||||
* @returns {Array} Array of subtitle objects { url, label, language, kind, format, isDefault }
|
||||
*/
|
||||
const extractSubtitles = (sources) => {
|
||||
const raw = sources.subtitles || sources.captions || [];
|
||||
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
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",
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// QUALITY FALLBACK
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Get best available stream with quality fallback ----
|
||||
/**
|
||||
* Returns the best available HLS stream with automatic quality fallback.
|
||||
* Tries 1080p → 720p → 480p → 360p in order.
|
||||
* Falls back to first HLS stream if no quality match.
|
||||
*
|
||||
* @param {object} sources - The sources response
|
||||
* @param {string} [preferredQuality="1080p"] - Preferred quality
|
||||
* @returns {object|null} Best available stream object or null
|
||||
*/
|
||||
const getBestStream = (sources, preferredQuality = "1080p") => {
|
||||
const streams = (sources.streams || []).filter((s) => s.type === "hls" && s.url);
|
||||
if (streams.length === 0) return null;
|
||||
|
||||
// NOTE: Try preferred quality first, then fallback
|
||||
const qualityOrder = ["1080p", "720p", "480p", "360p"];
|
||||
const startIdx = qualityOrder.indexOf(preferredQuality);
|
||||
const ordered = startIdx >= 0
|
||||
? qualityOrder.slice(startIdx)
|
||||
: qualityOrder;
|
||||
|
||||
for (const q of ordered) {
|
||||
const match = streams.find((s) => s.quality === q);
|
||||
if (match) return 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];
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// DOWNLOAD URL
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Get download URL for an episode ----
|
||||
/**
|
||||
* Fetches the download URL for a specific episode from the pipe.
|
||||
* Returns the direct download link, subtitles, and best stream if available.
|
||||
*
|
||||
* @param {string} provider - Provider name
|
||||
* @param {number} anilistId - The AniList anime ID
|
||||
* @param {string} category - Audio category ("sub" or "dub")
|
||||
* @param {string} slug - Episode slug (e.g., "animepahe-1")
|
||||
* @returns {Promise<object>} Object with download URL, subtitles, best stream, and metadata
|
||||
*/
|
||||
const getDownloadUrl = async (provider, anilistId, category, slug) => {
|
||||
const sources = await getWatchSources(provider, anilistId, category, slug);
|
||||
const subtitles = extractSubtitles(sources);
|
||||
const bestStream = getBestStream(sources);
|
||||
|
||||
return {
|
||||
download: sources.download || null,
|
||||
subtitles,
|
||||
bestStream,
|
||||
provider,
|
||||
anilistId,
|
||||
category,
|
||||
slug,
|
||||
hasDownload: !!sources.download,
|
||||
hasSubtitles: subtitles.length > 0,
|
||||
};
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// BATCH EPISODES
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Get sources for multiple episodes at once ----
|
||||
/**
|
||||
* Fetches streaming sources for multiple episodes in parallel.
|
||||
* Useful for preloading next episodes or batch downloading.
|
||||
*
|
||||
* @param {string} provider - Provider name
|
||||
* @param {number} anilistId - The AniList anime ID
|
||||
* @param {string} category - Audio category ("sub" or "dub")
|
||||
* @param {string[]} slugs - Array of episode slugs (e.g., ["animepahe-1", "animepahe-2"])
|
||||
* @returns {Promise<object>} Object mapping slug → sources (with errors for failed ones)
|
||||
*/
|
||||
const getBatchSources = async (provider, anilistId, category, slugs) => {
|
||||
const results = {};
|
||||
|
||||
const fetchOne = async (slug) => {
|
||||
try {
|
||||
const sources = await getWatchSources(provider, anilistId, category, slug);
|
||||
const best = getBestStream(sources);
|
||||
const subtitles = extractSubtitles(sources);
|
||||
results[slug] = {
|
||||
success: true,
|
||||
streams: sources.streams,
|
||||
bestStream: best,
|
||||
subtitles,
|
||||
download: sources.download || null,
|
||||
};
|
||||
} catch (err) {
|
||||
results[slug] = {
|
||||
success: false,
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: Fetch up to 5 episodes in parallel to avoid rate limiting
|
||||
const batchSize = 5;
|
||||
for (let i = 0; i < slugs.length; i += batchSize) {
|
||||
const batch = slugs.slice(i, i + batchSize);
|
||||
await Promise.all(batch.map(fetchOne));
|
||||
}
|
||||
|
||||
return {
|
||||
provider,
|
||||
anilistId,
|
||||
category,
|
||||
total: slugs.length,
|
||||
successful: Object.values(results).filter((r) => r.success).length,
|
||||
failed: Object.values(results).filter((r) => !r.success).length,
|
||||
episodes: results,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getEpisodes,
|
||||
getSources,
|
||||
getWatchSources,
|
||||
getDownloadUrl,
|
||||
getBatchSources,
|
||||
extractSubtitles,
|
||||
extractSkipTimes,
|
||||
getBestStream,
|
||||
encodePipeRequest,
|
||||
decodePipeResponse,
|
||||
translateId,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* MiruroAPI — creatorInfo.js
|
||||
* Repository: https://github.com/Shineii86/MiruroAPI
|
||||
*
|
||||
* @description
|
||||
* Express middleware that injects creator attribution metadata
|
||||
* into every JSON API response. Overrides res.json() to append
|
||||
* author details and a server-side timestamp, unless the
|
||||
* response already contains creator information.
|
||||
*
|
||||
* @exports
|
||||
* addCreatorInfo — Express middleware function
|
||||
* creatorInfo — Static creator metadata object
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CREATOR METADATA
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// NOTE: Injected into every response unless `data.creator` already exists
|
||||
const creatorInfo = {
|
||||
creator: "Shinei Nouzen",
|
||||
github: "https://github.com/Shineii86",
|
||||
telegram: "https://telegram.me/Shineii86",
|
||||
message: "Build with ❤️ by Shinei Nouzen",
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// MIDDLEWARE
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Express middleware that enriches every JSON response with
|
||||
* creator attribution and an IN-formatted timestamp.
|
||||
*
|
||||
* @param {import('express').Request} req - Express request object
|
||||
* @param {import('express').Response} res - Express response object
|
||||
* @param {import('express').NextFunction} next - Passes control forward
|
||||
* @returns {void}
|
||||
*
|
||||
* @example
|
||||
* app.use(addCreatorInfo);
|
||||
* res.json({ anime: [...] });
|
||||
* // Response includes creator, github, telegram, message, and timestamp
|
||||
*/
|
||||
const addCreatorInfo = (req, res, next) => {
|
||||
// NOTE: Preserve the original json method so we can wrap it
|
||||
const originalJson = res.json.bind(res);
|
||||
|
||||
// NOTE: Monkey-patch res.json to inject creator info
|
||||
res.json = (data) => {
|
||||
// NOTE: Only inject if data is a plain object and missing creator field
|
||||
if (typeof data === "object" && data !== null && !data.creator) {
|
||||
data = {
|
||||
...data,
|
||||
...creatorInfo,
|
||||
timestamp: new Date().toLocaleString("en-IN", { timeZone: "Asia/Kolkata", hour12: true }),
|
||||
};
|
||||
}
|
||||
return originalJson(data);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = { addCreatorInfo, creatorInfo };
|
||||
// ══════════════════════════════════════════════════════════════ END: creatorInfo.js
|
||||
+822
-40
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
||||
* Repository: https://github.com/Shineii86/MiruroAPI
|
||||
*
|
||||
* @description
|
||||
* Integration test suite for MiruroAPI endpoints.
|
||||
* Tests all major 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
|
||||
@@ -15,24 +15,67 @@
|
||||
const BASE = process.env.API_URL || "https://mirurotvapi.vercel.app/api";
|
||||
|
||||
const tests = [
|
||||
// System
|
||||
{ name: "Health", url: "/health" },
|
||||
{ name: "Stats", url: "/stats" },
|
||||
{ name: "OpenAPI", url: "/openapi" },
|
||||
{ name: "Providers", url: "/providers" },
|
||||
|
||||
// Search & Discovery
|
||||
{ name: "Search", url: "/search?query=naruto" },
|
||||
{ name: "Suggestions", url: "/suggestions?query=naruto" },
|
||||
{ name: "Filter", url: "/filter?genre=Action&per_page=1" },
|
||||
{ name: "Multi-Search", url: "/multi-search?q=naruto&genre=Action" },
|
||||
|
||||
// Collections
|
||||
{ name: "Trending", url: "/trending?per_page=1" },
|
||||
{ name: "Trending Daily", url: "/trending/daily" },
|
||||
{ name: "Trending Weekly", url: "/trending/weekly" },
|
||||
{ name: "Popular", url: "/popular?per_page=1" },
|
||||
{ name: "Top", url: "/top?per_page=1" },
|
||||
{ name: "Upcoming", url: "/upcoming?per_page=1" },
|
||||
{ name: "Recent", url: "/recent?per_page=1" },
|
||||
{ name: "Spotlight", url: "/spotlight" },
|
||||
{ name: "Schedule", url: "/schedule" },
|
||||
{ name: "Random", url: "/random" },
|
||||
{ name: "Upcoming Countdown", url: "/upcoming/countdown" },
|
||||
|
||||
// Seasonal & Studio
|
||||
{ name: "Seasonal", url: "/season/2025/spring" },
|
||||
{ name: "Studio", url: "/studio/MAPPA" },
|
||||
|
||||
// Genres & Tags
|
||||
{ name: "Genres", url: "/genres" },
|
||||
{ name: "Tags", url: "/tags" },
|
||||
{ name: "Genre", url: "/genre/Action" },
|
||||
{ name: "Year", url: "/year/2024" },
|
||||
{ name: "Status", url: "/status/RELEASING" },
|
||||
{ name: "Format", url: "/format/MOVIE" },
|
||||
|
||||
// Character & Staff
|
||||
{ name: "Character", url: "/character/1" },
|
||||
{ name: "Staff", url: "/staff/1" },
|
||||
{ name: "Characters Search", url: "/characters?query=kaneki" },
|
||||
{ name: "Staff Search", url: "/staff?query=oda" },
|
||||
|
||||
// Anime Details
|
||||
{ name: "Info", url: "/info/20" },
|
||||
{ name: "Characters", url: "/anime/20/characters" },
|
||||
{ name: "Anime Characters", url: "/anime/20/characters" },
|
||||
{ name: "Relations", url: "/anime/20/relations" },
|
||||
{ name: "Recommendations", url: "/anime/20/recommendations" },
|
||||
|
||||
// Streaming
|
||||
{ name: "Episodes", url: "/episodes/20" },
|
||||
{ name: "Batch Episodes", url: "/episodes/20/batch?provider=kiwi&category=sub&slugs=animepahe-1,animepahe-2" },
|
||||
{ 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: "Watch", url: "/watch/kiwi/20/sub/animepahe-1" },
|
||||
|
||||
// Utility
|
||||
{ name: "Compare", url: "/compare/20/21" },
|
||||
{ name: "Genre Stats", url: "/stats/genre" },
|
||||
{ name: "Calendar", url: "/calendar" },
|
||||
{ name: "Timeline", url: "/timeline/20" },
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
@@ -61,7 +104,7 @@ async function runTest(test) {
|
||||
}
|
||||
|
||||
async function runAll() {
|
||||
console.log(`\n🧪 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);
|
||||
|
||||
+5
-10
@@ -1,13 +1,8 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "server.js",
|
||||
"use": "@vercel/node"
|
||||
"functions": {
|
||||
"api/index.js": {
|
||||
"includeFiles": "src/**",
|
||||
"maxDuration": 30
|
||||
}
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{ "src": "/api/(.*)", "dest": "server.js" },
|
||||
{ "src": "/(.*)", "dest": "server.js" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user