#!/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("❌ 请提供有效的 ISBN(10/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 更新已有记录。") 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="ISBN(10/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()