mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 10:15:10 +00:00
feat: MiruroAPI v1.0.0 - Free anime streaming API
- 16 API endpoints powered by AniList GraphQL - Miruro pipe integration for streaming sources - 12 streaming providers (kiwi, pewe, bee, bonk, etc.) - M3U8 streaming URLs with subtitles and skip timestamps - In-memory caching with TTL - CORS support and health endpoint
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
from flask import Flask, jsonify, request
|
||||
import requests
|
||||
import time
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
ANILIST_API = 'https://graphql.anilist.co'
|
||||
|
||||
MEDIA_FIELDS = '''
|
||||
id
|
||||
idMal
|
||||
title { romaji english native userPreferred }
|
||||
description(asHtml: false)
|
||||
coverImage { large medium color }
|
||||
bannerImage
|
||||
format
|
||||
status
|
||||
episodes
|
||||
duration
|
||||
season
|
||||
seasonYear
|
||||
genres
|
||||
tags { name }
|
||||
averageScore
|
||||
meanScore
|
||||
popularity
|
||||
trending
|
||||
favourites
|
||||
startDate { year month day }
|
||||
endDate { year month day }
|
||||
nextAiringEpisode { episode timeUntilAiring airingAt }
|
||||
studios(isMain: true) { edges { node { id name } isMain } }
|
||||
trailer { id site thumbnail url }
|
||||
rankings { rank type context year season allTime }
|
||||
'''
|
||||
|
||||
|
||||
def anilist_query(query, variables=None):
|
||||
if variables is None:
|
||||
variables = {}
|
||||
res = requests.post(ANILIST_API, json={'query': query, 'variables': variables})
|
||||
data = res.json()
|
||||
if 'errors' in data:
|
||||
raise Exception(data['errors'][0]['message'])
|
||||
return data['data']
|
||||
|
||||
|
||||
def format_media(m):
|
||||
return {
|
||||
'id': m['id'],
|
||||
'idMal': m.get('idMal'),
|
||||
'title': m.get('title', {}),
|
||||
'description': m.get('description'),
|
||||
'coverImage': m.get('coverImage', {}),
|
||||
'bannerImage': m.get('bannerImage'),
|
||||
'format': m.get('format'),
|
||||
'status': m.get('status'),
|
||||
'episodes': m.get('episodes'),
|
||||
'duration': m.get('duration'),
|
||||
'season': m.get('season'),
|
||||
'seasonYear': m.get('seasonYear'),
|
||||
'genres': m.get('genres', []),
|
||||
'tags': [t['name'] for t in (m.get('tags') or [])],
|
||||
'averageScore': m.get('averageScore'),
|
||||
'meanScore': m.get('meanScore'),
|
||||
'popularity': m.get('popularity'),
|
||||
'trending': m.get('trending'),
|
||||
'favourites': m.get('favourites'),
|
||||
'startDate': m.get('startDate'),
|
||||
'endDate': m.get('endDate'),
|
||||
'nextAiringEpisode': m.get('nextAiringEpisode'),
|
||||
'studios': [
|
||||
{'id': e['node']['id'], 'name': e['node']['name'], 'isMain': e['isMain']}
|
||||
for e in (m.get('studios', {}).get('edges') or [])
|
||||
],
|
||||
'trailer': m.get('trailer'),
|
||||
'rankings': m.get('rankings', []),
|
||||
}
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
return jsonify({
|
||||
'name': 'Miruro Anime API',
|
||||
'version': '1.0.0',
|
||||
'status': 'running',
|
||||
'endpoints': [
|
||||
'GET /api/trending',
|
||||
'GET /api/popular',
|
||||
'GET /api/top',
|
||||
'GET /api/seasonal?season=SPRING&year=2026',
|
||||
'GET /api/schedule?day=MONDAY',
|
||||
'GET /api/airing',
|
||||
'GET /api/search?query=naruto&sort=SCORE_DESC&page=1',
|
||||
'GET /api/info/<id>',
|
||||
'GET /api/info/<id>/episodes',
|
||||
'GET /api/characters/<id>',
|
||||
'GET /api/recommendations/<id>',
|
||||
'GET /api/genres',
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/trending')
|
||||
def trending():
|
||||
limit = request.args.get('limit', 20, type=int)
|
||||
query = f'query {{ Page(perPage: {limit}) {{ media(sort: TRENDING_DESC, type: ANIME) {{ {MEDIA_FIELDS} }} }} }}'
|
||||
data = anilist_query(query)
|
||||
return jsonify({'success': True, 'data': [format_media(m) for m in data['Page']['media']]})
|
||||
|
||||
|
||||
@app.route('/api/popular')
|
||||
def popular():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('limit', 20, type=int)
|
||||
query = f'query {{ Page(page: {page}, perPage: {limit}) {{ media(sort: POPULARITY_DESC, type: ANIME) {{ {MEDIA_FIELDS} }} pageInfo {{ total currentPage lastPage perPage }} }} }}'
|
||||
data = anilist_query(query)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': [format_media(m) for m in data['Page']['media']],
|
||||
'pagination': data['Page']['pageInfo'],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/top')
|
||||
def top():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('limit', 20, type=int)
|
||||
query = f'query {{ Page(page: {page}, perPage: {limit}) {{ media(sort: SCORE_DESC, type: ANIME) {{ {MEDIA_FIELDS} }} pageInfo {{ total currentPage lastPage perPage }} }} }}'
|
||||
data = anilist_query(query)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': [format_media(m) for m in data['Page']['media']],
|
||||
'pagination': data['Page']['pageInfo'],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/seasonal')
|
||||
def seasonal():
|
||||
season = request.args.get('season', 'SPRING').upper()
|
||||
year = request.args.get('year', 2026, type=int)
|
||||
limit = request.args.get('limit', 20, type=int)
|
||||
query = f'''query ($season: MediaSeason, $seasonYear: Int) {{
|
||||
Page(perPage: {limit}) {{
|
||||
media(season: $season, seasonYear: $seasonYear, sort: POPULARITY_DESC, type: ANIME) {{ {MEDIA_FIELDS} }}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query, {'season': season, 'seasonYear': year})
|
||||
return jsonify({'success': True, 'season': season, 'year': year, 'data': [format_media(m) for m in data['Page']['media']] })
|
||||
|
||||
|
||||
@app.route('/api/schedule')
|
||||
def schedule():
|
||||
day = request.args.get('day', '').upper()
|
||||
query = f'''query {{
|
||||
Page(perPage: 50) {{
|
||||
media(sort: TRENDING_DESC, type: ANIME, status: RELEASING) {{
|
||||
id
|
||||
title {{ romaji english native userPreferred }}
|
||||
coverImage {{ large medium color }}
|
||||
nextAiringEpisode {{ episode timeUntilAiring airingAt }}
|
||||
}}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query)
|
||||
episodes = []
|
||||
for m in data['Page']['media']:
|
||||
if not m.get('nextAiringEpisode'):
|
||||
continue
|
||||
ep = m['nextAiringEpisode']
|
||||
air_date = time.gmtime(ep['airingAt'])
|
||||
dow = ['SUNDAY','MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY']
|
||||
entry = {
|
||||
'id': m['id'],
|
||||
'title': m['title']['userPreferred'],
|
||||
'coverImage': m.get('coverImage', {}).get('large'),
|
||||
'nextEpisode': ep['episode'],
|
||||
'airingAt': ep['airingAt'],
|
||||
'dayOfWeek': dow[air_date.tm_wday],
|
||||
'timeUntilAiring': ep['timeUntilAiring'],
|
||||
}
|
||||
if day and dow[air_date.tm_wday] != day:
|
||||
continue
|
||||
episodes.append(entry)
|
||||
episodes.sort(key=lambda x: x['airingAt'])
|
||||
return jsonify({'success': True, 'data': episodes})
|
||||
|
||||
|
||||
@app.route('/api/airing')
|
||||
def airing():
|
||||
query = f'''query {{
|
||||
Page(perPage: 25) {{
|
||||
media(sort: TRENDING_DESC, type: ANIME, status: RELEASING) {{
|
||||
id
|
||||
title {{ romaji english native userPreferred }}
|
||||
coverImage {{ large medium color }}
|
||||
bannerImage
|
||||
format
|
||||
episodes
|
||||
averageScore
|
||||
nextAiringEpisode {{ episode timeUntilAiring airingAt }}
|
||||
}}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query)
|
||||
result = []
|
||||
for m in data['Page']['media']:
|
||||
if not m.get('nextAiringEpisode'):
|
||||
continue
|
||||
result.append({
|
||||
'id': m['id'],
|
||||
'title': m['title']['userPreferred'],
|
||||
'coverImage': m.get('coverImage', {}).get('large'),
|
||||
'bannerImage': m.get('bannerImage'),
|
||||
'format': m.get('format'),
|
||||
'episodes': m.get('episodes'),
|
||||
'averageScore': m.get('averageScore'),
|
||||
'nextEpisode': m['nextAiringEpisode']['episode'],
|
||||
'airingAt': m['nextAiringEpisode']['airingAt'],
|
||||
'timeUntilAiring': m['nextAiringEpisode']['timeUntilAiring'],
|
||||
})
|
||||
return jsonify({'success': True, 'data': result})
|
||||
|
||||
|
||||
@app.route('/api/search')
|
||||
def search():
|
||||
q = request.args.get('query', '')
|
||||
sort = request.args.get('sort', 'POPULARITY_DESC').upper()
|
||||
genre = request.args.get('genre', '')
|
||||
fmt = request.args.get('format', '').upper()
|
||||
status = request.args.get('status', '').upper()
|
||||
season = request.args.get('season', '').upper()
|
||||
year = request.args.get('year', '', type=str)
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('limit', 20, type=int)
|
||||
|
||||
if not q and not genre:
|
||||
return jsonify({'success': False, 'error': 'query or genre parameter required'}), 400
|
||||
|
||||
sort_map = {
|
||||
'SCORE_DESC': 'SCORE_DESC', 'POPULARITY_DESC': 'POPULARITY_DESC',
|
||||
'TRENDING_DESC': 'TRENDING_DESC', 'FAVOURITES_DESC': 'FAVOURITES_DESC',
|
||||
'START_DATE_DESC': 'START_DATE_DESC', 'UPDATED_AT_DESC': 'UPDATED_AT_DESC',
|
||||
}
|
||||
s = sort_map.get(sort, 'POPULARITY_DESC')
|
||||
|
||||
filters = ''
|
||||
variables = {'search': q or None, 'sort': s, 'page': page, 'perPage': limit}
|
||||
if genre:
|
||||
filters += ', genre: $genre'
|
||||
variables['genre'] = genre
|
||||
if fmt:
|
||||
filters += ', format: $format'
|
||||
variables['format'] = fmt
|
||||
if status:
|
||||
filters += ', status: $status'
|
||||
variables['status'] = status
|
||||
if season:
|
||||
filters += ', season: $season'
|
||||
variables['season'] = season
|
||||
if year:
|
||||
filters += ', seasonYear: $seasonYear'
|
||||
variables['seasonYear'] = int(year)
|
||||
|
||||
query = f'''query ($search: String, $sort: [MediaSort], $genre: String, $format: MediaFormat, $status: MediaStatus, $season: MediaSeason, $seasonYear: Int, $page: Int, $perPage: Int) {{
|
||||
Page(page: $page, perPage: $perPage) {{
|
||||
media(search: $search, type: ANIME, sort: $sort{filters}) {{ {MEDIA_FIELDS} }}
|
||||
pageInfo {{ total currentPage lastPage perPage }}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query, variables)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': [format_media(m) for m in data['Page']['media']],
|
||||
'pagination': data['Page']['pageInfo'],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/info/<int:anime_id>')
|
||||
def info(anime_id):
|
||||
query = f'''query ($id: Int) {{
|
||||
Media(id: $id, type: ANIME) {{
|
||||
{MEDIA_FIELDS}
|
||||
relations {{
|
||||
edges {{
|
||||
node {{ id title {{ romaji english native userPreferred }} format coverImage {{ large medium }} }}
|
||||
relationType
|
||||
}}
|
||||
}}
|
||||
externalLinks {{ id site url type language }}
|
||||
streamingEpisodes {{ title thumbnail url site }}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query, {'id': anime_id})
|
||||
if not data.get('Media'):
|
||||
return jsonify({'success': False, 'error': 'Anime not found'}), 404
|
||||
m = data['Media']
|
||||
result = format_media(m)
|
||||
result['relations'] = [
|
||||
{
|
||||
'id': e['node']['id'],
|
||||
'title': e['node']['title']['userPreferred'],
|
||||
'format': e['node']['format'],
|
||||
'coverImage': e['node']['coverImage'].get('large') if e['node'].get('coverImage') else None,
|
||||
'relationType': e['relationType'],
|
||||
}
|
||||
for e in (m.get('relations', {}).get('edges') or [])
|
||||
]
|
||||
result['externalLinks'] = m.get('externalLinks') or []
|
||||
result['streamingEpisodes'] = m.get('streamingEpisodes') or []
|
||||
return jsonify({'success': True, 'data': result})
|
||||
|
||||
|
||||
@app.route('/api/info/<int:anime_id>/episodes')
|
||||
def episodes(anime_id):
|
||||
query = f'''query ($id: Int) {{
|
||||
Media(id: $id, type: ANIME) {{
|
||||
id
|
||||
title {{ romaji english native userPreferred }}
|
||||
episodes
|
||||
streamingEpisodes {{ title thumbnail url site }}
|
||||
externalLinks {{ id site url type }}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query, {'id': anime_id})
|
||||
if not data.get('Media'):
|
||||
return jsonify({'success': False, 'error': 'Anime not found'}), 404
|
||||
m = data['Media']
|
||||
eps = []
|
||||
for i, ep in enumerate(m.get('streamingEpisodes') or []):
|
||||
eps.append({
|
||||
'number': i + 1,
|
||||
'title': ep.get('title'),
|
||||
'thumbnail': ep.get('thumbnail'),
|
||||
'url': ep.get('url'),
|
||||
'site': ep.get('site'),
|
||||
})
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'animeId': m['id'],
|
||||
'title': m['title']['userPreferred'],
|
||||
'totalEpisodes': m.get('episodes'),
|
||||
'episodes': eps,
|
||||
'providers': ['kiwi', 'pewe', 'bee', 'bonk', 'bun', 'ally', 'nun', 'twin', 'cog', 'moo', 'hop'],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/characters/<int:anime_id>')
|
||||
def characters(anime_id):
|
||||
query = f'''query ($id: Int) {{
|
||||
Media(id: $id, type: ANIME) {{
|
||||
id
|
||||
characters(sort: ROLE, perPage: 25) {{
|
||||
edges {{
|
||||
node {{ id name {{ full native alternative }} image {{ large medium }} gender age bloodType }}
|
||||
role
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query, {'id': anime_id})
|
||||
if not data.get('Media'):
|
||||
return jsonify({'success': False, 'error': 'Anime not found'}), 404
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'animeId': data['Media']['id'],
|
||||
'characters': [
|
||||
{
|
||||
'id': e['node']['id'],
|
||||
'name': e['node']['name'],
|
||||
'image': e['node']['image'],
|
||||
'role': e['role'],
|
||||
'gender': e['node'].get('gender'),
|
||||
'age': e['node'].get('age'),
|
||||
'bloodType': e['node'].get('bloodType'),
|
||||
}
|
||||
for e in data['Media']['characters']['edges']
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/recommendations/<int:anime_id>')
|
||||
def recommendations(anime_id):
|
||||
query = f'''query ($id: Int) {{
|
||||
Media(id: $id, type: ANIME) {{
|
||||
id
|
||||
recommendations(perPage: 10, sort: RATING_DESC) {{
|
||||
edges {{
|
||||
node {{
|
||||
id rating
|
||||
mediaRecommendation {{
|
||||
id
|
||||
title {{ romaji english native userPreferred }}
|
||||
coverImage {{ large medium color }}
|
||||
format episodes averageScore popularity
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}'''
|
||||
data = anilist_query(query, {'id': anime_id})
|
||||
if not data.get('Media'):
|
||||
return jsonify({'success': False, 'error': 'Anime not found'}), 404
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'animeId': data['Media']['id'],
|
||||
'recommendations': [
|
||||
{
|
||||
'id': e['node']['mediaRecommendation']['id'],
|
||||
'title': e['node']['mediaRecommendation']['title']['userPreferred'],
|
||||
'coverImage': e['node']['mediaRecommendation']['coverImage'].get('large'),
|
||||
'format': e['node']['mediaRecommendation']['format'],
|
||||
'episodes': e['node']['mediaRecommendation']['episodes'],
|
||||
'averageScore': e['node']['mediaRecommendation']['averageScore'],
|
||||
'rating': e['node']['rating'],
|
||||
}
|
||||
for e in data['Media']['recommendations']['edges']
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/genres')
|
||||
def genres():
|
||||
data = anilist_query('{ GenreCollection }')
|
||||
return jsonify({'success': True, 'data': data['GenreCollection']})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('\n 🎬 Miruro Anime API running on http://localhost:3000\n')
|
||||
print(' Endpoints:')
|
||||
print(' GET /api/trending')
|
||||
print(' GET /api/popular')
|
||||
print(' GET /api/top')
|
||||
print(' GET /api/seasonal?season=SPRING&year=2026')
|
||||
print(' GET /api/schedule?day=MONDAY')
|
||||
print(' GET /api/airing')
|
||||
print(' GET /api/search?query=naruto')
|
||||
print(' GET /api/info/<id>')
|
||||
print(' GET /api/info/<id>/episodes')
|
||||
print(' GET /api/characters/<id>')
|
||||
print(' GET /api/recommendations/<id>')
|
||||
print(' GET /api/genres\n')
|
||||
app.run(host='0.0.0.0', port=3000, debug=True)
|
||||
Reference in New Issue
Block a user