""" Хранение состояния скачивания в SQLite. """ import sqlite3 from datetime import datetime from pathlib import Path from typing import Optional DB_PATH = Path("/app/state/progress.db") class StateDB: def __init__(self, db_path: Path = DB_PATH): db_path.parent.mkdir(parents=True, exist_ok=True) self.conn = sqlite3.connect(str(db_path), check_same_thread=False) self.conn.row_factory = sqlite3.Row self._init() def _init(self): self.conn.execute(""" CREATE TABLE IF NOT EXISTS mangas ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, title TEXT, title_ru TEXT, title_full TEXT, pub_status TEXT DEFAULT 'unknown', auto_update INTEGER DEFAULT 0, last_checked_at TEXT, status TEXT DEFAULT 'queued', format TEXT DEFAULT 'cbz', chapters_total INTEGER DEFAULT 0, chapters_done INTEGER DEFAULT 0, added_at TEXT, updated_at TEXT, started_at TEXT, finished_at TEXT ) """) self.conn.execute(""" CREATE TABLE IF NOT EXISTS chapters ( id INTEGER PRIMARY KEY AUTOINCREMENT, manga_url TEXT NOT NULL, chapter_url TEXT NOT NULL UNIQUE, title TEXT, number REAL, volume INTEGER, status TEXT DEFAULT 'pending', pages_total INTEGER DEFAULT 0, pages_done INTEGER DEFAULT 0, output_cbz TEXT, output_pdf TEXT, output_epub TEXT, updated_at TEXT ) """) self.conn.execute(""" CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, manga_url TEXT NOT NULL, event_type TEXT NOT NULL, chapter_url TEXT, chapter_title TEXT, chapter_number REAL, volume INTEGER, details TEXT, created_at TEXT ) """) # Migrate old DB: add missing columns migrations = [ ("chapters", "pages_total", "INTEGER DEFAULT 0"), ("chapters", "pages_done", "INTEGER DEFAULT 0"), ("mangas", "title_ru", "TEXT"), ("mangas", "title_full", "TEXT"), ("mangas", "pub_status", "TEXT DEFAULT 'unknown'"), ("mangas", "auto_update", "INTEGER DEFAULT 0"), ("mangas", "last_checked_at", "TEXT"), ("mangas", "started_at", "TEXT"), ("mangas", "finished_at", "TEXT"), ] for table, col, typedef in migrations: try: self.conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}") except Exception: pass self.conn.commit() # ── Mangas ──────────────────────────────────── def add_manga(self, url: str, fmt: str = "cbz") -> bool: """Добавляет мангу в очередь. Возвращает True если новая.""" cur = self.conn.execute("SELECT id FROM mangas WHERE url=?", (url,)) if cur.fetchone(): return False self.conn.execute(""" INSERT INTO mangas (url, format, status, added_at, updated_at) VALUES (?, ?, 'queued', ?, ?) """, (url, fmt, _now(), _now())) self.conn.commit() return True def update_manga_info(self, url: str, title: str, chapters_total: int, title_ru: str = "", title_full: str = "", pub_status: str = "unknown"): self.conn.execute(""" UPDATE mangas SET title=?, title_ru=?, title_full=?, pub_status=?, chapters_total=?, updated_at=? WHERE url=? """, (title, title_ru, title_full, pub_status, chapters_total, _now(), url)) self.conn.commit() def set_auto_update(self, url: str, enabled: bool): self.conn.execute(""" UPDATE mangas SET auto_update=?, updated_at=? WHERE url=? """, (1 if enabled else 0, _now(), url)) self.conn.commit() def set_last_checked(self, url: str): self.conn.execute(""" UPDATE mangas SET last_checked_at=?, updated_at=? WHERE url=? """, (_now(), _now(), url)) self.conn.commit() def update_manga_status(self, url: str, status: str): self.conn.execute(""" UPDATE mangas SET status=?, updated_at=? WHERE url=? """, (status, _now(), url)) self.conn.commit() def mark_started(self, url: str) -> str: """Записывает время начала загрузки. Возвращает timestamp.""" ts = _now() self.conn.execute(""" UPDATE mangas SET started_at=?, finished_at=NULL, updated_at=? WHERE url=? """, (ts, ts, url)) self.conn.commit() return ts def mark_finished(self, url: str) -> str: """Записывает время окончания загрузки. Возвращает timestamp.""" ts = _now() self.conn.execute(""" UPDATE mangas SET finished_at=?, updated_at=? WHERE url=? """, (ts, ts, url)) self.conn.commit() return ts def sync_chapters_done(self, url: str): """Синхронизирует chapters_done из реального счёта таблицы chapters.""" count = self.conn.execute( "SELECT COUNT(*) FROM chapters WHERE manga_url=? AND status='done'", (url,) ).fetchone()[0] self.conn.execute( "UPDATE mangas SET chapters_done=?, updated_at=? WHERE url=?", (count, _now(), url) ) self.conn.commit() return count def increment_manga_chapters_done(self, url: str): # Оставлен для совместимости, но не используется в воркере pass def get_manga(self, url: str) -> Optional[dict]: cur = self.conn.execute("SELECT * FROM mangas WHERE url=?", (url,)) row = cur.fetchone() return dict(row) if row else None def get_all_mangas(self) -> list[dict]: cur = self.conn.execute("SELECT * FROM mangas ORDER BY added_at DESC") return [dict(r) for r in cur.fetchall()] def get_manga_format(self, url: str) -> str: cur = self.conn.execute("SELECT format FROM mangas WHERE url=?", (url,)) row = cur.fetchone() return row["format"] if row else "cbz" # ── Chapters ────────────────────────────────── def upsert_chapter(self, manga_url: str, chapter_url: str, title: str = "", number: float = 0, volume: int = 0): self.conn.execute(""" INSERT INTO chapters (manga_url, chapter_url, title, number, volume, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(chapter_url) DO UPDATE SET title = excluded.title, number = excluded.number, volume = excluded.volume """, (manga_url, chapter_url, title, number, volume, _now())) self.conn.commit() def reset_chapter(self, chapter_url: str): self.conn.execute(""" UPDATE chapters SET status='pending', pages_total=0, pages_done=0, output_cbz=NULL, output_pdf=NULL, output_epub=NULL, updated_at=? WHERE chapter_url=? """, (_now(), chapter_url)) self.conn.commit() def mark_done(self, chapter_url: str, fmt: str, output_path: str): col = f"output_{fmt}" self.conn.execute(f""" UPDATE chapters SET status='done', {col}=?, updated_at=? WHERE chapter_url=? """, (output_path, _now(), chapter_url)) self.conn.commit() def mark_failed(self, chapter_url: str): self.conn.execute(""" UPDATE chapters SET status='failed', updated_at=? WHERE chapter_url=? """, (_now(), chapter_url)) self.conn.commit() def update_chapter_pages(self, chapter_url: str, pages_total: int, pages_done: int): self.conn.execute(""" UPDATE chapters SET pages_total=?, pages_done=?, updated_at=? WHERE chapter_url=? """, (pages_total, pages_done, _now(), chapter_url)) self.conn.commit() def get_pending(self, manga_url: str) -> list[dict]: cur = self.conn.execute(""" SELECT chapter_url, title, number, volume FROM chapters WHERE manga_url=? AND status != 'done' ORDER BY volume, number """, (manga_url,)) return [dict(r) for r in cur.fetchall()] def get_all_chapters(self, manga_url: str) -> list[dict]: cur = self.conn.execute(""" SELECT * FROM chapters WHERE manga_url=? ORDER BY volume, number """, (manga_url,)) return [dict(r) for r in cur.fetchall()] def chapter_status(self, chapter_url: str) -> Optional[str]: cur = self.conn.execute( "SELECT status FROM chapters WHERE chapter_url=?", (chapter_url,)) row = cur.fetchone() return row["status"] if row else None def get_all(self, manga_url: str) -> list[dict]: return self.get_all_chapters(manga_url) # ── History ─────────────────────────────────── def add_history(self, manga_url: str, event_type: str, chapter_url: str = "", chapter_title: str = "", chapter_number: float = 0, volume: int = 0, details: str = ""): """ event_type: downloaded | auto_downloaded | new_chapter_found | check_started | check_done """ self.conn.execute(""" INSERT INTO history (manga_url, event_type, chapter_url, chapter_title, chapter_number, volume, details, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, (manga_url, event_type, chapter_url, chapter_title, chapter_number, volume, details, _now())) self.conn.commit() def get_history(self, limit: int = 200, manga_url: str = "") -> list[dict]: if manga_url: cur = self.conn.execute(""" SELECT h.*, m.title as manga_title, m.title_ru FROM history h LEFT JOIN mangas m ON h.manga_url = m.url WHERE h.manga_url=? ORDER BY h.created_at DESC LIMIT ? """, (manga_url, limit)) else: cur = self.conn.execute(""" SELECT h.*, m.title as manga_title, m.title_ru FROM history h LEFT JOIN mangas m ON h.manga_url = m.url ORDER BY h.created_at DESC LIMIT ? """, (limit,)) return [dict(r) for r in cur.fetchall()] def get_autos(self) -> list[dict]: """Манги с включённым авто-обновлением.""" cur = self.conn.execute(""" SELECT * FROM mangas WHERE auto_update=1 AND status NOT IN ('downloading') """) return [dict(r) for r in cur.fetchall()] def close(self): self.conn.close() def _now() -> str: return datetime.utcnow().isoformat()