- 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()
83 lines
3.2 KiB
Python
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)}
|