v0.1.0 — Réécriture complète de OhmStreaming
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).
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user