api bugfix

This commit is contained in:
2026-05-23 14:18:44 +02:00
parent 852f6b84ef
commit 3dab98cb41
2 changed files with 178 additions and 67 deletions
+124 -39
View File
@@ -68,6 +68,8 @@ _AGE_RATING_MAP = {
}
_TRACKER_URL_TEMPLATES = {
# Keys are normalised via _normalise_key (alphanumeric only, lowercase),
# so e.g. the source key "anime_news_network" matches "animenewsnetwork".
"anilist": "https://anilist.co/manga/{id}",
"myanimelist": "https://myanimelist.net/manga/{id}",
"mal": "https://myanimelist.net/manga/{id}",
@@ -76,6 +78,8 @@ _TRACKER_URL_TEMPLATES = {
"kitsu": "https://kitsu.app/manga/{id}",
"animenewsnetwork": "https://www.animenewsnetwork.com/encyclopedia/manga.php?id={id}",
"ann": "https://www.animenewsnetwork.com/encyclopedia/manga.php?id={id}",
"animeplanet": "https://www.anime-planet.com/manga/{id}",
"shikimori": "https://shikimori.one/mangas/{id}",
}
# MangaDex relationship types that indicate child works (spin-offs, sequels …)
@@ -96,6 +100,11 @@ def _normalise_key(key) -> str:
return re.sub(r"[^a-z0-9]", "", str(key).lower())
def _format_term(value: str) -> str:
"""Converts a MangaBaka genre slug ('slice_of_life') to display form."""
return str(value).replace("_", " ").strip().title() if value else ""
# --------------------------------------------------------------------------
# Main class
# --------------------------------------------------------------------------
@@ -381,7 +390,10 @@ class ComicInfoBuilder:
add("Imprint", orig_pub)
# ----- Genres / Tags ------------------------------------------------
add("Genre", ", ".join(md.get("genres") or []))
# Genres come back as lowercase snake_case ("slice_of_life"); convert
# to display form ("Slice Of Life") so Kavita / readers show them
# consistently with the (already-titled-cased) Tags field.
add("Genre", ", ".join(_format_term(g) for g in (md.get("genres") or [])))
add("Tags", ", ".join(md.get("tags") or []))
# ----- Characters from MAL ------------------------------------------
@@ -396,9 +408,11 @@ class ComicInfoBuilder:
add("Manga", self._manga_flag(md))
add("AgeRating", _AGE_RATING_MAP.get(md.get("content_rating"), "Unknown"))
if md.get("rating"):
if md.get("rating") is not None:
try:
add("CommunityRating", round(float(md["rating"]) / 2, 1))
# MangaBaka rating is on a 0..100 scale -> ComicInfo
# CommunityRating uses 0..5.
add("CommunityRating", round(float(md["rating"]) / 20, 1))
except (TypeError, ValueError):
pass
@@ -563,38 +577,81 @@ class ComicInfoBuilder:
def _collect_alt_titles(self, md: dict) -> "dict[str, str]":
"""
Returns {lang_code: title} for EN, DE and JP (kanji + romaji).
Handles both list-of-dicts and plain-dict formats from the API.
Returns {lang_code: title} for EN, DE, JP kanji and JP romaji.
MangaBaka stores alt-titles in the `titles` list, where each entry is
a dict {language, title, traits, is_primary, note}.
Important caveats observed against the real API:
* `romanized_title` is the romanization of whatever the series'
native script is — for a Japanese manga with a Korean licence it
can hold the Korean romanization, NOT the Japanese romaji.
Always prefer `titles[language="ja-Latn"]` for romaji instead.
* `native_title` holds the kanji form for Japanese manga, but
`titles[language="ja", traits contains "native"]` is more
reliable when present.
* Each language can have several entries; primary + official
traits win over generic ones.
"""
result: dict[str, str] = {}
titles = md.get("titles") or md.get("alt_titles") or []
if md.get("romanized_title"):
result["romaji"] = md["romanized_title"]
if md.get("native_title"):
result["jp"] = md["native_title"]
alt = md.get("alt_titles") or md.get("titles") or []
if isinstance(alt, list):
for entry in alt:
def pick(language_codes: tuple, prefer_trait: "str | None" = None
) -> "str | None":
"""Picks the best title entry for any of the given language codes."""
if not isinstance(titles, list):
return None
best_score = -1
best_title: "str | None" = None
for entry in titles:
if not isinstance(entry, dict):
continue
lang = entry.get("lang")
lang = (entry.get("language") or entry.get("lang") or "").lower()
if lang not in language_codes:
continue
title = entry.get("title")
if not lang or not title:
# Single-key format: {"en": "Call of the Night"}
for k, v in entry.items():
if isinstance(v, str) and len(k) <= 10:
lang, title = k, v
break
if lang and title and lang.lower() in ("en", "de"):
result[lang.lower()] = title
elif isinstance(alt, dict):
for lang, title in alt.items():
if isinstance(title, str) and lang.lower() in ("en", "de"):
result[lang.lower()] = title
if not title:
continue
traits = entry.get("traits") or []
score = 0
if prefer_trait and prefer_trait in traits:
score += 4
if "official" in traits:
score += 2
if entry.get("is_primary"):
score += 1
if score > best_score:
best_score, best_title = score, title
return best_title
if "en" not in result and md.get("title"):
result["en"] = md["title"]
result: dict[str, str] = {}
# JP kanji (prefer entry with "native" trait, fall back to native_title)
kanji = pick(("ja",), prefer_trait="native") or md.get("native_title")
if kanji:
result["jp"] = kanji
# JP romaji — explicitly from "ja-Latn" entries. Do NOT fall back to
# `romanized_title` blindly; that field can hold a non-Japanese
# romanization (e.g. Korean) for the same series.
romaji = pick(("ja-latn", "ja-romaji"))
if not romaji:
# Heuristic fallback only when romanized_title looks Latin
rt = md.get("romanized_title") or ""
if rt and all(ord(c) < 128 for c in rt):
romaji = rt
if romaji:
result["romaji"] = romaji
# English (prefer official + primary)
en = pick(("en",))
if not en:
en = md.get("title") if md.get("title") else None
if en:
result["en"] = en
# German
de = pick(("de",))
if de:
result["de"] = de
return result
@@ -758,20 +815,48 @@ class ComicInfoBuilder:
# Module-level helpers (shared with MangaBakaWorksResolver logic)
# --------------------------------------------------------------------------
def _pick_cover_url(cover) -> "str | None":
"""Selects the best cover URL (raw preferred) from a MangaBaka cover object."""
"""
Selects the best cover URL from a MangaBaka cover object.
Real API shape (from `GET /v1/series/{id}` and `/works`):
{
"raw": {"url": "...", "size": ..., "height": ..., "width": ...},
"x150": {"x1": "...", "x2": "...", "x3": "..."},
"x250": {"x1": "...", "x2": "...", "x3": "..."},
"x350": {"x1": "...", "x2": "...", "x3": "..."}
}
Order of preference: raw original > x350@x3 > x250@x3 > x150@x3
(falling through to lower densities and sizes as needed).
"""
if not cover:
return None
if isinstance(cover, str):
return cover
if isinstance(cover, dict):
for key in ("raw", "default", "large", "medium", "small"):
val = cover.get(key)
if isinstance(val, str) and val:
return val
if isinstance(val, dict):
for sub in ("x2", "x1"):
if isinstance(val.get(sub), str) and val[sub]:
return val[sub]
if not isinstance(cover, dict):
return None
# 1) Preferred: the unscaled "raw" image
raw = cover.get("raw")
if isinstance(raw, dict):
url = raw.get("url")
if isinstance(url, str) and url:
return url
elif isinstance(raw, str) and raw:
return raw
# 2) Fallback: size-keyed variants, largest first, highest density first
for size_key in ("x350", "x250", "x150"):
variant = cover.get(size_key)
if isinstance(variant, dict):
for density in ("x3", "x2", "x1"):
url = variant.get(density)
if isinstance(url, str) and url:
return url
elif isinstance(variant, str) and variant:
return variant
# 3) Last-ditch fallback: any http URL anywhere in the structure
for val in cover.values():
if isinstance(val, str) and val.startswith("http"):
return val
+32 -6
View File
@@ -133,17 +133,43 @@ class MangaBakaWorksResolver:
# ------------------------------------------------------------------
@staticmethod
def _pick_cover_url(cover) -> "str | None":
"""Selects the best (raw-preferred) cover URL from a cover object."""
"""
Selects the best cover URL from a MangaBaka cover object.
Real API shape:
"raw": {"url": "...", "size": ..., "height": ..., "width": ...}
"x150": {"x1": "...", "x2": "...", "x3": "..."}
"x250": {...}
"x350": {...}
Order: raw original > x350@x3 > x250@x3 > x150@x3 ...
"""
if not cover:
return None
if isinstance(cover, str):
return cover
if isinstance(cover, dict):
url = cover.get("raw").get("url") or None
if url:
return url
if not isinstance(cover, dict):
return None
# Generic fallback: any HTTP URL in the dict
raw = cover.get("raw")
if isinstance(raw, dict):
url = raw.get("url")
if isinstance(url, str) and url:
return url
elif isinstance(raw, str) and raw:
return raw
for size_key in ("x350", "x250", "x150"):
variant = cover.get(size_key)
if isinstance(variant, dict):
for density in ("x3", "x2", "x1"):
url = variant.get(density)
if isinstance(url, str) and url:
return url
elif isinstance(variant, str) and variant:
return variant
# Last-ditch: any HTTP URL anywhere in the structure
for val in cover.values():
if isinstance(val, str) and val.startswith("http"):
return val