Préférence de contenus par compte : animés / séries / les deux

- Colonne users.content_preference (anime|serie|both, défaut both) avec
  migration automatique des bases existantes.
- GET /auth/me expose la préférence ; PUT /auth/preferences la met à
  jour (422 si valeur invalide).
- /api/search : filtre les résultats par media_type ET ignore les
  sources hors périmètre (une source anime-only n'est pas interrogée
  en mode séries). Les films restent associés aux animés.
- /api/discover : nouveautés filtrées selon la même règle.
- Sélecteur compact dans la topbar (⛩/📺/✨), persisté serveur.
- 7 tests dédiés (roundtrip, rejet invalide, filtrage search/discover,
  skip de sources) — 114 au total, ruff clean.

Prête pour la première source de séries : il ne reste qu'un module
SourceScraper avec media_types=("serie",) à écrire.
This commit is contained in:
Roman
2026-09-23 15:02:59 +00:00
parent 65761885ba
commit 28f1bbde31
9 changed files with 237 additions and 15 deletions
+26 -4
View File
@@ -25,6 +25,10 @@ class User:
username: str
is_admin: bool
is_active: bool
content_preference: str = "both" # anime | serie | both
VALID_CONTENT_PREFERENCES = ("anime", "serie", "both")
# ---------------------------------------------------------------- mots de passe
@@ -56,7 +60,8 @@ async def create_user(username: str, password: str) -> User:
async def authenticate(username: str, password: str) -> User | None:
row = await db.fetchone(
"SELECT id, username, password_hash, is_admin, is_active FROM users WHERE username = ?",
"SELECT id, username, password_hash, is_admin, is_active, content_preference "
"FROM users WHERE username = ?",
(username.strip(),),
)
if row is None or not verify_password(password, row["password_hash"]):
@@ -66,18 +71,35 @@ async def authenticate(username: str, password: str) -> User | None:
logger.warning("Compte désactivé : %r", username)
return None
return User(
id=row["id"], username=row["username"], is_admin=bool(row["is_admin"]), is_active=True
id=row["id"],
username=row["username"],
is_admin=bool(row["is_admin"]),
is_active=True,
content_preference=row["content_preference"],
)
async def get_user(user_id: int) -> User | None:
row = await db.fetchone(
"SELECT id, username, is_admin, is_active FROM users WHERE id = ?", (user_id,)
"SELECT id, username, is_admin, is_active, content_preference FROM users WHERE id = ?",
(user_id,),
)
if row is None or not row["is_active"]:
return None
return User(
id=row["id"], username=row["username"], is_admin=bool(row["is_admin"]), is_active=True
id=row["id"],
username=row["username"],
is_admin=bool(row["is_admin"]),
is_active=True,
content_preference=row["content_preference"],
)
async def set_content_preference(user_id: int, preference: str) -> None:
if preference not in VALID_CONTENT_PREFERENCES:
raise ValueError(f"Préférence invalide : {preference!r}")
await db.execute(
"UPDATE users SET content_preference = ? WHERE id = ?", (preference, user_id)
)