Files
manga/src/state.py
2026-04-29 16:50:04 +03:00

294 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Хранение состояния скачивания в 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()