This commit is contained in:
2026-05-23 16:55:21 +02:00
parent 377aff34d0
commit c544728c77
+90 -46
View File
@@ -105,6 +105,42 @@ def _format_term(value: str) -> str:
return str(value).replace("_", " ").strip().title() if value else ""
# Markdown backslash escape sequences recognised by CommonMark (e.g. \- → -)
_MD_ESCAPE_RE = re.compile(r'\\([\\`*_{}\[\]()\#+\-.!|~])')
def _md_to_html(text: str) -> str:
"""
Converts a subset of Markdown (as produced by MangaBaka) to HTML.
Handles: backslash escapes, [text](url) links, **bold**, *italic*,
blank-line paragraph splits, and single-newline line breaks.
Produces compact HTML with no raw newline characters — Kavita renders
every bare \\n as a <br>, so all line-breaks must be explicit.
"""
if not text:
return ""
# Unescape Markdown backslash sequences (\- → -, \* → *, …)
text = _MD_ESCAPE_RE.sub(r'\1', text)
# [text](url) → <a href="url">text</a>
text = re.sub(
r'\[([^\]]+)\]\(([^)]+)\)',
lambda m: f'<a href="{m.group(2)}">{m.group(1)}</a>',
text,
)
# **bold** before *italic* so ** is not mistaken for two *
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text, flags=re.DOTALL)
text = re.sub(r'\*(.+?)\*', r'<em>\1</em>', text, flags=re.DOTALL)
# Split on blank lines → <p> blocks; single newlines → <br>
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) # no raw \n — every \n becomes a <br> in Kavita
# --------------------------------------------------------------------------
# Main class
# --------------------------------------------------------------------------
@@ -320,8 +356,7 @@ class ComicInfoBuilder:
timeout=self.request_timeout)
resp.raise_for_status()
data = resp.json().get("data") or []
return data[0] # I trust the API's relevance sorting and just take the first result, if any
return data[0] if data else None
def _fetch_series_by_id(self, series_id) -> dict:
url = f"{self.api_base_url}/series/{series_id}"
@@ -665,60 +700,69 @@ class ComicInfoBuilder:
def _build_summary(self, md: dict, sd: dict,
mal_stats: "dict | None") -> "str | None":
"""
Builds the <Summary> content.
Appends a MAL statistics table (if available) after the description.
Builds <Summary> as HTML (Kavita supports HTML in this field).
Structure (top → bottom):
1. MAL statistics — HTML link + table with padded columns
2. Series description — Markdown converted to HTML
3. Alternate titles — HTML table
IMPORTANT: no raw \\n characters anywhere in the output — Kavita
renders every bare newline as a <br>. Sections are separated with
an explicit <br> instead.
"""
desc = (md.get("description") or sd.get("Summary") or "").strip()
# Inline style applied to label cells for readable column spacing.
_TD = 'style="padding-right:1.5em"'
if not mal_stats:
return desc or None
as_of = mal_stats.get("as_of", "")
score = mal_stats.get("score")
rank = mal_stats.get("rank")
scored = mal_stats.get("scored_by")
pop = mal_stats.get("popularity")
members = mal_stats.get("members")
favs = mal_stats.get("favorites")
url = mal_stats.get("url", "")
rows: list[str] = []
if score is not None: rows.append(f"Score\t{score}")
if rank is not None: rows.append(f"Ranked\t#{rank}")
if scored is not None: rows.append(f"Scored by\t{scored:,} users")
if pop is not None: rows.append(f"Popularity\t#{pop}")
if members is not None: rows.append(f"Members\t{members:,}")
if favs is not None: rows.append(f"Favorites\t{favs:,}")
if not rows:
return desc or None
table = f"[MyAnimeList]({url}) stats as of {as_of}:\n" + "\n".join(rows)
return f"{desc}\n\n{table}" if desc else table
def _build_notes(self, md: dict) -> "str | None":
"""
Builds the <Notes> field containing alternate titles and the
MangaBaka metadata source URL.
"""
parts: list[str] = []
# 1. MAL stats table (top) ----------------------------------------
if mal_stats:
url = mal_stats.get("url", "")
as_of = mal_stats.get("as_of", "")
score = mal_stats.get("score")
rank = mal_stats.get("rank")
scored = mal_stats.get("scored_by")
pop = mal_stats.get("popularity")
members = mal_stats.get("members")
favs = mal_stats.get("favorites")
rows: list[str] = []
if score is not None: rows.append(f"<tr><td {_TD}>Score</td><td>{score}</td></tr>")
if rank is not None: rows.append(f"<tr><td {_TD}>Ranked</td><td>#{rank}</td></tr>")
if scored is not None: rows.append(f"<tr><td {_TD}>Scored by</td><td>{scored:,} users</td></tr>")
if pop is not None: rows.append(f"<tr><td {_TD}>Popularity</td><td>#{pop}</td></tr>")
if members is not None: rows.append(f"<tr><td {_TD}>Members</td><td>{members:,}</td></tr>")
if favs is not None: rows.append(f"<tr><td {_TD}>Favorites</td><td>{favs:,}</td></tr>")
if rows:
link = f'<a href="{url}">MyAnimeList</a>' if url else "MyAnimeList"
parts.append(f"<p>{link} stats as of {as_of}:</p><table>{''.join(rows)}</table>")
# 2. Description — Markdown → HTML (middle) -----------------------
desc_raw = (md.get("description") or sd.get("Summary") or "").strip()
if desc_raw:
parts.append(_md_to_html(desc_raw))
# 3. Alternate titles table (bottom) ------------------------------
alt = self._collect_alt_titles(md)
if alt:
label_map = {"en": "EN", "de": "DE",
"romaji": "Romaji", "jp": "JP (kanji)"}
lines = []
label_map = {"en": "EN", "de": "DE", "romaji": "Romaji", "jp": "JP (Kanji)"}
alt_rows: list[str] = []
for code in ("en", "de", "romaji", "jp"):
if code in alt:
lines.append(f"{label_map[code]}: {alt[code]}")
if lines:
parts.append("Alternate titles:\n" + "\n".join(lines))
alt_rows.append(
f"<tr><td {_TD}>{label_map[code]}</td><td>{alt[code]}</td></tr>"
)
if alt_rows:
parts.append(f"<table>{''.join(alt_rows)}</table>")
return "<br>".join(parts) if parts else None
def _build_notes(self, md: dict) -> "str | None":
"""Builds the <Notes> field with the MangaBaka metadata source URL."""
series_id = str(md.get("id") or "")
if series_id:
parts.append(f"Metadata source: https://mangabaka.org/{series_id}")
return "\n\n".join(parts) if parts else None
return f"Metadata source: https://mangabaka.org/{series_id}" if series_id else None
# ======================================================================
# Static helpers