stuff and character update

This commit is contained in:
2026-05-23 21:43:16 +02:00
parent a04c052388
commit 067ca89f48
2 changed files with 122 additions and 17 deletions
+95 -3
View File
@@ -47,7 +47,9 @@ Dependencies
from __future__ import annotations from __future__ import annotations
import base64 import base64
import datetime
import difflib import difflib
import re
import requests import requests
@@ -270,16 +272,18 @@ class KavitaPersonUpdater:
if mal_id: if mal_id:
if kind == "character": if kind == "character":
details = self._mal.get_character_details(mal_id) details = self._mal.get_character_details(mal_id)
if details:
description = _build_character_description(details) or None
else: else:
details = self._mal.get_person_details(mal_id) details = self._mal.get_person_details(mal_id)
if details: if details:
description = (details.get("about") or "").strip() or None description = _build_person_description(details) or None
needs_desc = bool(description) needs_desc = bool(description)
# ------ Metadata update ------------------------------------------ # ------ Metadata update ------------------------------------------
changed = False changed = False
if needs_mal_id or needs_desc: if needs_mal_id or True or needs_desc:
payload: dict = { payload: dict = {
"id": person_id, "id": person_id,
"name": person_name, "name": person_name,
@@ -391,6 +395,94 @@ class KavitaPersonUpdater:
return False return False
# --------------------------------------------------------------------------
# Module helpers: description builders
# --------------------------------------------------------------------------
def _plain_to_html(text: str) -> str:
"""Converts plain text with paragraph breaks to compact HTML (no raw \\n)."""
if not text:
return ""
parts: list[str] = []
for para in re.split(r"\n{2,}", text.strip()):
para = para.strip()
if para:
parts.append(f"<p>{para.replace(chr(10), '<br>')}</p>")
return "".join(parts)
def _format_birthday(birthday: str) -> str:
"""Converts an ISO 8601 birthday string to "D Month YYYY"."""
if not birthday:
return ""
try:
dt = datetime.date.fromisoformat(birthday.split("T")[0])
return f"{dt.day} {dt.strftime('%B %Y')}"
except (ValueError, AttributeError):
return ""
def _build_character_description(details: dict) -> str:
"""
Builds a Kavita-safe HTML description for a MAL character.
Top line: "Favorites: N" as a link to the character's MAL page.
Remainder: the character's `about` text converted to HTML paragraphs.
"""
parts: list[str] = []
url = details.get("url") or ""
favorites = details.get("favorites")
if url and favorites is not None:
parts.append(f'<p><a href="{url}">Favorites: {favorites:,}</a></p>')
about = (details.get("about") or "").strip()
if about:
parts.append(_plain_to_html(about))
return "<br>".join(parts)
def _build_person_description(details: dict) -> str:
"""
Builds a Kavita-safe HTML description for a MAL person (mangaka / staff).
Renders a summary table (given name, family name, birthday, website,
member favorites) followed by the `about` biography as HTML paragraphs.
"""
_TD = 'style="padding-right:1.5em"'
rows: list[str] = []
given = (details.get("given_name") or "").strip()
family = (details.get("family_name") or "").strip()
birthday = details.get("birthday") or ""
favorites = details.get("favorites")
website = (details.get("website_url") or "").strip()
url = (details.get("url") or "").strip()
if given:
rows.append(f"<tr><td {_TD}>Given name</td><td>{given}</td></tr>")
if family:
rows.append(f"<tr><td {_TD}>Family name</td><td>{family}</td></tr>")
bday_str = _format_birthday(birthday)
if bday_str:
rows.append(f"<tr><td {_TD}>Birthday</td><td>{bday_str}</td></tr>")
if website:
rows.append(
f'<tr><td {_TD}>Website</td>'
f'<td><a href="{website}">{website}</a></td></tr>'
)
if favorites is not None:
fav_cell = (f'<a href="{url}">{favorites:,}</a>' if url
else f"{favorites:,}")
rows.append(
f"<tr><td {_TD}>Member Favorites</td><td>{fav_cell}</td></tr>")
parts: list[str] = []
if rows:
parts.append(f'<table>{"".join(rows)}</table>')
about = (details.get("about") or "").strip()
if about:
parts.append(_plain_to_html(about))
return "<br>".join(parts)
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Module helper # Module helper
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
+27 -14
View File
@@ -230,11 +230,14 @@ class MALResolver:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def get_staff_detailed(self, mal_id: "int | None") -> list[dict]: def get_staff_detailed(self, mal_id: "int | None") -> list[dict]:
""" """
Returns detailed staff entries for a manga: Returns detailed staff (author) entries for a manga:
[{mal_id, name, image_url, positions, about=None}, ...] [{mal_id, name, image_url, positions, about=None}, ...]
`about` is not populated here; call get_person_details(person_mal_id) Jikan has no `/manga/{id}/staff` endpoint — that route only exists for
to fetch it lazily when needed. anime. For manga the authors are listed on `/manga/{id}` under
`data.authors`, but each entry only has {mal_id, name, url}; the image
URL is fetched lazily via get_person_details (cached, so the later
description fetch is free).
""" """
if mal_id is None: if mal_id is None:
return [] return []
@@ -242,28 +245,29 @@ class MALResolver:
return self._staff_detailed_cache[mal_id] return self._staff_detailed_cache[mal_id]
try: try:
data = self._get(f"{self.JIKAN_BASE}/manga/{mal_id}/staff") data = self._get(f"{self.JIKAN_BASE}/manga/{mal_id}")
entries = data.get("data") or [] entry = data.get("data") or {}
except requests.RequestException: except requests.RequestException:
return [] return []
results = [] results = []
for entry in entries: for author in (entry.get("authors") or []):
person = entry.get("person") or {} raw_name = author.get("name") or ""
raw_name = person.get("name") or "" person_mal_id = author.get("mal_id")
if not raw_name: if not raw_name or person_mal_id is None:
continue continue
jpg = (person.get("images") or {}).get("jpg") or {} details = self.get_person_details(person_mal_id) or {}
results.append({ results.append({
"mal_id": person.get("mal_id"), "mal_id": person_mal_id,
"name": _clean_mal_name(raw_name), "name": _clean_mal_name(raw_name),
"raw_name": raw_name, "raw_name": raw_name,
"image_url": jpg.get("image_url") or jpg.get("small_image_url"), "image_url": details.get("image_url"),
"positions": entry.get("positions") or [], "positions": [],
"about": None, "about": None,
}) })
self._staff_detailed_cache[mal_id] = results if results:
self._staff_detailed_cache[mal_id] = results
return results return results
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -291,6 +295,9 @@ class MALResolver:
"name": entry.get("name") or "", "name": entry.get("name") or "",
"image_url": jpg.get("image_url") or jpg.get("small_image_url"), "image_url": jpg.get("image_url") or jpg.get("small_image_url"),
"about": entry.get("about"), "about": entry.get("about"),
"favorites": entry.get("favorites"),
"url": (entry.get("url")
or f"https://myanimelist.net/character/{char_mal_id}"),
} }
self._char_info_cache[char_mal_id] = result self._char_info_cache[char_mal_id] = result
return result return result
@@ -315,9 +322,15 @@ class MALResolver:
result = { result = {
"mal_id": entry.get("mal_id"), "mal_id": entry.get("mal_id"),
"name": entry.get("name") or "", "name": entry.get("name") or "",
"given_name": entry.get("given_name"),
"family_name": entry.get("family_name"),
"birthday": entry.get("birthday"),
"image_url": jpg.get("image_url") or jpg.get("small_image_url"), "image_url": jpg.get("image_url") or jpg.get("small_image_url"),
"about": entry.get("about"), "about": entry.get("about"),
"favorites": entry.get("favorites"),
"website_url": entry.get("website_url"), "website_url": entry.get("website_url"),
"url": (entry.get("url")
or f"https://myanimelist.net/people/{person_mal_id}"),
} }
self._person_info_cache[person_mal_id] = result self._person_info_cache[person_mal_id] = result
return result return result