# ============================================================ # File: scraper/utils/state_sync.py # Purpose: # State inspection + optional sync logic for unified book_idx model. # Generates full book-card compatible dicts for debug UI. # ============================================================ import os import redis from db.db import get_db def _build_card(sqlite_row, redis_state, merged): """ Creates a dict that matches the fields required by components/bookcard.html: b.book_idx b.title b.author b.cover_path b.status b.created_at b.download_done b.download_total b.audio_done b.audio_total """ return { "book_idx": sqlite_row.get("book_idx"), "title": sqlite_row.get("title") or "Unknown", "author": sqlite_row.get("author"), "cover_path": sqlite_row.get("cover_path"), # Use merged status (Redis > SQLite) "status": merged.get("status") or sqlite_row.get("status") or "unknown", # Meta "created_at": sqlite_row.get("created_at"), # Download counters "download_done": merged.get("downloaded", 0), "download_total": merged.get("chapters_total", 0), # Audio counters "audio_done": merged.get("audio_done", 0), "audio_total": merged.get("chapters_total", 0), } # ============================================================ # INSPECT ONLY — NO WRITES # ============================================================ def inspect_books_state_depecrated(): """ Reads all books from SQLite and fetches Redis progress. Builds: • entry.sqlite • entry.redis • entry.would_merge_to • entry.card (book-card compatible) """ r = redis.Redis.from_url(os.getenv("REDIS_BROKER"), decode_responses=True) db = get_db() cur = db.cursor() cur.execute("SELECT * FROM books") rows = cur.fetchall() results = [] for row in rows: sqlite_row = dict(row) book_idx = sqlite_row["book_idx"] redis_key = f"book:{book_idx}:state" redis_state = r.hgetall(redis_key) or {} # ================================ # DRY-RUN MERGE LOGIC # ================================ merged = sqlite_row.copy() if redis_state: merged["downloaded"] = int( redis_state.get("chapters_download_done", merged.get("downloaded", 0)) ) merged["parsed"] = int( redis_state.get("chapters_parsed_done", merged.get("parsed", 0)) ) merged["audio_done"] = int( redis_state.get("audio_done", merged.get("audio_done", 0)) ) merged["chapters_total"] = int( redis_state.get("chapters_total", merged.get("chapters_total", 0)) ) merged["status"] = redis_state.get( "status", merged.get("status", "unknown") ) # ================================ # Build book-card data # ================================ card = _build_card(sqlite_row, redis_state, merged) # ================================ # Append final result entry # ================================ results.append( { "book_idx": book_idx, "title": sqlite_row.get("title"), "sqlite": sqlite_row, "redis": redis_state, "would_merge_to": merged, "card": card, } ) return results # ============================================================ # INSPECT ONLY — NO WRITES # ============================================================ def inspect_books_state(): """ Reads canonical book state from repository. Builds: • entry.sqlite • entry.redis • entry.would_merge_to • entry.card (book-card compatible) """ from db.repository import get_book_state from db.db import get_db db = get_db() cur = db.cursor() # Alleen nodig om te weten *welke* books er zijn cur.execute("SELECT book_idx FROM books") rows = cur.fetchall() results = [] for row in rows: book_idx = row["book_idx"] # -------------------------------- # Canonical state (ENIGE waarheid) # -------------------------------- state = get_book_state(book_idx) # SQLite-view = alleen SQLite-kolommen sqlite_view = { k: v for k, v in state.items() if k in ( "book_idx", "title", "author", "description", "cover_path", "book_url", "chapters_total", "status", "downloaded", "parsed", "audio_done", "created_at", "processdate", "last_update", ) } # Redis-view = alleen Redis counters/status redis_view = { k: v for k, v in state.items() if k.startswith("chapters_") or k in ("status", "audio_done", "audio_skipped") } merged = state # letterlijk de canonieke state card = _build_card(sqlite_view, redis_view, merged) results.append( { "book_idx": book_idx, "title": state.get("title"), "sqlite": sqlite_view, "redis": redis_view, "would_merge_to": merged, "card": card, } ) return results # ============================================================ # SYNC REDIS → SQLITE (writes) # ============================================================ def sync_books_from_redis(): """ Writes Redis progress values back into SQLite. Uses unified book_idx as identifier. """ r = redis.Redis.from_url(os.getenv("REDIS_BROKER"), decode_responses=True) db = get_db() cur = db.cursor() cur.execute("SELECT * FROM books") rows = cur.fetchall() results = [] for row in rows: before = dict(row) book_idx = before["book_idx"] redis_key = f"book:{book_idx}:state" redis_state = r.hgetall(redis_key) if not redis_state: results.append( { "book_idx": book_idx, "before": before, "redis": {}, "after": before, } ) continue # Extract progress from Redis downloaded = int(redis_state.get("chapters_download_done", 0)) parsed = int(redis_state.get("chapters_parsed_done", 0)) audio_done = int(redis_state.get("audio_done", 0)) total = int(redis_state.get("chapters_total", 0)) status = redis_state.get("status", before.get("status")) # Update SQLite cur.execute( """ UPDATE books SET downloaded = ?, parsed = ?, audio_done = ?, chapters_total = ?, status = ?, last_update = datetime('now') WHERE book_idx = ? """, (downloaded, parsed, audio_done, total, status, book_idx), ) db.commit() cur.execute("SELECT * FROM books WHERE book_idx = ?", (book_idx,)) after = dict(cur.fetchone()) results.append( { "book_idx": book_idx, "before": before, "redis": redis_state, "after": after, } ) return results