Découvrir v2 : rails par type, exploration par genre

- Rails Nouveautés séparés : 🆕 animés (tri date Kitsu) et 🆕 séries & films
  (French-Stream) — chaque type garde son rail, plus d'écrasement mutuel
- Section 🎭 Explorer : chips type × genre — animés via catégories Kitsu,
  séries (/<genre>-series-/) et films (/films/<genre>/) via French-Stream ;
  état dans l'URL (/discover?t=serie&g=medical), partageable
- Scraper : browse(media_type, genre) + catalogue YAML surchargeable ;
  parsing div.short mutualisé avec les nouveautés
- Films réels retirés du mode « Animés » (ils suivent le rail séries)
- Endpoints /api/discover (latest_anime/latest_serie), /api/discover/genres,
  /api/discover/browse — parcours filtré par la préférence du compte
- Warmup démarrage : latest_by_type()
This commit is contained in:
Roman
2026-09-25 14:48:29 +00:00
parent 248ec60d35
commit d747c574b8
12 changed files with 526 additions and 84 deletions
+52 -5
View File
@@ -1,4 +1,4 @@
"""Découverte : nouveautés des sources, incontournables, recommandations."""
"""Découverte : nouveautés par type, incontournables, recommandations, exploration."""
import logging
from typing import Annotated
@@ -7,7 +7,8 @@ from fastapi import APIRouter, Depends, Query
from app.routers.auth import CurrentUser, current_user
from app.routers.search import _allowed_media_types
from app.services.discover import discover
from app.scrapers.base import ScrapeError, get_source
from app.services.discover import ANIME_GENRES, discover
logger = logging.getLogger(__name__)
@@ -21,9 +22,15 @@ async def get_discover(
must_watch_limit: Annotated[int, Query(ge=1, le=20)] = 20,
for_you_limit: Annotated[int, Query(ge=1, le=20)] = 20,
) -> dict:
"""Les trois sections de découverte en une requête (sections vides si source KO)."""
"""Rails de découverte en une requête (sections vides si source KO).
« latest_anime » et « latest_serie » (séries + films réels) sont des rails
indépendants, filtrés par la préférence de contenu du compte.
"""
allowed = _allowed_media_types(user.content_preference)
latest = await discover.latest(latest_limit, allowed)
rails = await discover.latest_by_type(latest_limit)
latest_anime = rails["anime"] if "anime" in allowed else []
latest_serie = (rails["serie"] + rails["film"]) if {"serie", "film"} & allowed else []
# Incontournables et Pour toi sont issus de Kitsu (catalogue animés) :
# en mode séries, elles n'ont pas de sens — on ne les calcule même pas.
if "anime" in allowed:
@@ -32,4 +39,44 @@ async def get_discover(
else:
must_watch = []
for_you = {"based_on": [], "items": []}
return {"latest": latest, "must_watch": must_watch, "for_you": for_you}
return {
"latest_anime": latest_anime,
"latest_serie": latest_serie,
"must_watch": must_watch,
"for_you": for_you,
}
@router.get("/discover/genres")
async def get_genres(user: CurrentUser) -> dict:
"""Catalogue des genres parcourables (page Explorer), selon la préférence."""
allowed = _allowed_media_types(user.content_preference)
catalog: dict[str, list[dict]] = {}
if "anime" in allowed:
catalog["anime"] = [{"key": key, "label": label} for key, label in ANIME_GENRES.items()]
try:
french_stream = get_source("french_stream")
available = french_stream.browse_catalog()
except ScrapeError:
available = {}
for kind in ("serie", "film"):
if kind in allowed and kind in available:
catalog[kind] = [
{"key": key, "label": label} for key, label in available[kind].items()
]
return catalog
@router.get("/discover/browse")
async def get_browse(
user: CurrentUser,
type: Annotated[str, Query(pattern="^(anime|serie|film)$")],
genre: Annotated[str, Query(min_length=1, max_length=40)],
limit: Annotated[int, Query(ge=1, le=40)] = 24,
) -> dict:
"""Titres d'un genre : animés via Kitsu, séries/films via French-Stream."""
if type not in _allowed_media_types(user.content_preference):
return {"items": []} # hors préférence du compte
if type == "anime":
return {"items": await discover.browse_anime(genre, limit)}
return {"items": await discover.browse_serie_film(type, genre, limit)}