stuff and character update
This commit is contained in:
@@ -47,7 +47,9 @@ Dependencies
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import difflib
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
@@ -270,16 +272,18 @@ class KavitaPersonUpdater:
|
||||
if mal_id:
|
||||
if kind == "character":
|
||||
details = self._mal.get_character_details(mal_id)
|
||||
if details:
|
||||
description = _build_character_description(details) or None
|
||||
else:
|
||||
details = self._mal.get_person_details(mal_id)
|
||||
if details:
|
||||
description = (details.get("about") or "").strip() or None
|
||||
description = _build_person_description(details) or None
|
||||
|
||||
needs_desc = bool(description)
|
||||
|
||||
# ------ Metadata update ------------------------------------------
|
||||
changed = False
|
||||
if needs_mal_id or needs_desc:
|
||||
if needs_mal_id or True or needs_desc:
|
||||
payload: dict = {
|
||||
"id": person_id,
|
||||
"name": person_name,
|
||||
@@ -391,6 +395,94 @@ class KavitaPersonUpdater:
|
||||
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
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
+26
-13
@@ -230,11 +230,14 @@ class MALResolver:
|
||||
# ------------------------------------------------------------------
|
||||
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}, ...]
|
||||
|
||||
`about` is not populated here; call get_person_details(person_mal_id)
|
||||
to fetch it lazily when needed.
|
||||
Jikan has no `/manga/{id}/staff` endpoint — that route only exists for
|
||||
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:
|
||||
return []
|
||||
@@ -242,27 +245,28 @@ class MALResolver:
|
||||
return self._staff_detailed_cache[mal_id]
|
||||
|
||||
try:
|
||||
data = self._get(f"{self.JIKAN_BASE}/manga/{mal_id}/staff")
|
||||
entries = data.get("data") or []
|
||||
data = self._get(f"{self.JIKAN_BASE}/manga/{mal_id}")
|
||||
entry = data.get("data") or {}
|
||||
except requests.RequestException:
|
||||
return []
|
||||
|
||||
results = []
|
||||
for entry in entries:
|
||||
person = entry.get("person") or {}
|
||||
raw_name = person.get("name") or ""
|
||||
if not raw_name:
|
||||
for author in (entry.get("authors") or []):
|
||||
raw_name = author.get("name") or ""
|
||||
person_mal_id = author.get("mal_id")
|
||||
if not raw_name or person_mal_id is None:
|
||||
continue
|
||||
jpg = (person.get("images") or {}).get("jpg") or {}
|
||||
details = self.get_person_details(person_mal_id) or {}
|
||||
results.append({
|
||||
"mal_id": person.get("mal_id"),
|
||||
"mal_id": person_mal_id,
|
||||
"name": _clean_mal_name(raw_name),
|
||||
"raw_name": raw_name,
|
||||
"image_url": jpg.get("image_url") or jpg.get("small_image_url"),
|
||||
"positions": entry.get("positions") or [],
|
||||
"image_url": details.get("image_url"),
|
||||
"positions": [],
|
||||
"about": None,
|
||||
})
|
||||
|
||||
if results:
|
||||
self._staff_detailed_cache[mal_id] = results
|
||||
return results
|
||||
|
||||
@@ -291,6 +295,9 @@ class MALResolver:
|
||||
"name": entry.get("name") or "",
|
||||
"image_url": jpg.get("image_url") or jpg.get("small_image_url"),
|
||||
"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
|
||||
return result
|
||||
@@ -315,9 +322,15 @@ class MALResolver:
|
||||
result = {
|
||||
"mal_id": entry.get("mal_id"),
|
||||
"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"),
|
||||
"about": entry.get("about"),
|
||||
"favorites": entry.get("favorites"),
|
||||
"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
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user