stuff and character update
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user