api bugfix
This commit is contained in:
+124
-39
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user