"""网页生成器 — 读取数据库,生成静态 HTML 展示页面。""" import os import json import db BASE_DIR = os.path.dirname(os.path.abspath(__file__)) TEMPLATE_PATH = os.path.join(BASE_DIR, "templates", "index.html") STYLE_PATH = os.path.join(BASE_DIR, "static", "style.css") OUTPUT_DIR = os.path.join(BASE_DIR, "output") OUTPUT_PATH = os.path.join(OUTPUT_DIR, "index.html") def _escape(text): """简单的 HTML 转义。""" if not text: return "" return (str(text) .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """)) def _render_book_card(book): """渲染单本书的 HTML 卡片。""" # 封面:优先本地 BLOB 提取后的相对路径,其次远程 URL cover = book.get("cover_url") or "" if book.get("cover_blob"): cover = f"covers/{book['id']}.jpg" cover_html = (f'封面' if cover else '
📖
') rating_html = "" if book.get("rating"): stars = "★" * book["rating"] + "☆" * (5 - book["rating"]) rating_html = f'
{stars}
' scores = [] if book.get("douban_score"): ds = f'D {book["douban_score"]}' if book.get("douban_url"): scores.append(f'{ds}') else: scores.append(f'{ds}') if book.get("goodreads_score"): gs = f'G {book["goodreads_score"]}' if book.get("goodreads_url"): scores.append(f'{gs}') else: scores.append(f'{gs}') scores_html = f'
{"".join(scores)}
' if scores else "" tags_html = "" if book.get("tags"): tag_spans = "".join( f'{_escape(t.strip())}' for t in book["tags"].split(",") if t.strip() ) tags_html = f'
{tag_spans}
' meta_parts = [] if book.get("author"): a_text = _escape(book["author"]) if book.get("author_en"): a_text += f' [{_escape(book["author_en"])}]' meta_parts.append(a_text) if book.get("translator"): meta_parts.append(f'译: {_escape(book["translator"])}') if book.get("publisher"): meta_parts.append(f'{_escape(book["publisher"])}') if book.get("pub_date"): meta_parts.append(f'{_escape(book["pub_date"])}') meta_html = f'
{" · ".join(meta_parts)}
' if meta_parts else "" notes_html = "" if book.get("notes"): notes_html = f'
{_escape(book["notes"])}
' dates = [] if book.get("start_date"): dates.append(f'开始: {_escape(book["start_date"])}') if book.get("finish_date"): dates.append(f'完成: {_escape(book["finish_date"])}') dates_html = f'
{" · ".join(dates)}
' if dates else "" format_badge = "📱 电子书" if book.get("format") == "ebook" else "📕 纸质书" group_html = f'
{_escape(book.get("group", ""))}
' if book.get("group") else "" priority = book.get("priority", "") priority_html = f'{priority}' if priority else "" # 默认加上在读状态的类(如果在读) is_reading = ' is-reading' if book.get('status') == 'reading' else '' return f"""
{cover_html}
{group_html}

{_escape(book.get('title', ''))} {priority_html}

{f'
{_escape(book["title_en"])}
' if book.get("title_en") else ""}
{meta_html}
{rating_html}
{dates_html}
{notes_html} {tags_html}
{format_badge}
{scores_html}
""" def build(): """生成静态 HTML 页面,返回输出路径。""" os.makedirs(OUTPUT_DIR, exist_ok=True) books = db.list_books() stats = db.get_stats() all_tags = db.get_all_tags() cards_html = "\n".join(_render_book_card(b) for b in books) # 读取 CSS with open(STYLE_PATH, "r", encoding="utf-8") as f: css = f.read() # 读取模板并填充 with open(TEMPLATE_PATH, "r", encoding="utf-8") as f: template = f.read() # 统计数据 total = stats["total"] reading = stats["by_status"].get("reading", 0) read = stats["by_status"].get("read", 0) to_read = stats["by_status"].get("to-read", 0) paper = stats["by_format"].get("paper", 0) ebook = stats["by_format"].get("ebook", 0) # 领域/分组概览 from collections import defaultdict group_books = defaultdict(list) for b in books: grp = b.get("group") or "未分类" group_books[grp].append(b) group_sections = [] for grp, gbooks in sorted(group_books.items()): covers_html = [] for b in gbooks: cid = str(b["id"]) if b.get("cover_blob") or b.get("cover_local"): src = f"./covers/{cid}.jpg" else: src = b.get("cover_url", "https://via.placeholder.com/150?text=No+Cover") cv = f'' covers_html.append(cv) section = f'''

{_escape(grp)} ({len(gbooks)})

{"".join(covers_html)}
''' group_sections.append(section) group_overview_html = "".join(group_sections) html = (template .replace("{{CSS}}", css) .replace("{{TOTAL}}", str(total)) .replace("{{READING}}", str(reading)) .replace("{{READ}}", str(read)) .replace("{{TO_READ}}", str(to_read)) .replace("{{PAPER}}", str(paper)) .replace("{{EBOOK}}", str(ebook)) .replace("{{GROUP_OVERVIEW_HTML}}", group_overview_html) .replace("{{BOOK_CARDS}}", cards_html)) with open(OUTPUT_PATH, "w", encoding="utf-8") as f: f.write(html) # 提取 BLOB 封面到 output/covers/ covers_dir = os.path.join(OUTPUT_DIR, "covers") os.makedirs(covers_dir, exist_ok=True) for b in books: if b.get("cover_blob"): cover_path = os.path.join(covers_dir, f"{b['id']}.jpg") with open(cover_path, "wb") as f: f.write(b["cover_blob"]) return OUTPUT_PATH