Files
bookshelf/db.py
T
kai 69c34d4faf feat: schema upgrade + import 159 books from reading list
- db.py: add group, isbn, priority, read_reason, read_order, cover_blob fields
- Import 159 books from booklist.md with group/priority/status metadata
- Include books.db in repo (personal use)
2026-03-25 14:55:23 +08:00

185 lines
6.1 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 ''"),
]
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):
"""添加一本书,返回新书 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)
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))
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",
}
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)