Files
Roman d747c574b8 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()
2026-09-25 14:48:29 +00:00

83 lines
3.2 KiB
Python

"""Découverte : nouveautés par type, incontournables, recommandations, exploration."""
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from app.routers.auth import CurrentUser, current_user
from app.routers.search import _allowed_media_types
from app.scrapers.base import ScrapeError, get_source
from app.services.discover import ANIME_GENRES, discover
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["discover"], dependencies=[Depends(current_user)])
@router.get("/discover")
async def get_discover(
user: CurrentUser,
latest_limit: Annotated[int, Query(ge=1, le=50)] = 24,
must_watch_limit: Annotated[int, Query(ge=1, le=20)] = 20,
for_you_limit: Annotated[int, Query(ge=1, le=20)] = 20,
) -> dict:
"""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)
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:
must_watch = await discover.must_watch(must_watch_limit)
for_you = await discover.for_you(user.id, for_you_limit)
else:
must_watch = []
for_you = {"based_on": [], "items": []}
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)}