summary
This commit is contained in:
+90
-46
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user