From c544728c77ee4e57251bbfc11992eadf82f0adfe Mon Sep 17 00:00:00 2001 From: JohannesBOT Date: Sat, 23 May 2026 16:55:21 +0200 Subject: [PATCH] summary --- src/ComicInfoBuilder.py | 136 ++++++++++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 46 deletions(-) diff --git a/src/ComicInfoBuilder.py b/src/ComicInfoBuilder.py index d3a28ce..759946b 100644 --- a/src/ComicInfoBuilder.py +++ b/src/ComicInfoBuilder.py @@ -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
, 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) → text + text = re.sub( + r'\[([^\]]+)\]\(([^)]+)\)', + lambda m: f'{m.group(1)}', + text, + ) + # **bold** before *italic* so ** is not mistaken for two * + text = re.sub(r'\*\*(.+?)\*\*', r'\1', text, flags=re.DOTALL) + text = re.sub(r'\*(.+?)\*', r'\1', text, flags=re.DOTALL) + # Split on blank lines →

blocks; single newlines →
+ parts: list[str] = [] + for para in re.split(r'\n{2,}', text.strip()): + para = para.strip() + if para: + parts.append(f"

{para.replace(chr(10), '
')}

") + return "".join(parts) # no raw \n — every \n becomes a
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 content. - Appends a MAL statistics table (if available) after the description. + Builds 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
. Sections are separated with + an explicit
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 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"Score{score}") + if rank is not None: rows.append(f"Ranked#{rank}") + if scored is not None: rows.append(f"Scored by{scored:,} users") + if pop is not None: rows.append(f"Popularity#{pop}") + if members is not None: rows.append(f"Members{members:,}") + if favs is not None: rows.append(f"Favorites{favs:,}") + + if rows: + link = f'MyAnimeList' if url else "MyAnimeList" + parts.append(f"

{link} stats as of {as_of}:

{''.join(rows)}
") + + # 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"{label_map[code]}{alt[code]}" + ) + if alt_rows: + parts.append(f"{''.join(alt_rows)}
") + return "
".join(parts) if parts else None + + def _build_notes(self, md: dict) -> "str | None": + """Builds the 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