finalising the Querries and the rest
This commit is contained in:
@@ -4,30 +4,13 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
|
|
||||||
ANILIST_URL = "https://graphql.anilist.co"
|
ANILIST_URL = "https://graphql.anilist.co"
|
||||||
|
|
||||||
|
|
||||||
def normalize_title(text: str) -> str:
|
def yyyymmdd(value: date) -> int:
|
||||||
cleaned = "".join(ch.lower() if ch.isalnum() else " " for ch in (text or ""))
|
return value.year * 10000 + value.month * 100 + value.day
|
||||||
return " ".join(cleaned.split())
|
|
||||||
|
|
||||||
|
|
||||||
def safe_date(sd: dict | None) -> date | None:
|
|
||||||
if not sd:
|
|
||||||
return None
|
|
||||||
year = sd.get("year") or 0
|
|
||||||
month = sd.get("month") or 0
|
|
||||||
day = sd.get("day") or 0
|
|
||||||
if year <= 0 or month <= 0:
|
|
||||||
return None
|
|
||||||
if day <= 0:
|
|
||||||
day = 1
|
|
||||||
try:
|
|
||||||
return date(year, month, day)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def post_graphql(query: str, variables: dict) -> dict:
|
def post_graphql(query: str, variables: dict) -> dict:
|
||||||
@@ -46,78 +29,69 @@ def post_graphql(query: str, variables: dict) -> dict:
|
|||||||
raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
|
raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
|
||||||
|
|
||||||
|
|
||||||
def pick_best_title(title: dict) -> str:
|
def map_anilist_movie(media: dict | None) -> dict:
|
||||||
english = (title.get("english") or "").strip()
|
|
||||||
if english:
|
|
||||||
return english
|
|
||||||
romaji = (title.get("romaji") or "").strip()
|
|
||||||
if romaji:
|
|
||||||
return romaji
|
|
||||||
native = (title.get("native") or "").strip()
|
|
||||||
return native or ""
|
|
||||||
|
|
||||||
|
|
||||||
def map_anilist_media(media: dict | None) -> dict:
|
|
||||||
media = media or {}
|
media = media or {}
|
||||||
title = media.get("title") or {}
|
title = media.get("title") or {}
|
||||||
|
start_date = media.get("startDate") or {}
|
||||||
studios = ((media.get("studios") or {}).get("nodes") or [])
|
studios = ((media.get("studios") or {}).get("nodes") or [])
|
||||||
studio_names = [((node or {}).get("name") or "").strip() for node in studios]
|
|
||||||
studio_names = [name for name in studio_names if name]
|
|
||||||
|
|
||||||
genres = [str(item).strip() for item in (media.get("genres") or [])]
|
studio_names = []
|
||||||
genres = [g for g in genres if g]
|
for node in studios:
|
||||||
|
name = ((node or {}).get("name") or "").strip()
|
||||||
|
if name:
|
||||||
|
studio_names.append(name)
|
||||||
|
|
||||||
|
genres = []
|
||||||
|
for entry in media.get("genres") or []:
|
||||||
|
text = str(entry or "").strip()
|
||||||
|
if text:
|
||||||
|
genres.append(text)
|
||||||
|
|
||||||
tags_raw = media.get("tags") or []
|
|
||||||
tags = []
|
tags = []
|
||||||
for tag in tags_raw:
|
for tag in media.get("tags") or []:
|
||||||
if not isinstance(tag, dict):
|
if not isinstance(tag, dict):
|
||||||
continue
|
continue
|
||||||
name = (tag.get("name") or "").strip()
|
name = (tag.get("name") or "").strip()
|
||||||
rank = int(tag.get("rank") or 0)
|
rank = int(tag.get("rank") or 0)
|
||||||
if name:
|
if name and rank >= 70:
|
||||||
tags.append({"name": name, "rank": rank})
|
tags.append(name)
|
||||||
|
|
||||||
mapped = {
|
return {
|
||||||
"id": media.get("id"),
|
"anilist_id": int(media.get("id") or 0),
|
||||||
"title_best": pick_best_title(title),
|
|
||||||
"title_english": (title.get("english") or "").strip(),
|
"title_english": (title.get("english") or "").strip(),
|
||||||
"title_romaji": (title.get("romaji") or "").strip(),
|
"title_romaji": (title.get("romaji") or "").strip(),
|
||||||
"title_native": (title.get("native") or "").strip(),
|
"title_native": (title.get("native") or "").strip(),
|
||||||
"start_date": safe_date(media.get("startDate")),
|
|
||||||
"format": (media.get("format") or "").strip(),
|
|
||||||
"episodes": media.get("episodes"),
|
|
||||||
"duration": media.get("duration"),
|
|
||||||
"source": str(media.get("source") or "").replace("_", " ").title(),
|
|
||||||
"description": (media.get("description") or "").strip(),
|
|
||||||
"genres": genres,
|
|
||||||
"genres_text": ", ".join(genres) if genres else "",
|
|
||||||
"tags": tags,
|
|
||||||
"tags_text": ", ".join(tag["name"] for tag in tags if tag["rank"] >= 70) or "",
|
|
||||||
"studio_names": studio_names,
|
|
||||||
"studio_text": ", ".join(studio_names) if studio_names else "",
|
|
||||||
"anilist_url": (media.get("siteUrl") or "").strip(),
|
|
||||||
"cover_image": (((media.get("coverImage") or {}).get("large")) or "").strip(),
|
"cover_image": (((media.get("coverImage") or {}).get("large")) or "").strip(),
|
||||||
"raw": media,
|
"anilist_url": (media.get("siteUrl") or "").strip(),
|
||||||
|
"start_year": int(start_date.get("year") or 0),
|
||||||
|
"format": (media.get("format") or "").strip() or "MOVIE",
|
||||||
|
"description": (media.get("description") or "").strip(),
|
||||||
|
"genres_text": ", ".join(genres),
|
||||||
|
"tags_text": ", ".join(tags),
|
||||||
|
"studio_text": ", ".join(studio_names),
|
||||||
}
|
}
|
||||||
return mapped
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_anilist_movie_by_search(search_text: str, cache: dict[str, dict | None]) -> dict | None:
|
def fetch_anilist_movie_candidates(today: date, years_window: int = 1) -> list[dict]:
|
||||||
key = normalize_title(search_text)
|
start_date = today - timedelta(days=365 * years_window)
|
||||||
if key in cache:
|
end_date = today + timedelta(days=365 * years_window)
|
||||||
return cache[key]
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
query ($search: String, $perPage: Int) {
|
query ($page: Int, $perPage: Int, $start: FuzzyDateInt, $end: FuzzyDateInt) {
|
||||||
Page(page: 1, perPage: $perPage) {
|
Page(page: $page, perPage: $perPage) {
|
||||||
media(type: ANIME, format: MOVIE, search: $search, sort: [SEARCH_MATCH, POPULARITY_DESC]) {
|
pageInfo { hasNextPage }
|
||||||
|
media(
|
||||||
|
type: ANIME
|
||||||
|
format: MOVIE
|
||||||
|
countryOfOrigin: JP
|
||||||
|
sort: [POPULARITY_DESC, START_DATE_DESC]
|
||||||
|
startDate_greater: $start
|
||||||
|
startDate_lesser: $end
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
title { english romaji native }
|
title { english romaji native }
|
||||||
startDate { year month day }
|
startDate { year }
|
||||||
format
|
format
|
||||||
episodes
|
|
||||||
duration
|
|
||||||
source
|
|
||||||
description(asHtml: false)
|
description(asHtml: false)
|
||||||
genres
|
genres
|
||||||
tags { name rank }
|
tags { name rank }
|
||||||
@@ -129,48 +103,39 @@ def fetch_anilist_movie_by_search(search_text: str, cache: dict[str, dict | None
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
page = 1
|
||||||
data = post_graphql(query, {"search": search_text, "perPage": 5})
|
results = []
|
||||||
except Exception:
|
seen_ids = set()
|
||||||
cache[key] = None
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "errors" in data:
|
while True:
|
||||||
cache[key] = None
|
payload = post_graphql(
|
||||||
return None
|
query,
|
||||||
|
{
|
||||||
|
"page": page,
|
||||||
|
"perPage": 50,
|
||||||
|
"start": yyyymmdd(start_date) - 1,
|
||||||
|
"end": yyyymmdd(end_date) + 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
candidates = data.get("data", {}).get("Page", {}).get("media", [])
|
if "errors" in payload:
|
||||||
if not candidates:
|
raise RuntimeError(payload["errors"])
|
||||||
cache[key] = None
|
|
||||||
return None
|
|
||||||
|
|
||||||
wanted = normalize_title(search_text)
|
page_data = payload.get("data", {}).get("Page", {})
|
||||||
best = None
|
for media in page_data.get("media", []):
|
||||||
best_score = -1
|
mapped = map_anilist_movie(media)
|
||||||
for media in candidates:
|
anilist_id = mapped.get("anilist_id") or 0
|
||||||
title = media.get("title") or {}
|
if anilist_id <= 0 or anilist_id in seen_ids:
|
||||||
options = [title.get("english"), title.get("romaji"), title.get("native")]
|
|
||||||
score = 0
|
|
||||||
for option in options:
|
|
||||||
normalized = normalize_title(str(option or ""))
|
|
||||||
if not normalized:
|
|
||||||
continue
|
continue
|
||||||
if normalized == wanted:
|
seen_ids.add(anilist_id)
|
||||||
score = max(score, 3)
|
|
||||||
elif wanted and (wanted in normalized or normalized in wanted):
|
|
||||||
score = max(score, 2)
|
|
||||||
elif normalized.split(" ")[:2] == wanted.split(" ")[:2]:
|
|
||||||
score = max(score, 1)
|
|
||||||
if score > best_score:
|
|
||||||
best_score = score
|
|
||||||
best = media
|
|
||||||
if score == 3:
|
|
||||||
break
|
|
||||||
|
|
||||||
if best_score <= 0:
|
if not mapped.get("title_english") and not mapped.get("title_romaji") and not mapped.get("title_native"):
|
||||||
cache[key] = None
|
continue
|
||||||
return None
|
|
||||||
|
|
||||||
mapped = map_anilist_media(best) if best else None
|
results.append(mapped)
|
||||||
cache[key] = mapped
|
|
||||||
return mapped
|
if not page_data.get("pageInfo", {}).get("hasNextPage"):
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import urllib.request
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
TMDB_API_BASE = "https://api.themoviedb.org/3"
|
TMDB_API_BASE = "https://api.themoviedb.org/3"
|
||||||
TMDB_ANIME_KEYWORD_ID = "210024"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_release_date(value: str) -> date | None:
|
def parse_release_date(value: str) -> date | None:
|
||||||
@@ -21,7 +20,10 @@ def parse_release_date(value: str) -> date | None:
|
|||||||
|
|
||||||
|
|
||||||
def tmdb_get_json(path: str, params: dict, token: str) -> dict:
|
def tmdb_get_json(path: str, params: dict, token: str) -> dict:
|
||||||
url = TMDB_API_BASE + path + "?" + urllib.parse.urlencode(params)
|
url = TMDB_API_BASE + path
|
||||||
|
if params:
|
||||||
|
url += "?" + urllib.parse.urlencode(params)
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
headers={
|
headers={
|
||||||
@@ -30,89 +32,74 @@ def tmdb_get_json(path: str, params: dict, token: str) -> dict:
|
|||||||
"User-Agent": "anime-movies-script/1.0",
|
"User-Agent": "anime-movies-script/1.0",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||||
return json.loads(resp.read().decode("utf-8"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def poster_url(path: str) -> str:
|
def search_tmdb_movies(title: str, token: str, language: str = "en-US") -> list[dict]:
|
||||||
text = (path or "").strip()
|
query = (title or "").strip()
|
||||||
if not text:
|
if not query:
|
||||||
return ""
|
|
||||||
return "https://image.tmdb.org/t/p/w780" + text
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_genre_map(token: str, language: str = "de-DE") -> dict[int, str]:
|
|
||||||
payload = tmdb_get_json("/genre/movie/list", {"language": language}, token)
|
|
||||||
result = {}
|
|
||||||
for entry in payload.get("genres", []):
|
|
||||||
try:
|
|
||||||
gid = int(entry.get("id"))
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
name = (entry.get("name") or "").strip()
|
|
||||||
if name:
|
|
||||||
result[gid] = name
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_tmdb_anime_movies(start_date: date, end_date: date, token: str, language: str = "de-DE") -> list[dict]:
|
|
||||||
if not token:
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
genre_map = fetch_genre_map(token, language=language)
|
payload = tmdb_get_json(
|
||||||
page = 1
|
"/search/movie",
|
||||||
results = []
|
{
|
||||||
seen_ids = set()
|
"query": query,
|
||||||
|
|
||||||
while True:
|
|
||||||
params = {
|
|
||||||
"include_adult": "false",
|
"include_adult": "false",
|
||||||
"include_video": "false",
|
|
||||||
"language": language,
|
"language": language,
|
||||||
"sort_by": "primary_release_date.desc",
|
"page": "1",
|
||||||
"primary_release_date.gte": start_date.isoformat(),
|
},
|
||||||
"primary_release_date.lte": end_date.isoformat(),
|
token,
|
||||||
"with_keywords": TMDB_ANIME_KEYWORD_ID,
|
)
|
||||||
"page": str(page),
|
|
||||||
}
|
|
||||||
payload = tmdb_get_json("/discover/movie", params, token)
|
|
||||||
page_results = payload.get("results", [])
|
|
||||||
if not page_results:
|
|
||||||
break
|
|
||||||
|
|
||||||
for movie in page_results:
|
results = []
|
||||||
movie_id = movie.get("id")
|
for item in payload.get("results", []):
|
||||||
if movie_id in seen_ids:
|
movie_id = item.get("id")
|
||||||
continue
|
if not movie_id:
|
||||||
|
continue
|
||||||
|
|
||||||
release_date = parse_release_date(str(movie.get("release_date") or ""))
|
results.append(
|
||||||
if not release_date:
|
{
|
||||||
continue
|
"tmdb_id": int(movie_id),
|
||||||
|
"title": (item.get("title") or "").strip(),
|
||||||
seen_ids.add(movie_id)
|
"original_title": (item.get("original_title") or "").strip(),
|
||||||
genre_names = [genre_map.get(gid, "") for gid in (movie.get("genre_ids") or [])]
|
"release_date": parse_release_date(str(item.get("release_date") or "")),
|
||||||
genre_names = [name for name in genre_names if name]
|
}
|
||||||
|
)
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"tmdb_id": movie_id,
|
|
||||||
"title": (movie.get("title") or "").strip(),
|
|
||||||
"original_title": (movie.get("original_title") or "").strip(),
|
|
||||||
"overview": (movie.get("overview") or "").strip(),
|
|
||||||
"release_date": release_date,
|
|
||||||
"genres": genre_names,
|
|
||||||
"genres_text": ", ".join(genre_names),
|
|
||||||
"poster_url": poster_url(str(movie.get("poster_path") or "")),
|
|
||||||
"popularity": movie.get("popularity"),
|
|
||||||
"vote_average": movie.get("vote_average"),
|
|
||||||
"vote_count": movie.get("vote_count"),
|
|
||||||
"raw": movie,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
total_pages = int(payload.get("total_pages") or page)
|
|
||||||
if page >= total_pages:
|
|
||||||
break
|
|
||||||
page += 1
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_tmdb_release_dates(movie_id: int, token: str) -> dict:
|
||||||
|
return tmdb_get_json(f"/movie/{movie_id}/release_dates", {}, token)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_de_theatrical_dates(release_payload: dict) -> list[date]:
|
||||||
|
german_block = None
|
||||||
|
for result in release_payload.get("results", []):
|
||||||
|
if str(result.get("iso_3166_1") or "").upper() == "DE":
|
||||||
|
german_block = result
|
||||||
|
break
|
||||||
|
|
||||||
|
if not german_block:
|
||||||
|
return []
|
||||||
|
|
||||||
|
dates = []
|
||||||
|
for entry in german_block.get("release_dates", []):
|
||||||
|
release_type = int(entry.get("type") or 0)
|
||||||
|
if release_type not in {2, 3}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
release_date = parse_release_date(str(entry.get("release_date") or ""))
|
||||||
|
if release_date:
|
||||||
|
dates.append(release_date)
|
||||||
|
|
||||||
|
return sorted(dates)
|
||||||
|
|
||||||
|
|
||||||
|
def select_release_in_range(release_dates: list[date], start_date: date, end_date: date) -> date | None:
|
||||||
|
for release_date in sorted(release_dates):
|
||||||
|
if start_date <= release_date <= end_date:
|
||||||
|
return release_date
|
||||||
|
return None
|
||||||
|
|||||||
@@ -2,147 +2,213 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
from app_config import get_settings
|
from app_config import get_settings
|
||||||
from data_sources.anilist_source import fetch_anilist_movie_by_search
|
from data_sources.anilist_source import fetch_anilist_movie_candidates
|
||||||
from data_sources.animeschedule_source import fetch_animeschedule_anime_by_title
|
from data_sources.animeschedule_source import fetch_animeschedule_anime_by_title
|
||||||
from data_sources.tmdb_source import fetch_tmdb_anime_movies
|
from data_sources.tmdb_source import (
|
||||||
|
extract_de_theatrical_dates,
|
||||||
|
fetch_tmdb_release_dates,
|
||||||
|
search_tmdb_movies,
|
||||||
|
select_release_in_range,
|
||||||
|
)
|
||||||
|
|
||||||
|
FUZZY_MATCH_THRESHOLD = 0.65
|
||||||
|
SCHEDULE_MATCH_THRESHOLD = 2
|
||||||
|
|
||||||
|
|
||||||
def add_months(d: date, months: int) -> date:
|
def add_months(value: date, months: int) -> date:
|
||||||
year = d.year + ((d.month - 1 + months) // 12)
|
year = value.year + ((value.month - 1 + months) // 12)
|
||||||
month = ((d.month - 1 + months) % 12) + 1
|
month = ((value.month - 1 + months) % 12) + 1
|
||||||
return date(year, month, 1)
|
return date(year, month, 1)
|
||||||
|
|
||||||
|
|
||||||
def format_date(d: date, locale: str) -> str:
|
def normalize_title(text: str) -> str:
|
||||||
|
cleaned = "".join(ch.lower() if ch.isalnum() else " " for ch in (text or ""))
|
||||||
|
return " ".join(cleaned.split())
|
||||||
|
|
||||||
|
|
||||||
|
def fuzzy_ratio(left: str, right: str) -> float:
|
||||||
|
a = normalize_title(left)
|
||||||
|
b = normalize_title(right)
|
||||||
|
if not a or not b:
|
||||||
|
return 0.0
|
||||||
|
return SequenceMatcher(None, a, b).ratio()
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(value: date, locale: str) -> str:
|
||||||
if locale == "de-DE":
|
if locale == "de-DE":
|
||||||
return f"{d.day:02d}.{d.month:02d}.{d.year}"
|
return f"{value.day:02d}.{value.month:02d}.{value.year}"
|
||||||
return d.isoformat()
|
return value.isoformat()
|
||||||
|
|
||||||
|
|
||||||
def select_title(anilist: dict, schedule: dict, tmdb: dict) -> str:
|
def parse_record_release_date(record: dict) -> date:
|
||||||
return (
|
text = str(record.get("releaseDate") or "").strip()
|
||||||
(schedule.get("english_title") or "").strip()
|
try:
|
||||||
or (anilist.get("title_english") or "").strip()
|
return date.fromisoformat(text)
|
||||||
or (anilist.get("title_best") or "").strip()
|
except ValueError:
|
||||||
or (tmdb.get("title") or "").strip()
|
return date.max
|
||||||
or (tmdb.get("original_title") or "").strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def structure_movie_record(tmdb_movie: dict, anilist: dict, schedule: dict, locale: str) -> dict:
|
def is_year_match(anilist_year: int, tmdb_year: int) -> bool:
|
||||||
release_date = tmdb_movie.get("release_date")
|
if anilist_year <= 0 or tmdb_year <= 0:
|
||||||
release = format_date(release_date, locale) if isinstance(release_date, date) else "n/a"
|
return False
|
||||||
|
return abs(anilist_year - tmdb_year) <= 1
|
||||||
|
|
||||||
has_anilist = bool(anilist)
|
|
||||||
has_schedule = bool(schedule and (schedule.get("title") or schedule.get("english_title") or schedule.get("url")))
|
|
||||||
|
|
||||||
final_description = (
|
def title_score(anilist_english: str, anilist_romaji: str, anilist_native: str, tmdb_title: str, tmdb_original: str) -> tuple[int, int, int, float]:
|
||||||
(tmdb_movie.get("overview") or "").strip()
|
english_norm = normalize_title(anilist_english)
|
||||||
or (anilist.get("description") or "").strip()
|
romaji_norm = normalize_title(anilist_romaji)
|
||||||
or (schedule.get("description") or "").strip()
|
native_norm = normalize_title(anilist_native)
|
||||||
or "n/a"
|
tmdb_options = [normalize_title(tmdb_title), normalize_title(tmdb_original)]
|
||||||
)
|
|
||||||
|
|
||||||
# User requirement: cover images should come from AniList.
|
exact_english = 1 if english_norm and english_norm in tmdb_options else 0
|
||||||
cover_image = (anilist.get("cover_image") or "").strip()
|
exact_romaji = 1 if romaji_norm and romaji_norm in tmdb_options else 0
|
||||||
|
exact_native = 1 if native_norm and native_norm in tmdb_options else 0
|
||||||
|
|
||||||
genres_text = (
|
best_ratio = 0.0
|
||||||
(tmdb_movie.get("genres_text") or "").strip()
|
for option in (tmdb_title, tmdb_original):
|
||||||
or (anilist.get("genres_text") or "").strip()
|
best_ratio = max(
|
||||||
or (schedule.get("genres") or "").strip()
|
best_ratio,
|
||||||
or "n/a"
|
fuzzy_ratio(anilist_english, option),
|
||||||
)
|
fuzzy_ratio(anilist_romaji, option),
|
||||||
|
fuzzy_ratio(anilist_native, option),
|
||||||
|
)
|
||||||
|
|
||||||
record = {
|
return exact_english, exact_romaji, exact_native, best_ratio
|
||||||
"title": select_title(anilist, schedule, tmdb_movie),
|
|
||||||
"title_english_anilist": (anilist.get("title_english") or "").strip(),
|
|
||||||
"title_anilist": (anilist.get("title_best") or "").strip(),
|
def collect_tmdb_candidates_for_anilist(anilist_entry: dict, tmdb_token: str, search_cache: dict[str, list[dict]]) -> list[dict]:
|
||||||
"title_schedule_english": (schedule.get("english_title") or "").strip(),
|
english = (anilist_entry.get("title_english") or "").strip()
|
||||||
"title_romaji": (anilist.get("title_romaji") or "").strip() or "n/a",
|
romaji = (anilist_entry.get("title_romaji") or "").strip()
|
||||||
"title_native": (anilist.get("title_native") or "").strip() or "n/a",
|
native = (anilist_entry.get("title_native") or "").strip()
|
||||||
"studio": (anilist.get("studio_text") or "").strip() or (schedule.get("studios") or "").strip() or "n/a",
|
|
||||||
"genres": genres_text,
|
queries = []
|
||||||
"tags": (anilist.get("tags_text") or "").strip() or "n/a",
|
if english:
|
||||||
"release": release,
|
queries.append(english)
|
||||||
"anilist_url": (anilist.get("anilist_url") or "").strip() or "n/a",
|
if romaji and normalize_title(romaji) != normalize_title(english):
|
||||||
"format": (anilist.get("format") or "").strip() or (schedule.get("format") or "").strip() or "MOVIE",
|
queries.append(romaji)
|
||||||
"episodes": anilist.get("episodes") or "n/a",
|
if native and normalize_title(native) not in {normalize_title(english), normalize_title(romaji)}:
|
||||||
"duration": anilist.get("duration") or "n/a",
|
queries.append(native)
|
||||||
"source": (anilist.get("source") or "").strip() or "n/a",
|
|
||||||
"cover_image": cover_image,
|
if not queries:
|
||||||
"description": final_description,
|
return []
|
||||||
"schedule_url": (schedule.get("url") or "").strip(),
|
|
||||||
"schedule_title": (schedule.get("title") or "").strip(),
|
candidates_by_id = {}
|
||||||
"release_source": "TMDb DE",
|
languages = ["de-DE", "en-US", "ja-JP"]
|
||||||
"source_presence": {
|
|
||||||
"tmdb": True,
|
for query in queries:
|
||||||
"anilist": has_anilist,
|
normalized_query = normalize_title(query)
|
||||||
"animeschedule": has_schedule,
|
for language in languages:
|
||||||
|
cache_key = f"{language}:{normalized_query}"
|
||||||
|
if cache_key not in search_cache:
|
||||||
|
search_cache[cache_key] = search_tmdb_movies(query, tmdb_token, language=language)
|
||||||
|
|
||||||
|
for item in search_cache[cache_key]:
|
||||||
|
tmdb_id = int(item.get("tmdb_id") or 0)
|
||||||
|
if tmdb_id <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
existing = candidates_by_id.get(tmdb_id)
|
||||||
|
if not existing:
|
||||||
|
candidates_by_id[tmdb_id] = item
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Prefer candidate carrying a release date if duplicate appears from different language queries.
|
||||||
|
if not existing.get("release_date") and item.get("release_date"):
|
||||||
|
candidates_by_id[tmdb_id] = item
|
||||||
|
|
||||||
|
return list(candidates_by_id.values())
|
||||||
|
|
||||||
|
|
||||||
|
def pick_best_tmdb_match(anilist_entry: dict, tmdb_candidates: list[dict]) -> dict | None:
|
||||||
|
english = (anilist_entry.get("title_english") or "").strip()
|
||||||
|
romaji = (anilist_entry.get("title_romaji") or "").strip()
|
||||||
|
native = (anilist_entry.get("title_native") or "").strip()
|
||||||
|
anilist_year = int(anilist_entry.get("start_year") or 0)
|
||||||
|
|
||||||
|
best = None
|
||||||
|
best_tuple = (-1, -1, -1, 0.0)
|
||||||
|
|
||||||
|
for candidate in tmdb_candidates:
|
||||||
|
exact_english, exact_romaji, exact_native, ratio = title_score(
|
||||||
|
english,
|
||||||
|
romaji,
|
||||||
|
native,
|
||||||
|
str(candidate.get("title") or ""),
|
||||||
|
str(candidate.get("original_title") or ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
tmdb_release = candidate.get("release_date")
|
||||||
|
tmdb_year = tmdb_release.year if tmdb_release else 0
|
||||||
|
has_exact_match = exact_english == 1 or exact_romaji == 1 or exact_native == 1
|
||||||
|
if not has_exact_match and not is_year_match(anilist_year, tmdb_year):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ratio < FUZZY_MATCH_THRESHOLD and exact_english == 0 and exact_romaji == 0 and exact_native == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
score_tuple = (exact_english, exact_romaji, exact_native, ratio)
|
||||||
|
if score_tuple > best_tuple:
|
||||||
|
best_tuple = score_tuple
|
||||||
|
best = candidate
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_titles(anilist_entry: dict, schedule_token: str, schedule_cache: dict[str, dict]) -> tuple[str, str]:
|
||||||
|
english = (anilist_entry.get("title_english") or "").strip()
|
||||||
|
romaji = (anilist_entry.get("title_romaji") or "").strip()
|
||||||
|
native = (anilist_entry.get("title_native") or "").strip()
|
||||||
|
schedule_english = ""
|
||||||
|
|
||||||
|
if not english and romaji:
|
||||||
|
schedule = fetch_animeschedule_anime_by_title(romaji, schedule_token, schedule_cache)
|
||||||
|
if int(schedule.get("match_score") or 0) >= SCHEDULE_MATCH_THRESHOLD:
|
||||||
|
schedule_english = (schedule.get("english_title") or "").strip()
|
||||||
|
|
||||||
|
preferred_title = schedule_english or english or romaji or native
|
||||||
|
return preferred_title, schedule_english
|
||||||
|
|
||||||
|
|
||||||
|
def build_record(anilist_entry: dict, tmdb_entry: dict, release_date: date, locale: str, title: str, schedule_english: str) -> dict:
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"title_english_anilist": (anilist_entry.get("title_english") or "").strip(),
|
||||||
|
"title_anilist": (anilist_entry.get("title_english") or "").strip() or (anilist_entry.get("title_romaji") or "").strip(),
|
||||||
|
"title_schedule_english": schedule_english,
|
||||||
|
"title_romaji": (anilist_entry.get("title_romaji") or "").strip(),
|
||||||
|
"title_native": (anilist_entry.get("title_native") or "").strip(),
|
||||||
|
"studio": (anilist_entry.get("studio_text") or "").strip(),
|
||||||
|
"genres": (anilist_entry.get("genres_text") or "").strip() or "n/a",
|
||||||
|
"tags": (anilist_entry.get("tags_text") or "").strip(),
|
||||||
|
"release": format_date(release_date, locale),
|
||||||
|
"releaseDate": release_date.isoformat(),
|
||||||
|
"anilist_url": (anilist_entry.get("anilist_url") or "").strip() or "n/a",
|
||||||
|
"format": (anilist_entry.get("format") or "").strip() or "MOVIE",
|
||||||
|
"cover_image": (anilist_entry.get("cover_image") or "").strip(),
|
||||||
|
"description": (anilist_entry.get("description") or "").strip(),
|
||||||
|
"tmdb_title": (tmdb_entry.get("title") or "").strip(),
|
||||||
|
"tmdb_id": int(tmdb_entry.get("tmdb_id") or 0),
|
||||||
|
"ids": {
|
||||||
|
"anilist": int(anilist_entry.get("anilist_id") or 0),
|
||||||
|
"tmdb": int(tmdb_entry.get("tmdb_id") or 0),
|
||||||
},
|
},
|
||||||
"tmdb": tmdb_movie,
|
|
||||||
"anilist": anilist,
|
|
||||||
"animeschedule": schedule,
|
|
||||||
}
|
}
|
||||||
return record
|
|
||||||
|
|
||||||
|
|
||||||
def sort_and_structure_movies(tmdb_movies: list[dict], locale: str, schedule_token: str) -> list[dict]:
|
def sort_dedup_records(records: list[dict]) -> list[dict]:
|
||||||
records = []
|
unique = {}
|
||||||
anilist_cache: dict[str, dict | None] = {}
|
for record in records:
|
||||||
schedule_cache: dict[str, dict] = {}
|
ids = record.get("ids") or {}
|
||||||
|
key = (int(ids.get("anilist") or 0), int(ids.get("tmdb") or 0), str(record.get("releaseDate") or ""))
|
||||||
|
if key not in unique:
|
||||||
|
unique[key] = record
|
||||||
|
|
||||||
sorted_tmdb = sorted(
|
result = list(unique.values())
|
||||||
tmdb_movies,
|
result.sort(key=lambda item: (parse_record_release_date(item), str(item.get("title") or "")))
|
||||||
key=lambda item: item.get("release_date") or date.min,
|
return result
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for tmdb_movie in sorted_tmdb:
|
|
||||||
anilist = None
|
|
||||||
for candidate in ((tmdb_movie.get("title") or "").strip(), (tmdb_movie.get("original_title") or "").strip()):
|
|
||||||
if not candidate:
|
|
||||||
continue
|
|
||||||
anilist = fetch_anilist_movie_by_search(candidate, anilist_cache)
|
|
||||||
if anilist:
|
|
||||||
break
|
|
||||||
anilist = anilist or {}
|
|
||||||
|
|
||||||
schedule_candidates = [
|
|
||||||
(anilist.get("title_english") or "").strip(),
|
|
||||||
(anilist.get("title_best") or "").strip(),
|
|
||||||
(tmdb_movie.get("title") or "").strip(),
|
|
||||||
(tmdb_movie.get("original_title") or "").strip(),
|
|
||||||
]
|
|
||||||
|
|
||||||
best_schedule = {}
|
|
||||||
best_schedule_score = 0
|
|
||||||
seen_schedule_candidates = set()
|
|
||||||
for schedule_candidate in schedule_candidates:
|
|
||||||
if not schedule_candidate:
|
|
||||||
continue
|
|
||||||
norm_key = schedule_candidate.lower()
|
|
||||||
if norm_key in seen_schedule_candidates:
|
|
||||||
continue
|
|
||||||
seen_schedule_candidates.add(norm_key)
|
|
||||||
|
|
||||||
candidate_result = fetch_animeschedule_anime_by_title(schedule_candidate, schedule_token, schedule_cache)
|
|
||||||
score = int(candidate_result.get("match_score") or 0)
|
|
||||||
if score > best_schedule_score:
|
|
||||||
best_schedule = candidate_result
|
|
||||||
best_schedule_score = score
|
|
||||||
|
|
||||||
# Prefer results that have explicit English title when scores are tied.
|
|
||||||
if score == best_schedule_score and score > 0:
|
|
||||||
if not (best_schedule.get("english_title") or "").strip() and (candidate_result.get("english_title") or "").strip():
|
|
||||||
best_schedule = candidate_result
|
|
||||||
|
|
||||||
schedule = best_schedule if best_schedule_score > 0 else {}
|
|
||||||
|
|
||||||
records.append(structure_movie_record(tmdb_movie, anilist, schedule, locale))
|
|
||||||
|
|
||||||
return records
|
|
||||||
|
|
||||||
|
|
||||||
def get_upcoming_movie_records(
|
def get_upcoming_movie_records(
|
||||||
@@ -155,9 +221,59 @@ def get_upcoming_movie_records(
|
|||||||
schedule_token = (anime_schedule_token or settings.animeschedule_api_token).strip()
|
schedule_token = (anime_schedule_token or settings.animeschedule_api_token).strip()
|
||||||
tmdb_token = (tmdb_read_access_token or settings.tmdb_read_access_token).strip()
|
tmdb_token = (tmdb_read_access_token or settings.tmdb_read_access_token).strip()
|
||||||
|
|
||||||
|
if not tmdb_token:
|
||||||
|
return []
|
||||||
|
|
||||||
current_day = today or date.today()
|
current_day = today or date.today()
|
||||||
month_start = date(current_day.year, current_day.month, 1)
|
month_start = date(current_day.year, current_day.month, 1)
|
||||||
end_date = add_months(month_start, 2) - timedelta(days=1)
|
end_date = add_months(month_start, 2) - timedelta(days=1)
|
||||||
|
|
||||||
tmdb_movies = fetch_tmdb_anime_movies(current_day, end_date, tmdb_token, language=locale)
|
anilist_candidates = fetch_anilist_movie_candidates(current_day)
|
||||||
return sort_and_structure_movies(tmdb_movies, locale, schedule_token)
|
schedule_cache: dict[str, dict] = {}
|
||||||
|
tmdb_search_cache: dict[str, list[dict]] = {}
|
||||||
|
tmdb_release_cache: dict[int, dict] = {}
|
||||||
|
|
||||||
|
output_records = []
|
||||||
|
|
||||||
|
for anilist_entry in anilist_candidates:
|
||||||
|
tmdb_candidates = collect_tmdb_candidates_for_anilist(anilist_entry, tmdb_token, tmdb_search_cache)
|
||||||
|
if not tmdb_candidates:
|
||||||
|
continue
|
||||||
|
|
||||||
|
matched_tmdb = pick_best_tmdb_match(anilist_entry, tmdb_candidates)
|
||||||
|
if not matched_tmdb:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tmdb_id = int(matched_tmdb.get("tmdb_id") or 0)
|
||||||
|
if tmdb_id <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if tmdb_id not in tmdb_release_cache:
|
||||||
|
tmdb_release_cache[tmdb_id] = fetch_tmdb_release_dates(tmdb_id, tmdb_token)
|
||||||
|
|
||||||
|
release_payload = tmdb_release_cache[tmdb_id]
|
||||||
|
de_theatrical_dates = extract_de_theatrical_dates(release_payload)
|
||||||
|
release_date = select_release_in_range(de_theatrical_dates, current_day, end_date)
|
||||||
|
if not release_date:
|
||||||
|
candidate_release = matched_tmdb.get("release_date")
|
||||||
|
if isinstance(candidate_release, date) and current_day <= candidate_release <= end_date:
|
||||||
|
release_date = candidate_release
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
preferred_title, schedule_english = resolve_titles(anilist_entry, schedule_token, schedule_cache)
|
||||||
|
if not preferred_title:
|
||||||
|
continue
|
||||||
|
|
||||||
|
output_records.append(
|
||||||
|
build_record(
|
||||||
|
anilist_entry=anilist_entry,
|
||||||
|
tmdb_entry=matched_tmdb,
|
||||||
|
release_date=release_date,
|
||||||
|
locale=locale,
|
||||||
|
title=preferred_title,
|
||||||
|
schedule_english=schedule_english,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return sort_dedup_records(output_records)
|
||||||
|
|||||||
Reference in New Issue
Block a user