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:
+71
-18
@@ -154,7 +154,7 @@ async def test_latest_merges_and_sorts_by_release_date(monkeypatch):
|
||||
|
||||
service = DiscoverService()
|
||||
monkeypatch.setattr(service, "_kitsu_match_for_title", fake_match)
|
||||
result = await service.latest(limit=10)
|
||||
result = (await service.latest_by_type(limit=10))["anime"]
|
||||
|
||||
titles = [item["title"] for item in result]
|
||||
assert titles.count("Frieren VOSTFR") + titles.count("Frieren - Saison 1") == 1 # dédoublonné
|
||||
@@ -186,14 +186,14 @@ async def test_latest_orders_by_kitsu_start_date(monkeypatch):
|
||||
|
||||
service = DiscoverService()
|
||||
monkeypatch.setattr(service, "_kitsu_match_for_title", fake_match)
|
||||
result = await service.latest(limit=10)
|
||||
result = (await service.latest_by_type(limit=10))["anime"]
|
||||
assert [item["title"] for item in result] == ["Anime 1", "Anime 0", "Anime 2"]
|
||||
assert result[0]["rating"] is None
|
||||
|
||||
|
||||
async def test_latest_serie_preference_keeps_undated_titles(monkeypatch):
|
||||
"""Régression : en mode séries, les titres sans date Kitsu ne doivent pas être
|
||||
évincés du rail par la troncature (les séries n'existent pas chez Kitsu)."""
|
||||
async def test_latest_by_type_keeps_every_type(monkeypatch):
|
||||
"""Chaque type garde son rail complet : les animés nombreux ne vicient pas
|
||||
le rail séries (les séries n'existent pas chez Kitsu, donc sans date)."""
|
||||
import app.services.discover as discover_module
|
||||
|
||||
animes = [
|
||||
@@ -227,9 +227,11 @@ async def test_latest_serie_preference_keeps_undated_titles(monkeypatch):
|
||||
service = DiscoverService()
|
||||
monkeypatch.setattr(service, "_kitsu_match_for_title", fake_match)
|
||||
|
||||
result = await service.latest(limit=24, allowed={"serie"})
|
||||
assert {item["media_type"] for item in result} == {"serie"}
|
||||
assert len(result) == 9 # les 9 séries, malgré 30 animés sans filtre interne
|
||||
rails = await service.latest_by_type(limit=24)
|
||||
assert len(rails["anime"]) == 24 # tronqué à la limite
|
||||
assert len(rails["serie"]) == 9 # les 9 séries intactes, malgré 30 animés
|
||||
assert all(item["media_type"] == "serie" for item in rails["serie"])
|
||||
|
||||
async def test_latest_skips_broken_source_and_uses_cache(monkeypatch):
|
||||
"""Une source en échec disparaît sans erreur, et le TTL évite les re-scrapes."""
|
||||
import app.services.discover as discover_module
|
||||
@@ -253,8 +255,8 @@ async def test_latest_skips_broken_source_and_uses_cache(monkeypatch):
|
||||
monkeypatch.setattr(discover_module, "is_source_enabled", enabled)
|
||||
|
||||
service = DiscoverService()
|
||||
assert await service.latest() == []
|
||||
assert await service.latest() == []
|
||||
assert await service.latest_by_type() == {"anime": [], "serie": [], "film": []}
|
||||
assert await service.latest_by_type() == {"anime": [], "serie": [], "film": []}
|
||||
assert calls["n"] == 1 # deuxième appel servi depuis le cache TTL
|
||||
|
||||
|
||||
@@ -375,11 +377,18 @@ async def test_api_discover_requires_auth(client):
|
||||
|
||||
|
||||
async def test_api_discover_sections(client, admin_cookies, monkeypatch):
|
||||
async def fake_latest(limit: int = 24, allowed: set[str] | None = None):
|
||||
return [
|
||||
{"source": "vostfree", "label": "Vostfree", "source_id": "a", "title": "T",
|
||||
"start_date": "2026-01-01", "status": "current", "rating": 8.1},
|
||||
]
|
||||
async def fake_latest_by_type(limit: int = 24):
|
||||
return {
|
||||
"anime": [
|
||||
{"source": "vostfree", "label": "Vostfree", "source_id": "a", "title": "T",
|
||||
"media_type": "anime", "start_date": "2026-01-01", "status": "current", "rating": 8.1},
|
||||
],
|
||||
"serie": [
|
||||
{"source": "french_stream", "label": "French-Stream", "source_id": "b",
|
||||
"title": "S", "media_type": "serie"},
|
||||
],
|
||||
"film": [],
|
||||
}
|
||||
|
||||
async def fake_must_watch(limit: int = 20):
|
||||
return [{"kitsu_id": "1", "title": "Attack on Titan", "rating": 8.5}]
|
||||
@@ -387,19 +396,63 @@ async def test_api_discover_sections(client, admin_cookies, monkeypatch):
|
||||
async def fake_for_you(user_id: int, limit: int = 20):
|
||||
return {"based_on": ["Action"], "items": [{"kitsu_id": "2", "title": "X"}]}
|
||||
|
||||
monkeypatch.setattr(discover, "latest", fake_latest)
|
||||
monkeypatch.setattr(discover, "latest_by_type", fake_latest_by_type)
|
||||
monkeypatch.setattr(discover, "must_watch", fake_must_watch)
|
||||
monkeypatch.setattr(discover, "for_you", fake_for_you)
|
||||
|
||||
r = await client.get("/api/discover", cookies=admin_cookies)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["latest"][0]["label"] == "Vostfree"
|
||||
assert data["latest"][0]["start_date"] == "2026-01-01"
|
||||
assert data["latest_anime"][0]["label"] == "Vostfree"
|
||||
assert data["latest_anime"][0]["start_date"] == "2026-01-01"
|
||||
assert data["latest_serie"][0]["label"] == "French-Stream"
|
||||
assert data["must_watch"][0]["title"] == "Attack on Titan"
|
||||
assert data["for_you"]["based_on"] == ["Action"]
|
||||
|
||||
|
||||
async def test_api_browse(client, admin_cookies, monkeypatch):
|
||||
"""Explorer : animés via Kitsu, séries via French-Stream, hors préférence → vide."""
|
||||
|
||||
async def fake_browse_anime(genre: str, limit: int = 20):
|
||||
assert genre == "comedy"
|
||||
return [{"kitsu_id": "1", "title": "Nichijou"}]
|
||||
|
||||
async def fake_browse_serie_film(media_type: str, genre: str, limit: int = 24):
|
||||
assert (media_type, genre) == ("serie", "medical")
|
||||
return [{"source": "french_stream", "source_id": "9", "title": "STAT - Saison 5",
|
||||
"media_type": "serie"}]
|
||||
|
||||
monkeypatch.setattr(discover, "browse_anime", fake_browse_anime)
|
||||
monkeypatch.setattr(discover, "browse_serie_film", fake_browse_serie_film)
|
||||
|
||||
r = await client.get(
|
||||
"/api/discover/browse", params={"type": "anime", "genre": "comedy"}, cookies=admin_cookies
|
||||
)
|
||||
assert r.json()["items"][0]["title"] == "Nichijou"
|
||||
|
||||
r = await client.get(
|
||||
"/api/discover/browse", params={"type": "serie", "genre": "medical"}, cookies=admin_cookies
|
||||
)
|
||||
assert r.json()["items"][0]["title"] == "STAT - Saison 5"
|
||||
|
||||
# Préférence animés : le parcours séries renvoie vide sans même scraper
|
||||
await client.put("/auth/preferences", json={"content_preference": "anime"}, cookies=admin_cookies)
|
||||
r = await client.get(
|
||||
"/api/discover/browse", params={"type": "serie", "genre": "medical"}, cookies=admin_cookies
|
||||
)
|
||||
assert r.json()["items"] == []
|
||||
|
||||
|
||||
async def test_api_genres_catalog(client, admin_cookies):
|
||||
"""Catalogue de genres : animés + séries/films French-Stream en mode « les deux »."""
|
||||
r = await client.get("/api/discover/genres", cookies=admin_cookies)
|
||||
assert r.status_code == 200
|
||||
catalog = r.json()
|
||||
assert {"anime", "serie", "film"} <= set(catalog)
|
||||
assert {"key": "thriller", "label": "Thriller"} in catalog["serie"]
|
||||
assert any(g["key"] == "comedies" for g in catalog["film"])
|
||||
|
||||
|
||||
async def test_discover_page_renders(client, admin_cookies):
|
||||
r = await client.get("/discover", cookies=admin_cookies)
|
||||
assert r.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user