Pour toi : fallback sans Sonarr et amorçage sans historique
- Sans Sonarr, les séries/films téléchargés donnent leurs genres via leur fiche French-Stream (page_url des téléchargements), convertis vers les catégories Kitsu (mapping FR→EN ; genres sans équivalent ignorés) - Aucun historique (ni téléchargement, ni favori, ni Sonarr) : réponse cold_start non cachée → carte d'amorçage « téléchargez un premier titre » - Fin de téléchargement → invalidation immédiate des recommandations - genres_of_page ignore les fiches hors French-Stream (URLs d'animés)
This commit is contained in:
@@ -67,6 +67,30 @@ ANIME_GENRES: dict[str, str] = {
|
||||
"music": "Musique",
|
||||
}
|
||||
|
||||
# Genres des fiches séries/films (French-Stream, libellés FR, slugifiés) →
|
||||
# catégorie Kitsu équivalente. Les genres sans équivalent (Médical, Western…)
|
||||
# sont ignorés : mieux vaut un signal incomplet qu'une catégorie inexistante.
|
||||
_GENRE_FR_TO_KITSU: dict[str, str] = {
|
||||
"action": "Action",
|
||||
"aventure": "Adventure",
|
||||
"aventures": "Adventure",
|
||||
"comedie": "Comedy",
|
||||
"comedies": "Comedy",
|
||||
"drame": "Drama",
|
||||
"drames": "Drama",
|
||||
"epouvante-horreur": "Horror",
|
||||
"horreur": "Horror",
|
||||
"fantastique": "Fantasy",
|
||||
"fantastiques": "Fantasy",
|
||||
"romance": "Romance",
|
||||
"romances": "Romance",
|
||||
"science-fiction": "Science Fiction",
|
||||
"science-fictions": "Science Fiction",
|
||||
"surnaturel": "Supernatural",
|
||||
"thriller": "Thriller",
|
||||
"thrillers": "Thriller",
|
||||
}
|
||||
|
||||
_LATEST_TTL_SECONDS = 600 # nouveautés : re-scrape au bout de 10 min
|
||||
_MUST_WATCH_TTL_SECONDS = 21600 # incontournables : quasi statique, 6 h
|
||||
_FOR_YOU_TTL_SECONDS = 3600 # recommandations : 1 h (l'historique évolue lentement)
|
||||
@@ -256,14 +280,23 @@ class DiscoverService:
|
||||
if cached is not None:
|
||||
return cached # type: ignore[return-value]
|
||||
|
||||
owned, favorite_genres = await self._owned(user_id)
|
||||
owned, favorite_genres, series_pages = await self._owned(user_id)
|
||||
genre_counts = await self._genres_from_downloads(owned)
|
||||
for genre, count in favorite_genres.items():
|
||||
genre_counts[genre] = genre_counts.get(genre, 0) + count
|
||||
if series_pages:
|
||||
# Séries/films téléchargés : genres lus sur leur fiche source (sans Sonarr)
|
||||
for genre, count in (await self._genres_from_series_pages(series_pages)).items():
|
||||
genre_counts[genre] = genre_counts.get(genre, 0) + count
|
||||
sonarr_owned, sonarr_genres = await sonarr.profile()
|
||||
for genre, count in sonarr_genres.items():
|
||||
genre_counts[genre] = genre_counts.get(genre, 0) + count
|
||||
|
||||
if not (owned or favorite_genres or sonarr_owned):
|
||||
# Aucun historique : proposition d'amorçage plutôt qu'une section muette
|
||||
return {"based_on": [], "items": [], "cold_start": True}
|
||||
owned |= sonarr_owned
|
||||
|
||||
if not genre_counts:
|
||||
result: dict = {"based_on": [], "items": []}
|
||||
self._cache.set(cache_key, result, _FOR_YOU_TTL_SECONDS)
|
||||
@@ -283,22 +316,54 @@ class DiscoverService:
|
||||
self._cache.set(cache_key, result, _FOR_YOU_TTL_SECONDS)
|
||||
return result
|
||||
|
||||
async def _genres_from_series_pages(self, pages: dict[str, str]) -> dict[str, int]:
|
||||
"""Genres Kitsu des séries/films téléchargés, lus sur leur fiche source.
|
||||
|
||||
Les libellés français des fiches sont convertis vers les catégories Kitsu
|
||||
(anglais) ; les genres sans équivalent sont ignorés.
|
||||
"""
|
||||
if not pages:
|
||||
return {}
|
||||
try:
|
||||
scraper = get_source("french_stream")
|
||||
except ScrapeError:
|
||||
return {}
|
||||
counts: dict[str, int] = {}
|
||||
for page_url in list(pages.values())[:_MAX_HISTORY_TITLES]:
|
||||
for genre_fr in await scraper.genres_of_page(page_url):
|
||||
canonical = _GENRE_FR_TO_KITSU.get(category_slug(genre_fr))
|
||||
if canonical:
|
||||
counts[canonical] = counts.get(canonical, 0) + 1
|
||||
return counts
|
||||
|
||||
def invalidate_for_you(self) -> None:
|
||||
"""Recommandations recalculées au prochain appel (réglages Sonarr modifiés)."""
|
||||
self._cache.clear("for_you:")
|
||||
|
||||
async def _owned(self, user_id: int) -> tuple[set[str], dict[str, int]]:
|
||||
"""Titres possédés (normalisés) + genres directement connus via les favoris."""
|
||||
async def _owned(self, user_id: int) -> tuple[set[str], dict[str, int], dict[str, str]]:
|
||||
"""Titres possédés (normalisés), genres des favoris, fiches des téléchargements.
|
||||
|
||||
``series_pages`` relie un titre possédé à l'URL de sa fiche chez la source
|
||||
(séries/films French-Stream) : sans Sonarr, c'est la seule source de genres.
|
||||
"""
|
||||
rows = await db.fetchall(
|
||||
"SELECT DISTINCT title FROM downloads ORDER BY created_at DESC LIMIT ?",
|
||||
"SELECT DISTINCT title, page_url FROM downloads ORDER BY created_at DESC LIMIT ?",
|
||||
(_MAX_HISTORY_TITLES,),
|
||||
)
|
||||
fav_rows = await db.fetchall(
|
||||
"SELECT payload FROM favorites WHERE user_id = ? ORDER BY created_at DESC LIMIT ?",
|
||||
(user_id, _MAX_HISTORY_TITLES),
|
||||
)
|
||||
owned = {normalize_title(row["title"]).casefold() for row in rows}
|
||||
owned.discard("")
|
||||
owned: set[str] = set()
|
||||
series_pages: dict[str, str] = {}
|
||||
for row in rows:
|
||||
key = normalize_title(row["title"]).casefold()
|
||||
if not key:
|
||||
continue
|
||||
owned.add(key)
|
||||
page_url = row["page_url"] or ""
|
||||
if page_url and key not in series_pages:
|
||||
series_pages[key] = page_url
|
||||
genre_counts: dict[str, int] = {}
|
||||
for row in fav_rows:
|
||||
try:
|
||||
@@ -308,7 +373,7 @@ class DiscoverService:
|
||||
for genre in payload.get("genres") or []:
|
||||
if isinstance(genre, str) and genre.strip():
|
||||
genre_counts[genre.strip()] = genre_counts.get(genre.strip(), 0) + 1
|
||||
return owned, genre_counts
|
||||
return owned, genre_counts, series_pages
|
||||
|
||||
async def _genres_from_downloads(self, titles: set[str]) -> dict[str, int]:
|
||||
"""Genres Kitsu des titres téléchargés (cache DB puis recherche)."""
|
||||
|
||||
@@ -533,6 +533,10 @@ class DownloadManager:
|
||||
)
|
||||
await self._emit(download_id)
|
||||
logger.info("Téléchargement terminé (id=%s, %d octets)", download_id, size)
|
||||
# L'historique a changé : les recommandations « Pour toi » se recalculent
|
||||
from app.services.discover import discover
|
||||
|
||||
discover.invalidate_for_you()
|
||||
|
||||
# ------------------------------------------------------------ helpers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user