# ============================================================ # File: db/state_sql.py # Purpose: # Low-level SQLite snapshot layer for BookScraper metadata. # Used ONLY through db.repository façade. # ============================================================ import sqlite3 import os from logbus.publisher import log DB_PATH = os.getenv("BOOKSCRAPER_DB", "/app/db/books.db") # ------------------------------------------------------------ # INTERNAL HELPERS # ------------------------------------------------------------ def _connect(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn # ------------------------------------------------------------ # FETCH # ------------------------------------------------------------ def sql_fetch_book(book_id): conn = _connect() cur = conn.cursor() cur.execute("SELECT * FROM books WHERE book_id = ?", (book_id,)) row = cur.fetchone() conn.close() return dict(row) if row else None def sql_fetch_all_books(): conn = _connect() cur = conn.cursor() cur.execute("SELECT * FROM books ORDER BY rowid DESC") rows = cur.fetchall() conn.close() return [dict(r) for r in rows] # ------------------------------------------------------------ # REGISTER / UPDATE # ------------------------------------------------------------ def sql_register_book(book_id, fields: dict): conn = _connect() cur = conn.cursor() cols = ", ".join(["book_id"] + list(fields.keys())) placeholders = ", ".join(["?"] * (1 + len(fields))) values = [book_id] + list(fields.values()) cur.execute( f"INSERT OR REPLACE INTO books ({cols}) VALUES ({placeholders})", values ) conn.commit() conn.close() def sql_update_book(book_id, fields: dict): if not fields: return conn = _connect() cur = conn.cursor() set_clause = ", ".join([f"{k} = ?" for k in fields]) params = list(fields.values()) + [book_id] cur.execute(f"UPDATE books SET {set_clause} WHERE book_id = ?", params) conn.commit() conn.close() # ------------------------------------------------------------ # STATUS # ------------------------------------------------------------ def sql_set_status(book_id, status: str): conn = _connect() cur = conn.cursor() cur.execute("UPDATE books SET status = ? WHERE book_id = ?", (status, book_id)) conn.commit() conn.close() # ------------------------------------------------------------ # CHAPTER TOTAL (snapshot) # ------------------------------------------------------------ def sql_set_chapters_total(book_id, total: int): conn = _connect() cur = conn.cursor() cur.execute( "UPDATE books SET chapters_total = ? WHERE book_id = ?", (total, book_id) ) conn.commit() conn.close() # ------------------------------------------------------------ # COUNTERS (SNAPSHOT-ONLY) # ------------------------------------------------------------ def sql_inc_downloaded(book_id, amount=1): conn = _connect() cur = conn.cursor() cur.execute( """ UPDATE books SET downloaded = COALESCE(downloaded,0) + ? WHERE book_id = ? """, (amount, book_id), ) conn.commit() conn.close() def sql_inc_parsed(book_id, amount=1): conn = _connect() cur = conn.cursor() cur.execute( """ UPDATE books SET parsed = COALESCE(parsed,0) + ? WHERE book_id = ? """, (amount, book_id), ) conn.commit() conn.close() def sql_inc_audio_done(book_id, amount=1): log(f"[DB-SQL] Incrementing audio done for {book_id} by {amount}") conn = _connect() cur = conn.cursor() cur.execute( """ UPDATE books SET audio_done = COALESCE(audio_done,0) + ? WHERE book_id = ? """, (amount, book_id), ) conn.commit() conn.close() def sql_inc_audio_skipped(book_id, amount=1): log(f"[DB-SQL] Incrementing audio skipped for {book_id} by {amount}") conn = _connect() cur = conn.cursor() cur.execute( """ UPDATE books SET audio_skipped = COALESCE(audio_skipped,0) + ? WHERE book_id = ? """, (amount, book_id), ) conn.commit() conn.close()