Découverte : le filtre par préférence s'applique avant la troncature du rail

En mode séries, le rail Nouveautés était vide : discover.latest() triait par
date de sortie Kitsu (catalogue animés uniquement) puis tronquait à 24 AVANT
le filtrage — les séries et films réels, absents de Kitsu (donc sans date),
étaient systématiquement évincés.

- allowed (types autorisés par préférence) descend dans discover.latest()
  et filtre dès la collecte des sources
- remplissage du rail équilibré par type (round-robin) : en mode « les deux »,
  8 animés / 8 séries / 8 films au lieu de 24 animés
- test de non-régression : 9 séries conservées face à 30 animés datés
This commit is contained in:
Roman
2026-09-25 14:22:10 +00:00
parent 79a44297fd
commit 248ec60d35
3 changed files with 66 additions and 7 deletions
+25 -5
View File
@@ -92,20 +92,29 @@ class DiscoverService:
# ------------------------------------------------------------ nouveautés
async def latest(self, limit: int = 24) -> list[dict]:
async def latest(self, limit: int = 24, allowed: set[str] | None = None) -> list[dict]:
"""Nouveautés toutes sources confondues, triées par date de sortie réelle.
Les « récemment ajoutés » de chaque source sont fusionnés (doublons retirés),
enrichis via Kitsu (date de début, statut de diffusion) puis triés du plus
récent au plus ancien — ce qui sort / vient de sortir en premier.
``allowed`` restreint aux types de médias autorisés (préférence du compte),
AVANT fusion/tri/troncature : sans lui, les titres hors Kitsu (séries, films
réel) — sans date de sortie — seraient évincés du rail par la troncature.
Le remplissage final équilibre les types présents (round-robin) pour qu'aucun
ne soit écrasé par les autres en mode « les deux ».
"""
cached = self._cache.get(f"latest:{limit}")
cache_key = f"latest:{limit}:{','.join(sorted(allowed)) if allowed else 'all'}"
cached = self._cache.get(cache_key)
if cached is not None:
return cached # type: ignore[return-value]
sources = [s for s in all_sources() if await is_source_enabled(s.name)]
outcomes = await asyncio.gather(*(self._latest_of(source) for source in sources))
items = [item for outcome in outcomes for item in (outcome or [])]
if allowed is not None:
items = [item for item in items if item.get("media_type", "anime") in allowed]
merged: dict[str, dict] = {}
for item in items:
@@ -120,9 +129,20 @@ class DiscoverService:
return await self._with_release_info(item)
enriched = await asyncio.gather(*(bounded(item) for item in merged.values()))
result = sorted(enriched, key=lambda it: it.get("start_date") or "", reverse=True)
result = result[:limit]
self._cache.set(f"latest:{limit}", result, _LATEST_TTL_SECONDS)
by_type: dict[str, list[dict]] = {}
for item in sorted(enriched, key=lambda it: it.get("start_date") or "", reverse=True):
by_type.setdefault(item.get("media_type", "anime"), []).append(item)
result: list[dict] = []
pools = [list(pool) for pool in by_type.values()]
while len(result) < limit and pools:
for pool in pools[:]:
result.append(pool.pop(0))
if len(result) >= limit:
break
if not pool:
pools.remove(pool)
self._cache.set(cache_key, result, _LATEST_TTL_SECONDS)
return result
async def _latest_of(self, source: SourceScraper) -> list[dict] | None: