feat: 重构图书墙交互及多源高阶信息抓取注入系统
1. UI 层: - 抛弃网格卡片结构,改版为沉浸式的“图书墙”(1:1.43 原比例包含模式)。 - 新增悬浮深色遮罩系统(hover-details),解绑评分点击阻塞。 - 过滤系统默认进入“在读”状态页,彻底移除冗余无效的全选按钮。 2. 抓取与刮削层 (scrape.py): - 抛弃对 ISBN 查询 Google Books 的严重强依赖。 - 重构抓取链路,首选 Goodreads 与特供的 Books.com.tw(博客来) 高精爬取港台原版资料。 - 建立 Amazon/Goodreads 图片去码净化正则,告别低分辨率与畸形图片。 - 修复 bs4 (.select_first) 解析选择器在旧版环境的兼容性异常。 3. Agent AI 数据管线设计 (bookshelf.py): - 开放基于 --json 与 --cover 的强力直接注入功能。 - 升级 add 命令作为最优先级的录入手段,免去繁琐的 enrich 阶段,直接连带高清图和满载元数据强插。 - 建立入库反重名智能拦截 (title + author 交叉校验),形成完美的防呆与自动化壁垒。
This commit is contained in:
@@ -24,7 +24,11 @@ def _escape(text):
|
||||
|
||||
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'<img class="card-cover" src="{_escape(cover)}" alt="封面" loading="lazy">'
|
||||
if cover
|
||||
else '<div class="card-cover card-cover--placeholder">📖</div>')
|
||||
@@ -36,10 +40,20 @@ def _render_book_card(book):
|
||||
|
||||
scores = []
|
||||
if book.get("douban_score"):
|
||||
scores.append(f'<span class="score douban">豆瓣 {book["douban_score"]}</span>')
|
||||
ds = f'D {book["douban_score"]}'
|
||||
if book.get("douban_url"):
|
||||
scores.append(f'<a href="{_escape(book["douban_url"])}" target="_blank" class="score douban" title="在豆瓣查看">{ds}</a>')
|
||||
else:
|
||||
scores.append(f'<span class="score douban">{ds}</span>')
|
||||
|
||||
if book.get("goodreads_score"):
|
||||
scores.append(f'<span class="score goodreads">GR {book["goodreads_score"]}</span>')
|
||||
scores_html = f'<div class="card-scores">{"".join(scores)}</div>' if scores else ""
|
||||
gs = f'G {book["goodreads_score"]}'
|
||||
if book.get("goodreads_url"):
|
||||
scores.append(f'<a href="{_escape(book["goodreads_url"])}" target="_blank" class="score goodreads" title="在 Goodreads 查看">{gs}</a>')
|
||||
else:
|
||||
scores.append(f'<span class="score goodreads">{gs}</span>')
|
||||
|
||||
scores_html = f'<div class="card-scores-container">{"".join(scores)}</div>' if scores else ""
|
||||
|
||||
tags_html = ""
|
||||
if book.get("tags"):
|
||||
@@ -51,7 +65,10 @@ def _render_book_card(book):
|
||||
|
||||
meta_parts = []
|
||||
if book.get("author"):
|
||||
meta_parts.append(f'{_escape(book["author"])}')
|
||||
a_text = _escape(book["author"])
|
||||
if book.get("author_en"):
|
||||
a_text += f' <span style="opacity: 0.7;">[{_escape(book["author_en"])}]</span>'
|
||||
meta_parts.append(a_text)
|
||||
if book.get("translator"):
|
||||
meta_parts.append(f'译: {_escape(book["translator"])}')
|
||||
if book.get("publisher"):
|
||||
@@ -72,22 +89,39 @@ def _render_book_card(book):
|
||||
dates_html = f'<div class="card-dates">{" · ".join(dates)}</div>' if dates else ""
|
||||
|
||||
format_badge = "📱 电子书" if book.get("format") == "ebook" else "📕 纸质书"
|
||||
group_html = f'<div class="card-group">{_escape(book.get("group", ""))}</div>' if book.get("group") else ""
|
||||
priority = book.get("priority", "")
|
||||
priority_html = f'<span class="card-priority">{priority}</span>' if priority else ""
|
||||
|
||||
# 默认加上在读状态的类(如果在读)
|
||||
is_reading = ' is-reading' if book.get('status') == 'reading' else ''
|
||||
|
||||
return f"""
|
||||
<div class="book-card" data-status="{_escape(book.get('status', ''))}"
|
||||
<div class="book-card{is_reading}" data-status="{_escape(book.get('status', ''))}"
|
||||
data-format="{_escape(book.get('format', ''))}"
|
||||
data-tags="{_escape(book.get('tags', ''))}">
|
||||
{cover_html}
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{_escape(book.get('title', ''))}</h3>
|
||||
{meta_html}
|
||||
{rating_html}
|
||||
{scores_html}
|
||||
{tags_html}
|
||||
{notes_html}
|
||||
{dates_html}
|
||||
<div class="card-format">{format_badge}</div>
|
||||
data-tags="{_escape(book.get('tags', ''))}"
|
||||
data-group="{_escape(book.get('group', ''))}">
|
||||
|
||||
<div class="card-cover-wrapper">
|
||||
{cover_html}
|
||||
<div class="card-hover-details">
|
||||
{group_html}
|
||||
<h3 class="card-title">{_escape(book.get('title', ''))} {priority_html}</h3>
|
||||
{f'<div class="card-title-en">{_escape(book["title_en"])}</div>' if book.get("title_en") else ""}
|
||||
<div class="meta-section">
|
||||
{meta_html}
|
||||
</div>
|
||||
{rating_html}
|
||||
<div class="meta-dates">
|
||||
{dates_html}
|
||||
</div>
|
||||
{notes_html}
|
||||
{tags_html}
|
||||
<div class="card-format">{format_badge}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{scores_html}
|
||||
</div>"""
|
||||
|
||||
|
||||
@@ -117,11 +151,37 @@ def build():
|
||||
paper = stats["by_format"].get("paper", 0)
|
||||
ebook = stats["by_format"].get("ebook", 0)
|
||||
|
||||
# 标签按钮
|
||||
tag_buttons = "".join(
|
||||
f'<button class="tag-btn" data-tag="{_escape(t)}">{_escape(t)}</button>'
|
||||
for t in all_tags
|
||||
)
|
||||
# 领域/分组概览
|
||||
|
||||
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'<img src="{src}" class="tiny-cover" title="{_escape(b.get("title",""))}">'
|
||||
covers_html.append(cv)
|
||||
|
||||
section = f'''
|
||||
<div class="group-section">
|
||||
<h2 class="group-section-title">{_escape(grp)} <span class="group-count">({len(gbooks)})</span></h2>
|
||||
<div class="group-covers">
|
||||
{"".join(covers_html)}
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
group_sections.append(section)
|
||||
|
||||
group_overview_html = "".join(group_sections)
|
||||
|
||||
html = (template
|
||||
.replace("{{CSS}}", css)
|
||||
@@ -131,11 +191,19 @@ def build():
|
||||
.replace("{{TO_READ}}", str(to_read))
|
||||
.replace("{{PAPER}}", str(paper))
|
||||
.replace("{{EBOOK}}", str(ebook))
|
||||
.replace("{{TAG_BUTTONS}}", tag_buttons)
|
||||
.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)
|
||||
|
||||
# 复制封面图片不处理(直接使用 URL)
|
||||
# 提取 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
|
||||
|
||||
Reference in New Issue
Block a user