Nouvelle version réécrite de zéro : recherche multi-sources (Vostfree, French-Manga), extraction 2 niveaux, proxy vidéo intégré, streaming/téléchargement HLS, métadonnées Kitsu, bibliothèque locale, comptes JWT + administration, découverte fusionnée, indexeur Torznab (Sonarr/Prowlarr).
124 lines
4.2 KiB
Python
124 lines
4.2 KiB
Python
"""Couche d'accès SQLite (aiosqlite) — unique source de vérité de l'application."""
|
|
|
|
import aiosqlite
|
|
|
|
from app.config import get_settings
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
password_hash TEXT NOT NULL,
|
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
expires_at TEXT NOT NULL,
|
|
revoked INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS downloads (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source_key TEXT NOT NULL, -- clé de déduplication (video_url normalisée)
|
|
video_url TEXT NOT NULL,
|
|
page_url TEXT,
|
|
title TEXT NOT NULL,
|
|
file_path TEXT, -- relatif au dossier de téléchargement
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending','downloading','paused','done','failed','cancelled')),
|
|
total_bytes INTEGER,
|
|
downloaded_bytes INTEGER NOT NULL DEFAULT 0,
|
|
error TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_downloads_status ON downloads(status);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_downloads_active_source
|
|
ON downloads(source_key) WHERE status IN ('pending','downloading','paused');
|
|
|
|
CREATE TABLE IF NOT EXISTS metadata_cache (
|
|
cache_key TEXT PRIMARY KEY,
|
|
payload TEXT NOT NULL, -- JSON
|
|
fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL -- JSON
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS favorites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
source TEXT NOT NULL,
|
|
source_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
image_url TEXT,
|
|
payload TEXT, -- JSON métadonnées
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE (user_id, source, source_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS watch_progress (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
download_id INTEGER NOT NULL REFERENCES downloads(id) ON DELETE CASCADE,
|
|
position_seconds REAL NOT NULL DEFAULT 0,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE (user_id, download_id)
|
|
);
|
|
"""
|
|
|
|
|
|
class Database:
|
|
"""Connexion SQLite partagée, initialisée au démarrage de l'app."""
|
|
|
|
def __init__(self) -> None:
|
|
self._conn: aiosqlite.Connection | None = None
|
|
|
|
async def connect(self) -> None:
|
|
path = get_settings().database_path
|
|
self._conn = await aiosqlite.connect(path)
|
|
self._conn.row_factory = aiosqlite.Row
|
|
await self._conn.execute("PRAGMA journal_mode=WAL")
|
|
await self._conn.execute("PRAGMA foreign_keys=ON")
|
|
await self._conn.executescript(SCHEMA)
|
|
await self._conn.commit()
|
|
|
|
async def close(self) -> None:
|
|
if self._conn is not None:
|
|
await self._conn.close()
|
|
self._conn = None
|
|
|
|
@property
|
|
def conn(self) -> aiosqlite.Connection:
|
|
if self._conn is None:
|
|
raise RuntimeError("Database.connect() n'a pas été appelé")
|
|
return self._conn
|
|
|
|
async def execute(self, sql: str, params: tuple = ()) -> aiosqlite.Cursor:
|
|
cursor = await self.conn.execute(sql, params)
|
|
await self.conn.commit()
|
|
return cursor
|
|
|
|
async def fetchone(self, sql: str, params: tuple = ()) -> aiosqlite.Row | None:
|
|
cursor = await self.conn.execute(sql, params)
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return row
|
|
|
|
async def fetchall(self, sql: str, params: tuple = ()) -> list[aiosqlite.Row]:
|
|
cursor = await self.conn.execute(sql, params)
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return rows
|
|
|
|
|
|
db = Database()
|