Files
bookshelf/bookshelf.py
T
kai 8c8095d9ac feat: add book scraping (Douban via Playwright + Goodreads + Google Books)
- scrape.py: Douban scraping with Playwright (multi-strategy browser launch)
- scrape.py: Goodreads scraping with requests + BeautifulSoup
- scrape.py: Google Books API + Open Library as fallback
- bookshelf.py: add 'fetch' subcommand (ISBN / Douban URL / GR URL)
- Fix Douban #info parsing using span.pl tags
- Add requirements.txt (requests, beautifulsoup4, playwright)
2026-03-25 14:20:12 +08:00

231 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""bookshelf — 图书阅读管理 CLI。"""
import argparse
import sys
import db
import web
import scrape
STATUS_CHOICES = ["to-read", "reading", "read"]
FORMAT_CHOICES = ["paper", "ebook"]
def cmd_add(args):
db.init_db()
book_id = db.add_book(
args.title,
author=args.author,
translator=args.translator,
publisher=args.publisher,
pub_date=args.pub_date,
cover_url=args.cover_url,
format=args.format,
status=args.status,
rating=args.rating,
douban_score=args.douban_score,
goodreads_score=args.goodreads_score,
tags=args.tags or "",
notes=args.notes,
start_date=args.start_date,
finish_date=args.finish_date,
)
print(f"✅ 已添加:《{args.title}》(ID: {book_id})")
def cmd_list(args):
db.init_db()
books = db.list_books(status=args.status, format=args.format, tag=args.tag)
if not books:
print("📚 暂无书目。")
return
for b in books:
status_icon = {"to-read": "📖", "reading": "📗", "read": "✅"}.get(b["status"], "📕")
fmt_icon = "📱" if b["format"] == "ebook" else "📕"
rating_str = f" ⭐{b['rating']}" if b["rating"] else ""
tags_str = f" [{b['tags']}]" if b["tags"] else ""
print(f" {status_icon} [{b['id']:>3}] 《{b['title']}》— {b['author'] or '未知'}"
f" {fmt_icon}{rating_str}{tags_str}")
def cmd_update(args):
db.init_db()
book = db.get_book(args.id)
if not book:
print(f"❌ 未找到 ID 为 {args.id} 的书目。")
sys.exit(1)
updates = {}
for field in ["title", "author", "translator", "publisher", "pub_date",
"cover_url", "format", "status", "rating", "douban_score",
"goodreads_score", "tags", "notes", "start_date", "finish_date"]:
val = getattr(args, field.replace("-", "_"), None)
if val is not None:
updates[field] = val
if not updates:
print("⚠️ 未指定任何要更新的字段。")
return
db.update_book(args.id, **updates)
print(f"✅ 已更新:《{book['title']}》(ID: {args.id})")
def cmd_delete(args):
db.init_db()
book = db.get_book(args.id)
if not book:
print(f"❌ 未找到 ID 为 {args.id} 的书目。")
sys.exit(1)
db.delete_book(args.id)
print(f"🗑️ 已删除:《{book['title']}》(ID: {args.id})")
def cmd_fetch(args):
"""刮削图书元数据,可选择直接写入数据库。"""
identifier = args.identifier
# 选择刮削策略
if "douban.com" in identifier:
print(f"🔍 正在从豆瓣刮削:{identifier}")
data = scrape.fetch_douban(identifier)
source = "豆瓣"
elif "goodreads.com" in identifier:
print(f"🔍 正在从 Goodreads 刮削:{identifier}")
data = scrape.fetch_goodreads(identifier)
source = "Goodreads"
elif scrape._is_isbn(identifier):
print(f"🔍 正在刮削 ISBN{identifier}")
data, source = scrape.fetch_by_isbn(identifier)
else:
print("❌ 请提供有效的 ISBN10/13位)、豆瓣链接或 Goodreads 链接。")
sys.exit(1)
if not data:
print("❌ 刮削失败,未能获取到图书信息。")
print(" 提示:豆瓣偶尔会拒绝请求(418/403),请稍后重试。")
sys.exit(1)
# 打印预览
print(scrape.preview(data, source))
print()
if args.update_id:
# 更新已有记录
db.init_db()
book = db.get_book(args.update_id)
if not book:
print(f"❌ 未找到 ID 为 {args.update_id} 的书目。")
sys.exit(1)
# 只覆盖刮削到的字段,保留用户已设置的字段
updates = {k: v for k, v in data.items() if v}
if args.format:
updates["format"] = args.format
if args.status:
updates["status"] = args.status
db.update_book(args.update_id, **updates)
print(f"✅ 已更新:《{book['title']}》→《{data.get('title', book['title'])}》(ID: {args.update_id})")
elif args.add:
# 写入新记录
db.init_db()
book_id = db.add_book(
data.get("title", ""),
author=data.get("author"),
translator=data.get("translator"),
publisher=data.get("publisher"),
pub_date=data.get("pub_date"),
cover_url=data.get("cover_url"),
format=args.format or "paper",
status=args.status or "to-read",
douban_score=data.get("douban_score"),
goodreads_score=data.get("goodreads_score"),
tags=data.get("tags", ""),
)
print(f"✅ 已添加:《{data.get('title')}》(ID: {book_id})")
else:
print("💡 加 --add 选项可直接写入数据库,或用 --update-id <ID> 更新已有记录。")
def cmd_build(args):
db.init_db()
output_path = web.build()
print(f"🌐 网页已生成:{output_path}")
def main():
parser = argparse.ArgumentParser(
prog="bookshelf",
description="📚 个人图书阅读管理工具",
)
sub = parser.add_subparsers(dest="command", required=True)
# --- add ---
p_add = sub.add_parser("add", help="添加书目")
p_add.add_argument("title", help="书名")
p_add.add_argument("--author", help="作者")
p_add.add_argument("--translator", help="译者")
p_add.add_argument("--publisher", help="出版社")
p_add.add_argument("--pub-date", help="出版日期")
p_add.add_argument("--cover-url", help="封面图片 URL")
p_add.add_argument("--format", choices=FORMAT_CHOICES, default="paper", help="格式")
p_add.add_argument("--status", choices=STATUS_CHOICES, default="to-read", help="阅读状态")
p_add.add_argument("--rating", type=int, choices=range(1, 6), help="个人评分 1-5")
p_add.add_argument("--douban-score", type=float, help="豆瓣评分")
p_add.add_argument("--goodreads-score", type=float, help="Goodreads 评分")
p_add.add_argument("--tags", help="阅读主题标签,逗号分隔")
p_add.add_argument("--notes", help="笔记/简评")
p_add.add_argument("--start-date", help="开始阅读日期")
p_add.add_argument("--finish-date", help="完成阅读日期")
p_add.set_defaults(func=cmd_add)
# --- list ---
p_list = sub.add_parser("list", help="列出书目")
p_list.add_argument("--status", choices=STATUS_CHOICES, help="按状态筛选")
p_list.add_argument("--format", choices=FORMAT_CHOICES, help="按格式筛选")
p_list.add_argument("--tag", help="按标签筛选")
p_list.set_defaults(func=cmd_list)
# --- update ---
p_upd = sub.add_parser("update", help="更新书目")
p_upd.add_argument("id", type=int, help="书目 ID")
p_upd.add_argument("--title", help="书名")
p_upd.add_argument("--author", help="作者")
p_upd.add_argument("--translator", help="译者")
p_upd.add_argument("--publisher", help="出版社")
p_upd.add_argument("--pub-date", help="出版日期")
p_upd.add_argument("--cover-url", help="封面图片 URL")
p_upd.add_argument("--format", choices=FORMAT_CHOICES, help="格式")
p_upd.add_argument("--status", choices=STATUS_CHOICES, help="阅读状态")
p_upd.add_argument("--rating", type=int, choices=range(1, 6), help="个人评分 1-5")
p_upd.add_argument("--douban-score", type=float, help="豆瓣评分")
p_upd.add_argument("--goodreads-score", type=float, help="Goodreads 评分")
p_upd.add_argument("--tags", help="阅读主题标签,逗号分隔")
p_upd.add_argument("--notes", help="笔记/简评")
p_upd.add_argument("--start-date", help="开始阅读日期")
p_upd.add_argument("--finish-date", help="完成阅读日期")
p_upd.set_defaults(func=cmd_update)
# --- delete ---
p_del = sub.add_parser("delete", help="删除书目")
p_del.add_argument("id", type=int, help="书目 ID")
p_del.set_defaults(func=cmd_delete)
# --- fetch ---
p_fetch = sub.add_parser("fetch", help="从豆瓣/Google Books 刮削书目信息")
p_fetch.add_argument("identifier", help="ISBN10/13位)或豆瓣图书链接")
p_fetch.add_argument("--add", action="store_true", help="刮削后直接写入数据库")
p_fetch.add_argument("--update-id", type=int, metavar="ID", help="刮削后更新指定 ID 的书目")
p_fetch.add_argument("--format", choices=FORMAT_CHOICES, help="书籍格式(入库时使用)")
p_fetch.add_argument("--status", choices=STATUS_CHOICES, help="阅读状态(入库时使用)")
p_fetch.set_defaults(func=cmd_fetch)
# --- build ---
p_build = sub.add_parser("build", help="生成展示网页")
p_build.set_defaults(func=cmd_build)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()