Base app
This commit is contained in:
215
src/state.py
215
src/state.py
@@ -13,10 +13,31 @@ 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))
|
||||
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,
|
||||
@@ -26,14 +47,137 @@ class StateDB:
|
||||
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("""
|
||||
@@ -46,6 +190,14 @@ class StateDB:
|
||||
""", (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"""
|
||||
@@ -60,6 +212,12 @@ class StateDB:
|
||||
""", (_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
|
||||
@@ -67,21 +225,64 @@ class StateDB:
|
||||
WHERE manga_url=? AND status != 'done'
|
||||
ORDER BY volume, number
|
||||
""", (manga_url,))
|
||||
cols = [d[0] for d in cur.description]
|
||||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||
return [dict(r) for r in cur.fetchall()]
|
||||
|
||||
def get_all(self, manga_url: str) -> list[dict]:
|
||||
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,))
|
||||
cols = [d[0] for d in cur.description]
|
||||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||
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[0] if row else None
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user