Files
bookshelf/db.py
T
kai 0801f7c7df 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 交叉校验),形成完美的防呆与自动化壁垒。
2026-03-26 11:42:43 +08:00

192 lines
6.6 KiB
Python

"""数据库操作层 — 封装所有 SQLite 操作。"""
import sqlite3
import os
from datetime import datetime
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "books.db")
def _connect():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
def _column_exists(conn, table, column):
"""检查列是否已存在。"""
cols = conn.execute(f"PRAGMA table_info({table})").fetchall()
return any(c["name"] == column for c in cols)
def init_db():
"""初始化数据库和表结构。"""
conn = _connect()
conn.execute("""
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT,
translator TEXT,
publisher TEXT,
pub_date TEXT,
cover_url TEXT,
format TEXT DEFAULT 'paper',
status TEXT DEFAULT 'to-read',
rating INTEGER,
douban_score REAL,
goodreads_score REAL,
tags TEXT DEFAULT '',
notes TEXT,
start_date TEXT,
finish_date TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime')),
updated_at TEXT DEFAULT (datetime('now', 'localtime'))
)
""")
# v2 新增字段 — 兼容已有数据库
new_cols = [
("\"group\"", "TEXT DEFAULT ''"),
("isbn", "TEXT DEFAULT ''"),
("priority", "TEXT DEFAULT ''"),
("read_reason", "TEXT DEFAULT ''"),
("read_order", "INTEGER"),
("cover_blob", "BLOB"),
("cover_local", "TEXT DEFAULT ''"),
("title_en", "TEXT DEFAULT ''"),
("author_en", "TEXT DEFAULT ''"),
("douban_url", "TEXT DEFAULT ''"),
("goodreads_url","TEXT DEFAULT ''"),
]
for col_name, col_def in new_cols:
plain = col_name.strip('"')
if not _column_exists(conn, "books", plain):
conn.execute(f"ALTER TABLE books ADD COLUMN {col_name} {col_def}")
conn.commit()
conn.close()
def add_book(title, *, author=None, translator=None, publisher=None,
pub_date=None, cover_url=None, format="paper", status="to-read",
rating=None, douban_score=None, goodreads_score=None,
tags="", notes=None, start_date=None, finish_date=None,
group="", isbn="", priority="", read_reason="", read_order=None,
title_en="", author_en="", douban_url="", goodreads_url=""):
"""添加一本书,返回新书 ID。"""
conn = _connect()
cur = conn.execute("""
INSERT INTO books (title, author, translator, publisher, pub_date,
cover_url, format, status, rating, douban_score,
goodreads_score, tags, notes, start_date, finish_date,
"group", isbn, priority, read_reason, read_order,
title_en, author_en, douban_url, goodreads_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (title, author, translator, publisher, pub_date, cover_url,
format, status, rating, douban_score, goodreads_score,
tags, notes, start_date, finish_date,
group, isbn, priority, read_reason, read_order,
title_en, author_en, douban_url, goodreads_url))
conn.commit()
book_id = cur.lastrowid
conn.close()
return book_id
def update_book(book_id, **kwargs):
"""更新书目字段。只更新传入的字段。"""
if not kwargs:
return
allowed = {
"title", "author", "translator", "publisher", "pub_date",
"cover_url", "format", "status", "rating", "douban_score",
"goodreads_score", "tags", "notes", "start_date", "finish_date",
"group", "isbn", "priority", "read_reason", "read_order",
"cover_blob", "cover_local", "title_en", "author_en", "douban_url", "goodreads_url",
}
fields = {k: v for k, v in kwargs.items() if k in allowed}
if not fields:
return
fields["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
set_clause = ", ".join(f"{k} = ?" for k in fields)
values = list(fields.values()) + [book_id]
conn = _connect()
conn.execute(f"UPDATE books SET {set_clause} WHERE id = ?", values)
conn.commit()
conn.close()
def delete_book(book_id):
"""删除一本书。"""
conn = _connect()
conn.execute("DELETE FROM books WHERE id = ?", (book_id,))
conn.commit()
conn.close()
def get_book(book_id):
"""获取单本书详情,返回 dict 或 None。"""
conn = _connect()
row = conn.execute("SELECT * FROM books WHERE id = ?", (book_id,)).fetchone()
conn.close()
return dict(row) if row else None
def list_books(*, status=None, format=None, tag=None, group=None):
"""查询书目列表,支持按状态/格式/标签/分组过滤。"""
conn = _connect()
query = "SELECT * FROM books WHERE 1=1"
params = []
if status:
query += " AND status = ?"
params.append(status)
if format:
query += " AND format = ?"
params.append(format)
if tag:
query += " AND (',' || tags || ',' LIKE ?)"
params.append(f"%,{tag},%")
if group:
query += ' AND "group" = ?'
params.append(group)
query += " ORDER BY updated_at DESC"
rows = conn.execute(query, params).fetchall()
conn.close()
return [dict(r) for r in rows]
def get_stats():
"""获取统计数据。"""
conn = _connect()
total = conn.execute("SELECT COUNT(*) FROM books").fetchone()[0]
by_status = {}
for row in conn.execute("SELECT status, COUNT(*) as cnt FROM books GROUP BY status"):
by_status[row["status"]] = row["cnt"]
by_format = {}
for row in conn.execute("SELECT format, COUNT(*) as cnt FROM books GROUP BY format"):
by_format[row["format"]] = row["cnt"]
by_group = {}
for row in conn.execute('SELECT "group", COUNT(*) as cnt FROM books WHERE "group" != "" GROUP BY "group"'):
by_group[row["group"]] = row["cnt"]
conn.close()
return {
"total": total,
"by_status": by_status,
"by_format": by_format,
"by_group": by_group,
}
def get_all_tags():
"""获取所有不重复的标签列表。"""
conn = _connect()
rows = conn.execute("SELECT tags FROM books WHERE tags != ''").fetchall()
conn.close()
tag_set = set()
for row in rows:
for t in row["tags"].split(","):
t = t.strip()
if t:
tag_set.add(t)
return sorted(tag_set)