Add AniList resolver as MAL fallback; fix SeriesGroup, tag formatting, empty-cache bug

This commit is contained in:
2026-05-23 22:35:08 +02:00
parent ec1342d146
commit b8f897fa2e
6 changed files with 730 additions and 72 deletions
+44 -27
View File
@@ -47,6 +47,7 @@ import requests
from MangadexVolumeResolver import MangaDexVolumeResolver
from MangaBakaWorksResolver import MangaBakaWorksResolver
from MALResolver import MALResolver
from AniListResolver import AniListResolver
try:
from PIL import Image
@@ -168,7 +169,8 @@ class ComicInfoBuilder:
session: "requests.Session | None" = None,
volume_resolver: "MangaDexVolumeResolver | None" = None,
works_resolver: "MangaBakaWorksResolver | None" = None,
mal_resolver: "MALResolver | None" = None):
mal_resolver: "MALResolver | None" = None,
al_resolver: "AniListResolver | None" = None):
if not manga_title or not str(manga_title).strip():
raise ValueError("manga_title must not be empty.")
@@ -190,9 +192,11 @@ class ComicInfoBuilder:
api_base_url=api_base_url,
request_timeout=request_timeout,
session=self._session))
# MALResolver is a Singleton — it manages its own session and caches.
# Both resolvers are Singletons — they manage their own sessions/caches.
self._mal_resolver = mal_resolver or MALResolver(
request_timeout=request_timeout)
self._al_resolver = al_resolver or AniListResolver(
request_timeout=request_timeout)
self._metadata: "dict | None" = None
self._pages: list[dict] = []
@@ -405,6 +409,8 @@ class ComicInfoBuilder:
mal_id = (self._mal_id_from_source(md)
or self._mal_resolver.find_mal_id(
md.get("title") or self._manga_title))
al_id = self._al_id_from_source(md)
mal_stats = self._mal_resolver.get_stats(mal_id)
add("Summary", self._build_summary(md, sd, mal_stats))
@@ -432,10 +438,12 @@ class ComicInfoBuilder:
# 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 []))
add("Tags", ", ".join(_format_term(t) for t in (md.get("tags") or [])))
# ----- Characters from MAL ------------------------------------------
# ----- Characters — MAL first, AniList fallback ---------------------
characters = self._mal_resolver.get_characters(mal_id)
if not characters and al_id:
characters = self._al_resolver.get_characters(al_id)
add("Characters", ", ".join(characters) if characters else None)
# ----- Web links ----------------------------------------------------
@@ -571,32 +579,28 @@ class ComicInfoBuilder:
# ======================================================================
def _determine_series_group(self, md: dict) -> "str | None":
"""
Determines the SeriesGroup value from MangaDex relationships.
Determines SeriesGroup from MangaBaka's relationships_v2 field.
- If the series has a `main_story` parent -> use that title.
- If the series itself has child works (spin-offs, sequels …)
-> use the series own title so all related works are grouped.
- Otherwise -> None (no SeriesGroup).
- If the series has a 'parent' relationship entry → fetch the parent
series and return its MangaBaka title (so arcs/sequels appear under
the root series in Kavita).
- Otherwise → return the series' own title (it is the root, or a
standalone series with no parent).
"""
manga_id = self._mangadex_id_from_source(md)
if not manga_id:
return None
try:
relations = self._volume_resolver.get_series_relations(manga_id)
except Exception:
return None
for rel in (md.get("relationships_v2") or []):
if rel.get("relation_type") == "parent":
parent_id = rel.get("to_series_id")
if parent_id is not None:
try:
parent_md = self._fetch_series_by_id(parent_id)
parent_title = parent_md.get("title")
if parent_title:
return parent_title
except Exception:
pass
break
if not relations:
return None
main_stories = relations.get("main_story") or []
if main_stories:
return main_stories[0]
if any(t in relations for t in _CHILD_RELATION_TYPES):
return md.get("title") or self._manga_title
return None
return md.get("title") or self._manga_title
# ======================================================================
# Title helpers
@@ -867,6 +871,19 @@ class ComicInfoBuilder:
pass
return None
@staticmethod
def _al_id_from_source(md: dict) -> "int | None":
for raw_key, info in (md.get("source") or {}).items():
if _normalise_key(raw_key) == "anilist":
if isinstance(info, dict):
mid = info.get("id")
if mid is not None:
try:
return int(mid)
except (TypeError, ValueError):
pass
return None
@staticmethod
def _publishers_by_type(md: dict, ptype: str) -> "str | None":
names = [p.get("name") for p in (md.get("publishers") or [])