"""网页生成器 — 读取数据库,生成静态 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