|
|
|
|
@ -9,7 +9,7 @@
|
|
|
|
|
# - Provide a clean API for tasks and Flask UI
|
|
|
|
|
# ============================================================
|
|
|
|
|
# ============================================================
|
|
|
|
|
# File: db/repository.py (UPDATED for book_idx-only architecture)
|
|
|
|
|
# UPDATED — canonical read model via get_book_state
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
from scraper.logger_decorators import logcall
|
|
|
|
|
@ -17,7 +17,6 @@ from logbus.publisher import log
|
|
|
|
|
|
|
|
|
|
import redis
|
|
|
|
|
import os
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# SQL low-level engines (snapshot storage)
|
|
|
|
|
@ -29,10 +28,6 @@ from db.state_sql import (
|
|
|
|
|
sql_set_chapters_total,
|
|
|
|
|
sql_register_book,
|
|
|
|
|
sql_update_book,
|
|
|
|
|
sql_inc_downloaded,
|
|
|
|
|
sql_inc_parsed,
|
|
|
|
|
sql_inc_audio_done,
|
|
|
|
|
sql_inc_audio_skipped,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
@ -49,80 +44,34 @@ from db.state_redis import (
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Redis setup for legacy progress paths
|
|
|
|
|
# Redis client (read-only for legacy + guards)
|
|
|
|
|
# ============================================================
|
|
|
|
|
REDIS_URL = os.getenv("REDIS_BROKER", "redis://redis:6379/0")
|
|
|
|
|
_r = redis.Redis.from_url(REDIS_URL, decode_responses=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# INTERNAL — LEGACY PROGRESS HELPERS (kept for UI)
|
|
|
|
|
# Keys remain: progress:{book_idx}:*
|
|
|
|
|
# LEGACY PROGRESS (UI only, unchanged)
|
|
|
|
|
# ============================================================
|
|
|
|
|
def _legacy_set_total(book_idx, total):
|
|
|
|
|
_r.set(f"progress:{book_idx}:total", total)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _legacy_inc_completed(book_idx):
|
|
|
|
|
_r.incr(f"progress:{book_idx}:completed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _legacy_inc_skipped(book_idx):
|
|
|
|
|
_r.incr(f"progress:{book_idx}:skipped")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _legacy_inc_failed(book_idx):
|
|
|
|
|
_r.incr(f"progress:{book_idx}:failed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _legacy_add_failed_chapter(book_idx, chapter, reason):
|
|
|
|
|
entry = f"Chapter {chapter}: {reason}"
|
|
|
|
|
_r.rpush(f"progress:{book_idx}:failed_list", entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _legacy_get_failed_list(book_idx):
|
|
|
|
|
return _r.lrange(f"progress:{book_idx}:failed_list", 0, -1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _legacy_get_progress(book_idx):
|
|
|
|
|
total = int(_r.get(f"progress:{book_idx}:total") or 0)
|
|
|
|
|
completed = int(_r.get(f"progress:{book_idx}:completed") or 0)
|
|
|
|
|
skipped = int(_r.get(f"progress:{book_idx}:skipped") or 0)
|
|
|
|
|
failed = int(_r.get(f"progress:{book_idx}:failed") or 0)
|
|
|
|
|
abort = _r.exists(f"abort:{book_idx}") == 1
|
|
|
|
|
failed_list = _legacy_get_failed_list(book_idx)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"book_idx": book_idx,
|
|
|
|
|
"total": total,
|
|
|
|
|
"completed": completed,
|
|
|
|
|
"skipped": skipped,
|
|
|
|
|
"failed": failed,
|
|
|
|
|
"failed_list": failed_list,
|
|
|
|
|
"abort": abort,
|
|
|
|
|
"total": int(_r.get(f"progress:{book_idx}:total") or 0),
|
|
|
|
|
"completed": int(_r.get(f"progress:{book_idx}:completed") or 0),
|
|
|
|
|
"skipped": int(_r.get(f"progress:{book_idx}:skipped") or 0),
|
|
|
|
|
"failed": int(_r.get(f"progress:{book_idx}:failed") or 0),
|
|
|
|
|
"abort": _r.exists(f"abort:{book_idx}") == 1,
|
|
|
|
|
"failed_list": _r.lrange(f"progress:{book_idx}:failed_list", 0, -1),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# PUBLIC — PROGRESS API
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def get_progress(book_idx):
|
|
|
|
|
return _legacy_get_progress(book_idx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def add_failed_chapter(book_idx, chapter, reason):
|
|
|
|
|
_legacy_add_failed_chapter(book_idx, chapter, reason)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def get_failed_list(book_idx):
|
|
|
|
|
return _legacy_get_failed_list(book_idx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# FETCH OPERATIONS (SQLite snapshot)
|
|
|
|
|
# FETCH (SQLite snapshot)
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def fetch_book(book_idx):
|
|
|
|
|
@ -135,7 +84,7 @@ def fetch_all_books():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# INIT-FLOW (SQLite metadata only)
|
|
|
|
|
# INIT / UPDATE METADATA
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def register_book(
|
|
|
|
|
@ -147,26 +96,22 @@ def register_book(
|
|
|
|
|
cover_path=None,
|
|
|
|
|
book_url=None,
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
fields = {
|
|
|
|
|
"book_idx": book_idx,
|
|
|
|
|
"title": title,
|
|
|
|
|
"author": author,
|
|
|
|
|
"description": description,
|
|
|
|
|
"cover_url": cover_url,
|
|
|
|
|
"cover_path": cover_path,
|
|
|
|
|
"book_url": book_url,
|
|
|
|
|
"chapters_total": 0,
|
|
|
|
|
"status": "registered",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(f"[DB] Registering new book_idx={book_idx} title='{title}'")
|
|
|
|
|
sql_register_book(book_idx, fields)
|
|
|
|
|
sql_register_book(
|
|
|
|
|
book_idx,
|
|
|
|
|
{
|
|
|
|
|
"book_idx": book_idx,
|
|
|
|
|
"title": title,
|
|
|
|
|
"author": author,
|
|
|
|
|
"description": description,
|
|
|
|
|
"cover_url": cover_url,
|
|
|
|
|
"cover_path": cover_path,
|
|
|
|
|
"book_url": book_url,
|
|
|
|
|
"chapters_total": 0,
|
|
|
|
|
"status": "registered",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# SCRAPE-FLOW UPDATE
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def update_book_after_full_scrape(
|
|
|
|
|
book_idx,
|
|
|
|
|
@ -176,9 +121,7 @@ def update_book_after_full_scrape(
|
|
|
|
|
cover_url=None,
|
|
|
|
|
chapters_total=None,
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
fields = {}
|
|
|
|
|
|
|
|
|
|
if title is not None:
|
|
|
|
|
fields["title"] = title
|
|
|
|
|
if author is not None:
|
|
|
|
|
@ -191,143 +134,72 @@ def update_book_after_full_scrape(
|
|
|
|
|
fields["chapters_total"] = chapters_total
|
|
|
|
|
|
|
|
|
|
fields["status"] = "active"
|
|
|
|
|
|
|
|
|
|
log(f"[DB] update metadata for book_idx={book_idx}")
|
|
|
|
|
sql_update_book(book_idx, fields)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# ACTIVE BOOK LISTS
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def get_registered_books():
|
|
|
|
|
all_books = sql_fetch_all_books()
|
|
|
|
|
HIDDEN_STATES = {"hidden"}
|
|
|
|
|
log(f"[DB] Fetched all books for registered filter, total={len(all_books)}")
|
|
|
|
|
return [b for b in all_books if b.get("status") not in HIDDEN_STATES]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def get_active_books():
|
|
|
|
|
all_books = sql_fetch_all_books()
|
|
|
|
|
|
|
|
|
|
HIDDEN_STATES = {"hidden", "done"}
|
|
|
|
|
log(f"[DB] Fetched all books for active filter, total={len(all_books)}")
|
|
|
|
|
return [b for b in all_books if b.get("status") not in HIDDEN_STATES]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# STATUS MANAGEMENT
|
|
|
|
|
# STATUS
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def set_status(book_idx, status):
|
|
|
|
|
log(f"[DB] Setting status for {book_idx} to '{status}'")
|
|
|
|
|
redis_set_status(book_idx, status)
|
|
|
|
|
sql_set_status(book_idx, status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# CHAPTER TOTALS
|
|
|
|
|
# TOTALS
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def set_chapters_total(book_idx, total):
|
|
|
|
|
log(f"[DB] Setting chapter total for {book_idx} to {total}")
|
|
|
|
|
redis_set_chapters_total(book_idx, total)
|
|
|
|
|
sql_set_chapters_total(book_idx, total)
|
|
|
|
|
# _legacy_set_total(book_idx, total)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# COUNTERS — DOWNLOAD
|
|
|
|
|
# COUNTERS — WRITE ONLY
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_download_done(book_idx, amount=1):
|
|
|
|
|
log(f"[DB] Incrementing download done for {book_idx} by {amount}")
|
|
|
|
|
redis_inc_download_done(book_idx, amount)
|
|
|
|
|
# sql_inc_downloaded(book_idx, amount)
|
|
|
|
|
# _legacy_inc_completed(book_idx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_download_skipped(book_idx, amount=1):
|
|
|
|
|
log(f"[DB] Incrementing download skipped for {book_idx} by {amount}")
|
|
|
|
|
redis_inc_download_skipped(book_idx, amount)
|
|
|
|
|
# _legacy_inc_skipped(book_idx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# COUNTERS — PARSE
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_parsed_done(book_idx, amount=1):
|
|
|
|
|
log(f"[DB] Incrementing parsed done for {book_idx} by {amount}")
|
|
|
|
|
redis_inc_parsed_done(book_idx, amount)
|
|
|
|
|
# sql_inc_parsed(book_idx, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# COUNTERS — AUDIO
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_audio_skipped(book_idx, amount=1):
|
|
|
|
|
log(f"[DB] Incrementing audio skipped for {book_idx} by {amount}")
|
|
|
|
|
# sql_inc_audio_skipped(book_idx, amount)
|
|
|
|
|
redis_inc_audio_skipped(book_idx, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_audio_done(book_idx, amount=1):
|
|
|
|
|
log(f"[DB] Incrementing audio done for {book_idx} by {amount}")
|
|
|
|
|
redis_inc_audio_done(book_idx, amount)
|
|
|
|
|
# sql_inc_audio_done(book_idx, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# BACKWARDS COMPATIBILITY SHIMS
|
|
|
|
|
# These map the old API (book_id) to the new book_idx-only system
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_downloaded(book_idx, amount=1):
|
|
|
|
|
return inc_download_done(book_idx, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_parsed(book_idx, amount=1):
|
|
|
|
|
return inc_parsed_done(book_idx, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def inc_audio_done_legacy(book_idx, amount=1):
|
|
|
|
|
return inc_audio_done(book_idx, amount)
|
|
|
|
|
def inc_audio_skipped(book_idx, amount=1):
|
|
|
|
|
redis_inc_audio_skipped(book_idx, amount)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# READ — DERIVED BOOK STATE
|
|
|
|
|
# CANONICAL READ MODEL
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def get_book_state(book_idx):
|
|
|
|
|
"""
|
|
|
|
|
Canonical merged read-model for a single book.
|
|
|
|
|
Canonical merged read model.
|
|
|
|
|
|
|
|
|
|
Gedrag:
|
|
|
|
|
- Leest SQL (snapshot)
|
|
|
|
|
- Leest Redis (live counters)
|
|
|
|
|
- Rekent naar merged
|
|
|
|
|
- GEEN writes
|
|
|
|
|
- GEEN side-effects
|
|
|
|
|
|
|
|
|
|
Merge-regels:
|
|
|
|
|
Rules:
|
|
|
|
|
- SQL = snapshot baseline
|
|
|
|
|
- Redis = live counters
|
|
|
|
|
- merged = max(sql, redis)
|
|
|
|
|
- merged wordt gecapt op chapters_total
|
|
|
|
|
- capped at chapters_total
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# 1. Fetch bronnen
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
sqlite_row = sql_fetch_book(book_idx) or {}
|
|
|
|
|
|
|
|
|
|
key = f"book:{book_idx}:state"
|
|
|
|
|
redis_state = _r.hgetall(key) or {}
|
|
|
|
|
redis_state = _r.hgetall(f"book:{book_idx}:state") or {}
|
|
|
|
|
|
|
|
|
|
def _int(v):
|
|
|
|
|
try:
|
|
|
|
|
@ -335,56 +207,114 @@ def get_book_state(book_idx):
|
|
|
|
|
except Exception:
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# 2. SQL snapshot
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
chapters_total = _int(sqlite_row.get("chapters_total"))
|
|
|
|
|
|
|
|
|
|
# SQL snapshot
|
|
|
|
|
sql_downloaded = _int(sqlite_row.get("downloaded"))
|
|
|
|
|
sql_audio_done = _int(sqlite_row.get("audio_done"))
|
|
|
|
|
sql_audio_skipped = _int(sqlite_row.get("audio_skipped"))
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# 3. Redis live counters
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# Redis live
|
|
|
|
|
redis_downloaded = _int(redis_state.get("chapters_download_done")) + _int(
|
|
|
|
|
redis_state.get("chapters_download_skipped")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
redis_audio_done = _int(redis_state.get("audio_done"))
|
|
|
|
|
redis_audio_skipped = _int(redis_state.get("audio_skipped"))
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# 4. Merge (SQL vs Redis)
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# Merge
|
|
|
|
|
merged_downloaded = max(sql_downloaded, redis_downloaded)
|
|
|
|
|
merged_audio_done = max(sql_audio_done, redis_audio_done)
|
|
|
|
|
merged_audio_skipped = max(sql_audio_skipped, redis_audio_skipped)
|
|
|
|
|
|
|
|
|
|
if chapters_total > 0:
|
|
|
|
|
merged_downloaded = min(merged_downloaded, chapters_total)
|
|
|
|
|
merged_audio_done = min(merged_audio_done, chapters_total)
|
|
|
|
|
merged_audio_skipped = min(merged_audio_skipped, chapters_total)
|
|
|
|
|
|
|
|
|
|
audio_completed = merged_audio_done + merged_audio_skipped
|
|
|
|
|
|
|
|
|
|
# Build state
|
|
|
|
|
state = dict(sqlite_row)
|
|
|
|
|
state.update(
|
|
|
|
|
{
|
|
|
|
|
"downloaded": merged_downloaded,
|
|
|
|
|
"audio_done": merged_audio_done,
|
|
|
|
|
"audio_skipped": merged_audio_skipped,
|
|
|
|
|
"chapters_total": chapters_total,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# 5. Bouw merged state (read-only)
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
state = {}
|
|
|
|
|
|
|
|
|
|
# Basis = SQL
|
|
|
|
|
state.update(sqlite_row)
|
|
|
|
|
|
|
|
|
|
# Overschrijf alleen met merged conclusies
|
|
|
|
|
state["downloaded"] = merged_downloaded
|
|
|
|
|
state["audio_done"] = merged_audio_done
|
|
|
|
|
state["chapters_total"] = chapters_total
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
# 4b. Derive status (READ-ONLY)
|
|
|
|
|
# ----------------------------------------------------
|
|
|
|
|
derived_status = sqlite_row.get("status") or "unknown"
|
|
|
|
|
|
|
|
|
|
# Derived status
|
|
|
|
|
status = sqlite_row.get("status") or "unknown"
|
|
|
|
|
if chapters_total > 0:
|
|
|
|
|
if merged_downloaded < chapters_total:
|
|
|
|
|
derived_status = "downloading"
|
|
|
|
|
elif merged_downloaded == chapters_total and merged_audio_done < chapters_total:
|
|
|
|
|
derived_status = "audio"
|
|
|
|
|
elif merged_audio_done == chapters_total:
|
|
|
|
|
derived_status = "done"
|
|
|
|
|
state["status"] = derived_status
|
|
|
|
|
status = "downloading"
|
|
|
|
|
elif merged_downloaded == chapters_total and audio_completed < chapters_total:
|
|
|
|
|
status = "audio"
|
|
|
|
|
elif audio_completed >= chapters_total:
|
|
|
|
|
status = "done"
|
|
|
|
|
|
|
|
|
|
state["status"] = status
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# READ HELPERS (VIA get_book_state ONLY)
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def get_chapters_total(book_idx):
|
|
|
|
|
return int(get_book_state(book_idx).get("chapters_total", 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def get_audio_done(book_idx):
|
|
|
|
|
return int(get_book_state(book_idx).get("audio_done", 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def get_audio_completed_total(book_idx):
|
|
|
|
|
state = get_book_state(book_idx)
|
|
|
|
|
return int(state.get("audio_done", 0)) + int(state.get("audio_skipped", 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# STATUSCHECK GUARD (INTENTIONAL DIRECT REDIS)
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def try_trigger_statuscheck(book_idx):
|
|
|
|
|
return bool(_r.set(f"book:{book_idx}:statuscheck:triggered", "1", nx=True))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# ACTIVE / REGISTERED BOOK LISTS (UI API)
|
|
|
|
|
# ============================================================
|
|
|
|
|
@logcall
|
|
|
|
|
def get_registered_books():
|
|
|
|
|
"""
|
|
|
|
|
Books visible in the 'registered' list in the UI.
|
|
|
|
|
"""
|
|
|
|
|
all_books = sql_fetch_all_books()
|
|
|
|
|
HIDDEN_STATES = {"hidden"}
|
|
|
|
|
return [b for b in all_books if b.get("status") not in HIDDEN_STATES]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def get_active_books():
|
|
|
|
|
"""
|
|
|
|
|
Books currently active in the dashboard.
|
|
|
|
|
"""
|
|
|
|
|
all_books = sql_fetch_all_books()
|
|
|
|
|
HIDDEN_STATES = {"hidden", "done"}
|
|
|
|
|
return [b for b in all_books if b.get("status") not in HIDDEN_STATES]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@logcall
|
|
|
|
|
def store_m4b_error(book_idx: str, volume: str, error_text: str):
|
|
|
|
|
"""
|
|
|
|
|
Passive storage of m4b errors.
|
|
|
|
|
No logic, no retries, no state transitions.
|
|
|
|
|
"""
|
|
|
|
|
key = f"book:{book_idx}:m4b:errors"
|
|
|
|
|
entry = f"{volume}: {error_text}"
|
|
|
|
|
|
|
|
|
|
_r.rpush(key, entry)
|
|
|
|
|
|