# ============================================================ # File: db/db.py # Purpose: # Raw SQLite engine for BookScraper. # Provides ONLY low-level DB primitives. # - Connection management (existing DELETE journal mode) # - init_db() schema creation + safe schema upgrade # - upsert_book() atomic write # - raw fetch helpers (private) # ============================================================ import os import sqlite3 from threading import Lock DB_PATH = os.environ.get("BOOKSCRAPER_DB", "/app/data/books.db") # Ensure directory exists os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) # Per-process connection cache _connection_cache = {} _connection_lock = Lock() # ------------------------------------------------------------ # Connection handling # ------------------------------------------------------------ def get_db(): pid = os.getpid() if pid not in _connection_cache: with _connection_lock: conn = sqlite3.connect(DB_PATH, check_same_thread=False) conn.row_factory = sqlite3.Row enable_wal_mode(conn) _connection_cache[pid] = conn return _connection_cache[pid] def enable_wal_mode(conn): conn.execute("PRAGMA journal_mode=DELETE;") conn.execute("PRAGMA synchronous=NORMAL;") conn.commit() # ------------------------------------------------------------ # Schema creation + SAFE schema upgrades # ------------------------------------------------------------ def init_db(): conn = get_db() # -------------------------------------------------------- # BASE SCHEMA (unchanged) # -------------------------------------------------------- conn.execute( """ CREATE TABLE IF NOT EXISTS books ( book_id TEXT PRIMARY KEY, title TEXT, author TEXT, cover_url TEXT, cover_path TEXT, chapters_total INTEGER, status TEXT, downloaded INTEGER DEFAULT 0, parsed INTEGER DEFAULT 0, audio_done INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_update DATETIME ); """ ) conn.commit() # -------------------------------------------------------- # SCHEMA UPGRADE: add description column if missing # -------------------------------------------------------- cols = conn.execute("PRAGMA table_info(books);").fetchall() colnames = [c[1] for c in cols] if "description" not in colnames: conn.execute("ALTER TABLE books ADD COLUMN description TEXT;") conn.commit() # ------------------------------------------------------------ # WRITE OPERATIONS # ------------------------------------------------------------ def upsert_book(book_id, **fields): conn = get_db() keys = ["book_id"] + list(fields.keys()) values = [book_id] + list(fields.values()) placeholders = ",".join(["?"] * len(values)) updates = ", ".join([f"{k} = excluded.{k}" for k in fields.keys()]) sql = f""" INSERT INTO books ({','.join(keys)}) VALUES ({placeholders}) ON CONFLICT(book_id) DO UPDATE SET {updates}, last_update = CURRENT_TIMESTAMP; """ conn.execute(sql, values) conn.commit() # ------------------------------------------------------------ # RAW READ OPERATIONS (PRIVATE) # ------------------------------------------------------------ def _raw_get_book(book_id): conn = get_db() row = conn.execute("SELECT * FROM books WHERE book_id = ?;", (book_id,)).fetchone() return dict(row) if row else None def _raw_get_all_books(): conn = get_db() # unchanged cur = conn.execute("SELECT * FROM books ORDER BY created_at DESC;") return [dict(row) for row in cur.fetchall()]