262 lines
7.6 KiB
Python
262 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
ANIMESCHEDULE_API_BASE = "https://animeschedule.net/api/v3"
|
|
|
|
|
|
def normalize_title(text: str) -> str:
|
|
cleaned = "".join(ch.lower() if ch.isalnum() else " " for ch in (text or ""))
|
|
return " ".join(cleaned.split())
|
|
|
|
|
|
def flatten_items(payload: dict | list) -> list[dict]:
|
|
if isinstance(payload, list):
|
|
return [item for item in payload if isinstance(item, dict)]
|
|
if not isinstance(payload, dict):
|
|
return []
|
|
|
|
for key in ("anime", "items", "data", "results"):
|
|
value = payload.get(key)
|
|
if isinstance(value, list):
|
|
return [item for item in value if isinstance(item, dict)]
|
|
return []
|
|
|
|
|
|
def extract_names(item: dict) -> list[str]:
|
|
names = []
|
|
direct = [item.get("title"), item.get("name"), item.get("romaji"), item.get("english"), item.get("native")]
|
|
for candidate in direct:
|
|
text = (candidate or "").strip()
|
|
if text:
|
|
names.append(text)
|
|
|
|
nested = item.get("names") or {}
|
|
if isinstance(nested, dict):
|
|
for candidate in nested.values():
|
|
if isinstance(candidate, list):
|
|
for value in candidate:
|
|
text = (str(value) if value is not None else "").strip()
|
|
if text:
|
|
names.append(text)
|
|
continue
|
|
|
|
text = (str(candidate) if candidate is not None else "").strip()
|
|
if text:
|
|
names.append(text)
|
|
|
|
unique = []
|
|
seen = set()
|
|
for name in names:
|
|
lowered = name.lower()
|
|
if lowered in seen:
|
|
continue
|
|
seen.add(lowered)
|
|
unique.append(name)
|
|
return unique
|
|
|
|
|
|
def extract_url(item: dict) -> str:
|
|
slug = (item.get("slug") or item.get("route") or item.get("id") or "").strip()
|
|
if slug:
|
|
return f"https://animeschedule.net/anime/{slug}"
|
|
|
|
websites = item.get("websites") or item.get("links") or []
|
|
if isinstance(websites, list):
|
|
for entry in websites:
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
url = (entry.get("url") or "").strip()
|
|
if url:
|
|
return url
|
|
return ""
|
|
|
|
|
|
def extract_english_title(item: dict) -> str:
|
|
direct = [item.get("english"), item.get("titleEnglish"), item.get("englishTitle")]
|
|
for candidate in direct:
|
|
text = (candidate or "").strip()
|
|
if text:
|
|
return text
|
|
|
|
names = item.get("names") or {}
|
|
if isinstance(names, dict):
|
|
for key, value in names.items():
|
|
key_norm = str(key).strip().lower()
|
|
if key_norm in {"en", "eng", "english", "titleenglish"}:
|
|
text = (str(value) if value is not None else "").strip()
|
|
if text:
|
|
return text
|
|
return ""
|
|
|
|
|
|
def extract_list(item: dict, *keys: str) -> str:
|
|
for key in keys:
|
|
value = item.get(key)
|
|
if not value:
|
|
continue
|
|
if isinstance(value, str):
|
|
text = value.strip()
|
|
if text:
|
|
return text
|
|
if isinstance(value, list):
|
|
names = []
|
|
for entry in value:
|
|
if isinstance(entry, str):
|
|
text = entry.strip()
|
|
elif isinstance(entry, dict):
|
|
text = (entry.get("name") or entry.get("title") or "").strip()
|
|
else:
|
|
text = ""
|
|
if text:
|
|
names.append(text)
|
|
if names:
|
|
return ", ".join(names)
|
|
return ""
|
|
|
|
|
|
def extract_format(item: dict) -> str:
|
|
for key in ("format", "mediaType", "type"):
|
|
value = item.get(key)
|
|
text = (str(value) if value is not None else "").strip()
|
|
if text:
|
|
return text
|
|
return ""
|
|
|
|
|
|
def extract_description(item: dict) -> str:
|
|
for key in ("description", "synopsis", "overview", "summary"):
|
|
value = item.get(key)
|
|
text = (str(value) if value is not None else "").strip()
|
|
if text:
|
|
return text
|
|
|
|
details = item.get("details") or {}
|
|
if isinstance(details, dict):
|
|
for key in ("description", "synopsis", "overview", "summary"):
|
|
value = details.get(key)
|
|
text = (str(value) if value is not None else "").strip()
|
|
if text:
|
|
return text
|
|
return ""
|
|
|
|
|
|
def empty_result() -> dict:
|
|
return {
|
|
"url": "",
|
|
"title": "",
|
|
"english_title": "",
|
|
"format": "",
|
|
"genres": "",
|
|
"studios": "",
|
|
"description": "",
|
|
"names": [],
|
|
"match_score": 0,
|
|
"raw": {},
|
|
}
|
|
|
|
|
|
def title_match_score(wanted: str, names: list[str]) -> int:
|
|
wanted_norm = normalize_title(wanted)
|
|
if not wanted_norm:
|
|
return 0
|
|
|
|
best = 0
|
|
wanted_parts = wanted_norm.split(" ")
|
|
|
|
for candidate in names:
|
|
normalized = normalize_title(candidate)
|
|
if not normalized:
|
|
continue
|
|
|
|
if normalized == wanted_norm:
|
|
best = max(best, 4)
|
|
continue
|
|
|
|
if wanted_norm in normalized or normalized in wanted_norm:
|
|
best = max(best, 3)
|
|
continue
|
|
|
|
normalized_parts = normalized.split(" ")
|
|
if normalized_parts[:2] == wanted_parts[:2] and len(normalized_parts) >= 2 and len(wanted_parts) >= 2:
|
|
best = max(best, 2)
|
|
continue
|
|
|
|
if set(normalized_parts) & set(wanted_parts):
|
|
best = max(best, 1)
|
|
|
|
return best
|
|
|
|
|
|
def fetch_animeschedule_anime_by_title(title: str, token: str, cache: dict[str, dict]) -> dict:
|
|
key = normalize_title(title)
|
|
if key in cache:
|
|
return cache[key]
|
|
|
|
if not token:
|
|
cache[key] = empty_result()
|
|
return cache[key]
|
|
|
|
params = {"search": title, "take": "10"}
|
|
url = ANIMESCHEDULE_API_BASE + "/anime?" + urllib.parse.urlencode(params)
|
|
req = urllib.request.Request(
|
|
url,
|
|
headers={
|
|
"Accept": "application/json",
|
|
"Authorization": f"Bearer {token}",
|
|
"User-Agent": "anime-movies-script/1.0",
|
|
},
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
payload = json.loads(resp.read().decode("utf-8"))
|
|
except Exception:
|
|
cache[key] = empty_result()
|
|
return cache[key]
|
|
|
|
candidates = flatten_items(payload)
|
|
if not candidates:
|
|
cache[key] = empty_result()
|
|
return cache[key]
|
|
|
|
best = None
|
|
best_score = -1
|
|
|
|
for item in candidates:
|
|
names = extract_names(item)
|
|
if not names:
|
|
continue
|
|
|
|
score = title_match_score(title, names)
|
|
if score <= 0:
|
|
continue
|
|
|
|
if score > best_score:
|
|
best_score = score
|
|
best = item
|
|
if score == 4:
|
|
break
|
|
|
|
if not best:
|
|
cache[key] = empty_result()
|
|
return cache[key]
|
|
|
|
names = extract_names(best)
|
|
cache[key] = {
|
|
"url": extract_url(best),
|
|
"title": names[0] if names else "",
|
|
"english_title": extract_english_title(best),
|
|
"format": extract_format(best),
|
|
"genres": extract_list(best, "genres", "genre", "categories"),
|
|
"studios": extract_list(best, "studios", "studio"),
|
|
"description": extract_description(best),
|
|
"names": names,
|
|
"match_score": best_score,
|
|
"raw": best,
|
|
}
|
|
return cache[key]
|