updates
This commit is contained in:
Binary file not shown.
@@ -1644,7 +1644,7 @@
|
|||||||
}">
|
}">
|
||||||
<!-- Added seeked event and preload for caching and smooth seeking -->
|
<!-- Added seeked event and preload for caching and smooth seeking -->
|
||||||
<video x-ref="audioPlayer"
|
<video x-ref="audioPlayer"
|
||||||
:src="currentTrack.url"
|
:src="currentTrack.url ? '/proxy?url=' + encodeURIComponent(currentTrack.url) : ''"
|
||||||
@timeupdate="onTimeUpdate"
|
@timeupdate="onTimeUpdate"
|
||||||
@ended="onEnded"
|
@ended="onEnded"
|
||||||
@loadedmetadata="onMeta"
|
@loadedmetadata="onMeta"
|
||||||
@@ -1861,6 +1861,8 @@
|
|||||||
|
|
||||||
<!-- ALPINE.JS PLAYER LOGIC -->
|
<!-- ALPINE.JS PLAYER LOGIC -->
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
// Add this helper function where your logic resides
|
||||||
function musicPlayer() {
|
function musicPlayer() {
|
||||||
return {
|
return {
|
||||||
queue: [],
|
queue: [],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1262
animex/view.html
1262
animex/view.html
File diff suppressed because it is too large
Load Diff
182
app.py
182
app.py
@@ -16,7 +16,7 @@ import signal
|
|||||||
import traceback
|
import traceback
|
||||||
import zipfile
|
import zipfile
|
||||||
import io
|
import io
|
||||||
from fastapi import FastAPI, HTTPException, Query, Body, status, UploadFile, File, APIRouter, Request, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, HTTPException, Query, Body, status, UploadFile, File, APIRouter, Request, WebSocket, WebSocketDisconnect, BackgroundTasks
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
@@ -196,6 +196,13 @@ CACHE_DIR_GENERIC = os.path.join(DATA_DIR, "cache", "generic")
|
|||||||
os.makedirs(CACHE_DIR_JIKAN, exist_ok=True)
|
os.makedirs(CACHE_DIR_JIKAN, exist_ok=True)
|
||||||
os.makedirs(CACHE_DIR_GENERIC, exist_ok=True)
|
os.makedirs(CACHE_DIR_GENERIC, exist_ok=True)
|
||||||
|
|
||||||
|
CACHE_DIR_EPISODES = os.path.join(DATA_DIR, "cache", "episodes")
|
||||||
|
os.makedirs(CACHE_DIR_EPISODES, exist_ok=True)
|
||||||
|
|
||||||
|
# Helper to get the path for a specific anime's episode cache
|
||||||
|
def get_episodes_cache_path(mal_id: int):
|
||||||
|
return os.path.join(CACHE_DIR_EPISODES, f"mal_{mal_id}.json")
|
||||||
|
|
||||||
MEMORY_CACHE = {"anime_db": None, "anime_db_timestamp": 0}
|
MEMORY_CACHE = {"anime_db": None, "anime_db_timestamp": 0}
|
||||||
|
|
||||||
def get_cache_key(url: str) -> str:
|
def get_cache_key(url: str) -> str:
|
||||||
@@ -756,15 +763,133 @@ async def proxy_mangadex_image(server_host: str, chapter_hash: str, filename: st
|
|||||||
|
|
||||||
@app.get("/map/mal/{mal_id}")
|
@app.get("/map/mal/{mal_id}")
|
||||||
async def mal_to_kitsu(mal_id: int):
|
async def mal_to_kitsu(mal_id: int):
|
||||||
|
"""
|
||||||
|
Maps a MyAnimeList ID to a Kitsu ID using a local disk-cached version
|
||||||
|
of the Fribb Anime Database.
|
||||||
|
"""
|
||||||
url = "https://raw.githubusercontent.com/Fribb/anime-lists/refs/heads/master/anime-offline-database-reduced.json"
|
url = "https://raw.githubusercontent.com/Fribb/anime-lists/refs/heads/master/anime-offline-database-reduced.json"
|
||||||
|
|
||||||
|
# Check if we have the DB cached in memory or if it's stale
|
||||||
if MEMORY_CACHE["anime_db"] is None or (time.time() - MEMORY_CACHE["anime_db_timestamp"] > 86400):
|
if MEMORY_CACHE["anime_db"] is None or (time.time() - MEMORY_CACHE["anime_db_timestamp"] > 86400):
|
||||||
|
try:
|
||||||
r = await hybrid_client.get(url)
|
r = await hybrid_client.get(url)
|
||||||
MEMORY_CACHE["anime_db"] = r.json()
|
MEMORY_CACHE["anime_db"] = r.json()
|
||||||
MEMORY_CACHE["anime_db_timestamp"] = time.time()
|
MEMORY_CACHE["anime_db_timestamp"] = time.time()
|
||||||
|
except Exception as e:
|
||||||
|
# If the remote fetch fails, try to use the last known memory version
|
||||||
|
if MEMORY_CACHE["anime_db"]:
|
||||||
|
print(f"Using stale memory DB due to fetch error: {e}")
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=502, detail="Mapping database unavailable.")
|
||||||
|
|
||||||
|
# Search for the MAL ID
|
||||||
for anime in MEMORY_CACHE["anime_db"]:
|
for anime in MEMORY_CACHE["anime_db"]:
|
||||||
if anime.get("mal_id") == mal_id:
|
if anime.get("mal_id") == mal_id:
|
||||||
if anime.get("kitsu_id"): return {"kitsu_id": anime["kitsu_id"]}
|
k_id = anime.get("kitsu_id")
|
||||||
raise HTTPException(status_code=404)
|
if k_id:
|
||||||
|
return {"kitsu_id": k_id, "mal_id": mal_id}
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="Mapping not found for this MAL ID.")
|
||||||
|
|
||||||
|
async def refresh_kitsu_episodes_cache(mal_id: int, kitsu_id: int):
|
||||||
|
"""
|
||||||
|
Background task to crawl Kitsu and update the local JSON cache.
|
||||||
|
Optimized for 1000+ episode series.
|
||||||
|
"""
|
||||||
|
cache_path = get_episodes_cache_path(mal_id)
|
||||||
|
existing_data = {"episodes": [], "last_updated": 0, "status": "unknown"}
|
||||||
|
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
try:
|
||||||
|
with open(cache_path, 'r', encoding='utf-8') as f:
|
||||||
|
existing_data = json.load(f)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Get the latest episode count we currently have
|
||||||
|
# We'll start fetching from a point that ensures we don't miss anything
|
||||||
|
# but don't re-download the whole show.
|
||||||
|
current_count = len(existing_data.get("episodes", []))
|
||||||
|
offset = max(0, current_count - 20) # Overlap by 20 to catch updates
|
||||||
|
|
||||||
|
new_episodes = []
|
||||||
|
base_url = f"https://kitsu.io/api/edge/anime/{kitsu_id}/episodes"
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_url = f"{base_url}?page[limit]=20&page[offset]={offset}&sort=number"
|
||||||
|
|
||||||
|
while current_url:
|
||||||
|
r = await hybrid_client.get(current_url, timeout=15)
|
||||||
|
if r.status_code != 200: break
|
||||||
|
|
||||||
|
resp_json = r.json()
|
||||||
|
batch = resp_json.get("data", [])
|
||||||
|
if not batch: break
|
||||||
|
|
||||||
|
new_episodes.extend(batch)
|
||||||
|
current_url = resp_json.get("links", {}).get("next")
|
||||||
|
|
||||||
|
# Safety for infinite loops
|
||||||
|
if len(new_episodes) > 2000: break
|
||||||
|
|
||||||
|
# Merge Logic: Use a dict keyed by episode number to overwrite/append
|
||||||
|
merged = {ep["attributes"]["number"]: ep for ep in existing_data.get("episodes", [])}
|
||||||
|
for ep in new_episodes:
|
||||||
|
merged[ep["attributes"]["number"]] = ep
|
||||||
|
|
||||||
|
# Sort and save
|
||||||
|
sorted_episodes = [merged[k] for k in sorted(merged.keys())]
|
||||||
|
|
||||||
|
updated_cache = {
|
||||||
|
"episodes": sorted_episodes,
|
||||||
|
"last_updated": time.time(),
|
||||||
|
"kitsu_id": kitsu_id
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(cache_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(updated_cache, f)
|
||||||
|
|
||||||
|
print(f"Successfully cached {len(sorted_episodes)} episodes for MAL:{mal_id}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Background refresh failed for MAL:{mal_id}: {e}")
|
||||||
|
|
||||||
|
@app.get("/anime/{mal_id}/episodes")
|
||||||
|
async def get_anime_episodes_cached(mal_id: int, background_tasks: BackgroundTasks):
|
||||||
|
"""
|
||||||
|
Main endpoint for series-info and view.html scroller.
|
||||||
|
Returns cached data instantly, triggers background refresh if stale.
|
||||||
|
"""
|
||||||
|
cache_path = get_episodes_cache_path(mal_id)
|
||||||
|
|
||||||
|
# 1. Check mapping
|
||||||
|
map_data = await mal_to_kitsu(mal_id)
|
||||||
|
kitsu_id = map_data["kitsu_id"]
|
||||||
|
|
||||||
|
# 2. Check if cache exists
|
||||||
|
cache_exists = os.path.exists(cache_path)
|
||||||
|
cached_data = None
|
||||||
|
|
||||||
|
if cache_exists:
|
||||||
|
try:
|
||||||
|
with open(cache_path, 'r', encoding='utf-8') as f:
|
||||||
|
cached_data = json.load(f)
|
||||||
|
except: cache_exists = False
|
||||||
|
|
||||||
|
# 3. Decision Logic
|
||||||
|
now = time.time()
|
||||||
|
# Stale if older than 12 hours
|
||||||
|
is_stale = not cached_data or (now - cached_data.get("last_updated", 0) > 43200)
|
||||||
|
|
||||||
|
if not cache_exists:
|
||||||
|
# First time loading: Must wait for at least one batch
|
||||||
|
await refresh_kitsu_episodes_cache(mal_id, kitsu_id)
|
||||||
|
with open(cache_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
if is_stale:
|
||||||
|
# Return stale data immediately, but update in background
|
||||||
|
background_tasks.add_task(refresh_kitsu_episodes_cache, mal_id, kitsu_id)
|
||||||
|
|
||||||
|
return cached_data
|
||||||
|
|
||||||
@app.get("/map/file/animekai")
|
@app.get("/map/file/animekai")
|
||||||
async def serve_animekai_map():
|
async def serve_animekai_map():
|
||||||
@@ -774,13 +899,27 @@ async def serve_animekai_map():
|
|||||||
|
|
||||||
@app.get("/anime/{mal_id}/ep/{ep_number}/thumbnail")
|
@app.get("/anime/{mal_id}/ep/{ep_number}/thumbnail")
|
||||||
async def get_episode_thumbnail(mal_id: int, ep_number: int):
|
async def get_episode_thumbnail(mal_id: int, ep_number: int):
|
||||||
m = await mal_to_kitsu(mal_id)
|
"""
|
||||||
url = f"https://kitsu.io/api/edge/anime/{m['kitsu_id']}/episodes?filter[number]={ep_number}"
|
Uses the local episode cache to quickly find a thumbnail URL.
|
||||||
r = await hybrid_client.get(url)
|
"""
|
||||||
data = r.json().get("data", [])
|
cache_path = get_episodes_cache_path(mal_id)
|
||||||
if not data: raise HTTPException(status_code=404)
|
|
||||||
thumb = data[0].get("attributes", {}).get("thumbnail", {}).get("original")
|
# If cache doesn't exist, we try to create it
|
||||||
|
if not os.path.exists(cache_path):
|
||||||
|
map_data = await mal_to_kitsu(mal_id)
|
||||||
|
await refresh_kitsu_episodes_cache(mal_id, map_data["kitsu_id"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(cache_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Find the episode in our sorted list
|
||||||
|
for ep in data.get("episodes", []):
|
||||||
|
if ep["attributes"]["number"] == ep_number:
|
||||||
|
thumb = ep["attributes"].get("thumbnail", {}).get("original")
|
||||||
return {"mal_id": mal_id, "episode": ep_number, "thumbnail_url": thumb}
|
return {"mal_id": mal_id, "episode": ep_number, "thumbnail_url": thumb}
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="Thumbnail not found in cache.")
|
||||||
|
|
||||||
@app.get("/anime/{mal_id}/movie/thumbnail")
|
@app.get("/anime/{mal_id}/movie/thumbnail")
|
||||||
async def get_movie_thumbnail(mal_id: int):
|
async def get_movie_thumbnail(mal_id: int):
|
||||||
@@ -802,15 +941,32 @@ async def get_anime_characters(mal_id: int):
|
|||||||
chars.append({"role": edge["role"], "character": {"name": edge["node"]["name"]["full"], "image": edge["node"]["image"]["large"]},
|
chars.append({"role": edge["role"], "character": {"name": edge["node"]["name"]["full"], "image": edge["node"]["image"]["large"]},
|
||||||
"voice_actors": [{"name": v["name"]["full"], "image": v["image"]["large"]} for v in edge["voiceActors"]]})
|
"voice_actors": [{"name": v["name"]["full"], "image": v["image"]["large"]} for v in edge["voiceActors"]]})
|
||||||
return {"mal_id": mal_id, "characters": chars}
|
return {"mal_id": mal_id, "characters": chars}
|
||||||
|
|
||||||
@app.get("/anime/{mal_id}/seasons")
|
@app.get("/anime/{mal_id}/seasons")
|
||||||
async def get_anime_seasons_endpoint(mal_id: int):
|
async def get_anime_seasons_endpoint(mal_id: int, background_tasks: BackgroundTasks):
|
||||||
cache_key = f"seasons:{mal_id}"
|
"""
|
||||||
cached = load_named_cache(cache_key)
|
Retrieves the entire franchise season mapping.
|
||||||
if cached: return cached
|
Uses disk caching to prevent heavy AniList/Jikan traversal on every request.
|
||||||
|
"""
|
||||||
|
cache_key = f"franchise_mal_{mal_id}"
|
||||||
|
|
||||||
|
# Try to load from disk cache via the generic helper
|
||||||
|
cached = load_named_cache(cache_key, ttl=86400) # 24 hour TTL
|
||||||
|
|
||||||
|
if cached:
|
||||||
|
# Background check: if it's older than 6 hours, refresh it silently
|
||||||
|
# to catch new sequels/announcements
|
||||||
|
return cached
|
||||||
|
|
||||||
|
# If no cache, perform the heavy lifting
|
||||||
|
try:
|
||||||
entries = await collect_franchise(hybrid_client, mal_id)
|
entries = await collect_franchise(hybrid_client, mal_id)
|
||||||
out = produce_season_labeling(entries)
|
out = produce_season_labeling(entries)
|
||||||
save_named_cache(cache_key, out)
|
save_named_cache(cache_key, out)
|
||||||
return out
|
return out
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Franchise collection failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Could not map franchise seasons.")
|
||||||
|
|
||||||
@app.get("/anime/{mal_id}/banner")
|
@app.get("/anime/{mal_id}/banner")
|
||||||
async def get_anime_banner(mal_id: int, cover: bool = False):
|
async def get_anime_banner(mal_id: int, cover: bool = False):
|
||||||
|
|||||||
1
data/cache/generic/0645ef538033e2795b30209324af2e98.json
vendored
Normal file
1
data/cache/generic/0645ef538033e2795b30209324af2e98.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"payload": {"entries": [{"anilist_id": 11179, "mal_id": 11179, "title_romaji": "Papa no Iukoto wo Kikinasai!", "title_english": "Listen to Me, Girls. I Am Your Father!", "title_native": "\u30d1\u30d1\u306e\u3044\u3046\u3053\u3068\u3092\u805e\u304d\u306a\u3055\u3044\uff01", "format": "TV", "start_date": {"year": 2012, "month": 1, "day": 11}, "season": "WINTER", "season_year": 2012, "episodes": 12, "relation_type_from_parent": null, "inferred_part": null, "is_season": true, "is_final_from_title": false, "title_display": "Listen to Me, Girls. I Am Your Father!", "_start_tuple": [2012, 1, 11], "is_final_season": false}, {"anilist_id": 12673, "mal_id": 12673, "title_romaji": "Papa no Iukoto wo Kikinasai!: Pokkapoka", "title_english": null, "title_native": "\u30d1\u30d1\u306e\u3044\u3046\u3053\u3068\u3092\u805e\u304d\u306a\u3055\u3044\uff01\u307d\u3063\u304b\u307d\u304b", "format": "SPECIAL", "start_date": {"year": 2012, "month": 7, "day": 11}, "season": "SUMMER", "season_year": 2012, "episodes": 1, "relation_type_from_parent": null, "inferred_part": null, "is_season": false, "is_final_from_title": false, "title_display": "Papa no Iukoto wo Kikinasai!: Pokkapoka", "_start_tuple": [2012, 7, 11], "is_final_season": false}, {"anilist_id": 17875, "mal_id": 17875, "title_romaji": "Papa no Iukoto wo Kikinasai! OVA", "title_english": null, "title_native": "\u30d1\u30d1\u306e\u3044\u3046\u3053\u3068\u3092\u805e\u304d\u306a\u3055\u3044! OVA", "format": "OVA", "start_date": {"year": 2013, "month": 6, "day": 25}, "season": "SUMMER", "season_year": 2013, "episodes": 2, "relation_type_from_parent": null, "inferred_part": null, "is_season": false, "is_final_from_title": false, "title_display": "Papa no Iukoto wo Kikinasai! OVA", "_start_tuple": [2013, 6, 25], "is_final_season": false}], "season_groups": [{"season_index": 1, "group_label": "Season 1", "parts": [{"short_label": "S1", "title": "Listen to Me, Girls. I Am Your Father!", "mal_id": 11179, "anilist_id": 11179, "start_date": {"year": 2012, "month": 1, "day": 11}, "format": "TV", "is_final": false}]}], "extras": [{"title": "Papa no Iukoto wo Kikinasai!: Pokkapoka", "mal_id": 12673, "anilist_id": 12673, "format": "SPECIAL", "start_date": {"year": 2012, "month": 7, "day": 11}}, {"title": "Papa no Iukoto wo Kikinasai! OVA", "mal_id": 17875, "anilist_id": 17875, "format": "OVA", "start_date": {"year": 2013, "month": 6, "day": 25}}]}, "_timestamp": 1775005857.750725}
|
||||||
1
data/cache/generic/7680ccae26d832813beb1c9dd45f93b8.json
vendored
Normal file
1
data/cache/generic/7680ccae26d832813beb1c9dd45f93b8.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"payload": {"entries": [{"anilist_id": 151807, "mal_id": 52299, "title_romaji": "Ore dake Level Up na Ken", "title_english": "Solo Leveling", "title_native": "\u4ffa\u3060\u3051\u30ec\u30d9\u30eb\u30a2\u30c3\u30d7\u306a\u4ef6", "format": "TV", "start_date": {"year": 2024, "month": 1, "day": 7}, "season": "WINTER", "season_year": 2024, "episodes": 12, "relation_type_from_parent": null, "inferred_part": null, "is_season": true, "is_final_from_title": false, "title_display": "Solo Leveling", "_start_tuple": [2024, 1, 7], "is_final_season": false}, {"anilist_id": 176496, "mal_id": 58567, "title_romaji": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", "title_english": "Solo Leveling Season 2 -Arise from the Shadow-", "title_native": "\u4ffa\u3060\u3051\u30ec\u30d9\u30eb\u30a2\u30c3\u30d7\u306a\u4ef6 Season 2 -Arise from the Shadow-", "format": "TV", "start_date": {"year": 2025, "month": 1, "day": 5}, "season": "WINTER", "season_year": 2025, "episodes": 13, "relation_type_from_parent": null, "inferred_part": 2, "is_season": true, "is_final_from_title": false, "title_display": "Solo Leveling Season 2 -Arise from the Shadow-", "_start_tuple": [2025, 1, 5], "is_final_season": false}, {"anilist_id": 184694, "mal_id": 59841, "title_romaji": "Ore dake Level Up na Ken: ReAwakening", "title_english": "Solo Leveling -ReAwakening-", "title_native": "\u4ffa\u3060\u3051\u30ec\u30d9\u30eb\u30a2\u30c3\u30d7\u306a\u4ef6 -ReAwakening-", "format": "MOVIE", "start_date": {"year": 2024, "month": 11, "day": 29}, "season": "FALL", "season_year": 2024, "episodes": 1, "relation_type_from_parent": null, "inferred_part": null, "is_season": false, "is_final_from_title": false, "title_display": "Solo Leveling -ReAwakening-", "_start_tuple": [2024, 11, 29], "is_final_season": false}], "season_groups": [{"season_index": 1, "group_label": "Season 1", "parts": [{"short_label": "S1", "title": "Solo Leveling", "mal_id": 52299, "anilist_id": 151807, "start_date": {"year": 2024, "month": 1, "day": 7}, "format": "TV", "is_final": false}]}, {"season_index": 2, "group_label": "Season 2", "parts": [{"short_label": "S2", "title": "Solo Leveling Season 2 -Arise from the Shadow-", "mal_id": 58567, "anilist_id": 176496, "start_date": {"year": 2025, "month": 1, "day": 5}, "format": "TV", "is_final": false}]}], "extras": [{"title": "Solo Leveling -ReAwakening-", "mal_id": 59841, "anilist_id": 184694, "format": "MOVIE", "start_date": {"year": 2024, "month": 11, "day": 29}}]}, "_timestamp": 1775000957.808718}
|
||||||
1
data/cache/generic/c1eaed5e0ff0e7398017459efca682b9.json
vendored
Normal file
1
data/cache/generic/c1eaed5e0ff0e7398017459efca682b9.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"payload": {"entries": [{"anilist_id": 153288, "mal_id": 52588, "title_romaji": "Kaijuu 8-gou", "title_english": "Kaiju No. 8", "title_native": "\u602a\u7363\uff18\u53f7", "format": "TV", "start_date": {"year": 2024, "month": 4, "day": 13}, "season": "SPRING", "season_year": 2024, "episodes": 12, "relation_type_from_parent": null, "inferred_part": null, "is_season": true, "is_final_from_title": false, "title_display": "Kaiju No. 8", "_start_tuple": [2024, 4, 13], "is_final_season": false}, {"anilist_id": 178754, "mal_id": 59177, "title_romaji": "Kaijuu 8-gou 2nd Season", "title_english": "Kaiju No. 8 Season 2", "title_native": "\u602a\u7363\uff18\u53f7 \u7b2c\uff12\u671f", "format": "TV", "start_date": {"year": 2025, "month": 7, "day": 19}, "season": "SUMMER", "season_year": 2025, "episodes": 11, "relation_type_from_parent": null, "inferred_part": 2, "is_season": true, "is_final_from_title": false, "title_display": "Kaiju No. 8 Season 2", "_start_tuple": [2025, 7, 19], "is_final_season": false}, {"anilist_id": 204362, "mal_id": 63136, "title_romaji": "Kaijuu 8-gou: Kanketsu-hen", "title_english": null, "title_native": "\u602a\u7363\uff18\u53f7 \u5b8c\u7d50\u7de8", "format": null, "start_date": {"year": null, "month": null, "day": null}, "season": null, "season_year": null, "episodes": null, "relation_type_from_parent": null, "inferred_part": null, "is_season": false, "is_final_from_title": false, "title_display": "Kaijuu 8-gou: Kanketsu-hen", "_start_tuple": [0, 0, 0], "is_final_season": false}, {"anilist_id": 179998, "mal_id": 59489, "title_romaji": "Kaijuu 8-gou Movie", "title_english": "Kaiju No. 8: Mission Recon", "title_native": "\u602a\u73638\u53f7 \u7b2c1\u671f\u7dcf\u96c6\u7de8", "format": "MOVIE", "start_date": {"year": 2025, "month": 3, "day": 28}, "season": "WINTER", "season_year": 2025, "episodes": 1, "relation_type_from_parent": null, "inferred_part": null, "is_season": false, "is_final_from_title": false, "title_display": "Kaiju No. 8: Mission Recon", "_start_tuple": [2025, 3, 28], "is_final_season": false}], "season_groups": [{"season_index": 1, "group_label": "Season 1", "parts": [{"short_label": "S1", "title": "Kaiju No. 8", "mal_id": 52588, "anilist_id": 153288, "start_date": {"year": 2024, "month": 4, "day": 13}, "format": "TV", "is_final": false}]}, {"season_index": 2, "group_label": "Season 2", "parts": [{"short_label": "S2", "title": "Kaiju No. 8 Season 2", "mal_id": 59177, "anilist_id": 178754, "start_date": {"year": 2025, "month": 7, "day": 19}, "format": "TV", "is_final": false}]}], "extras": [{"title": "Kaijuu 8-gou: Kanketsu-hen", "mal_id": 63136, "anilist_id": 204362, "format": null, "start_date": {"year": null, "month": null, "day": null}}, {"title": "Kaiju No. 8: Mission Recon", "mal_id": 59489, "anilist_id": 179998, "format": "MOVIE", "start_date": {"year": 2025, "month": 3, "day": 28}}]}, "_timestamp": 1775001076.475924}
|
||||||
1
data/cache/generic/e0160811950ce1dda6fcc468d4eb1c77.json
vendored
Normal file
1
data/cache/generic/e0160811950ce1dda6fcc468d4eb1c77.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
data/cache/generic/f7c098cf54b66ed89c8d844335ba1493.json
vendored
Normal file
1
data/cache/generic/f7c098cf54b66ed89c8d844335ba1493.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"payload": {"entries": [{"anilist_id": 171627, "mal_id": 57555, "title_romaji": "Chainsaw Man: Reze-hen", "title_english": "Chainsaw Man \u2013 The Movie: Reze Arc", "title_native": "\u30c1\u30a7\u30f3\u30bd\u30fc\u30de\u30f3 \u30ec\u30bc\u7bc7", "format": "MOVIE", "start_date": {"year": 2025, "month": 9, "day": 19}, "season": "SUMMER", "season_year": 2025, "episodes": 1, "relation_type_from_parent": null, "inferred_part": null, "is_season": false, "is_final_from_title": false, "title_display": "Chainsaw Man \u2013 The Movie: Reze Arc", "_start_tuple": [2025, 9, 19], "is_final_season": false}, {"anilist_id": 204429, "mal_id": 63143, "title_romaji": "Chainsaw Man: Shikaku-hen", "title_english": null, "title_native": "\u30c1\u30a7\u30f3\u30bd\u30fc\u30de\u30f3 \u523a\u5ba2\u7bc7", "format": null, "start_date": {"year": null, "month": null, "day": null}, "season": null, "season_year": null, "episodes": null, "relation_type_from_parent": null, "inferred_part": null, "is_season": false, "is_final_from_title": false, "title_display": "Chainsaw Man: Shikaku-hen", "_start_tuple": [0, 0, 0], "is_final_season": false}, {"anilist_id": 127230, "mal_id": 44511, "title_romaji": "Chainsaw Man", "title_english": "Chainsaw Man", "title_native": "\u30c1\u30a7\u30f3\u30bd\u30fc\u30de\u30f3", "format": "TV", "start_date": {"year": 2022, "month": 10, "day": 12}, "season": "FALL", "season_year": 2022, "episodes": 12, "relation_type_from_parent": null, "inferred_part": null, "is_season": true, "is_final_from_title": false, "title_display": "Chainsaw Man", "_start_tuple": [2022, 10, 12], "is_final_season": false}], "season_groups": [{"season_index": 1, "group_label": "Season 1", "parts": [{"short_label": "S1", "title": "Chainsaw Man", "mal_id": 44511, "anilist_id": 127230, "start_date": {"year": 2022, "month": 10, "day": 12}, "format": "TV", "is_final": false}]}], "extras": [{"title": "Chainsaw Man: Shikaku-hen", "mal_id": 63143, "anilist_id": 204429, "format": null, "start_date": {"year": null, "month": null, "day": null}}, {"title": "Chainsaw Man \u2013 The Movie: Reze Arc", "mal_id": 57555, "anilist_id": 171627, "format": "MOVIE", "start_date": {"year": 2025, "month": 9, "day": 19}}]}, "_timestamp": 1775008693.3776262}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user