Files
ohm_streaming/tests/test_french_stream.py
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

258 lines
10 KiB
Python

"""Tests du scraper French-Stream (séries/films VF-VOSTFR) sur fixtures réelles."""
import json
import pytest
from app.scrapers.base import ScrapeError, get_source, import_all_scrapers
from app.scrapers.sources.french_stream import FrenchStreamScraper
# ------------------------------------------------------------ fixtures
SEARCH_HTML = """
<div class='search-item' onclick="location.href='/9562-breaking-bad-saison-2-streaming-complet-vf-vostfr.html'">
<div class='search-poster'><img src='https://image.tmdb.org/t/p/w400/bb.jpg' alt='Breaking Bad - Saison 2'></div>
<div class='search-info'><div class='search-title'>Breaking Bad - Saison 2</div></div>
</div>
<div class='search-item' onclick="location.href='/1022-inception-streaming-complet-vf-vostfr.html'">
<div class='search-poster'><img src='https://image.tmdb.org/t/p/w400/inc.jpg' alt='Inception'></div>
<div class='search-info'><div class='search-title'>Inception (2010)</div></div>
</div>
<div class='search-item'><div class='search-info'><div class='search-title'>Sans lien</div></div></div>
"""
SERIE_DETAILS_HTML = """
<div id="film-data" data-newsid="9562" data-affiche="https://image.tmdb.org/t/p/w300/bb.jpg"></div>
<div class="fmain"><div class="fcols fx-row"><div class="fmid">
<h1 id="s-title">Breaking Bad - Saison 2 </h1>
<div class="facts">
<span class="certification">-12</span> - <span class="release">2008 -</span>
<span class="genres">Drame, Crime</span> - <span class="runtime">48 min</span>
</div>
<div class="flist clearfix"><div class="fdesc"><p>Walter et Jesse montent leur propre affaire.</p></div></div>
</div></div></div>
<div class="fleft"><div class="fposter"><img src="https://image.tmdb.org/t/p/w400/bb.jpg" alt="Breaking Bad - Saison 2"></div></div>
<div id="serie-config" style="display:none" data-title="Breaking Bad - Saison 2" data-news-id="9562"></div>
"""
FILM_DETAILS_HTML = """
<div id="film-data" data-newsid="1022" data-affiche="https://image.tmdb.org/t/p/w300/inc.jpg"></div>
<div class="fmain"><div class="fcols fx-row"><div class="fmid">
<h1 id="s-title"> Inception - 2010 <span class="tag release_date"></span> </h1>
<div class="facts">
<span class="genres"><a href="/index.php?do=xfsearch&amp;xf=Action">Action</a><a href="/index.php?do=xfsearch&amp;xf=Science-Fiction">Science-Fiction</a></span>
<span class="runtime">- 2h28</span>
</div>
<div class="fdesc clearfix slice-this" id="s-desc">
<p class="desc-text">Résumé du film Inception en streaming complet vf et vostfr hd vod gratuit</p>
Dom Cobb est un voleur expérimenté.
</div>
</div></div></div>
"""
EP_DATA = {
"vf": {
"1": {"vidzy": "https://vidzy.cc/embed-aaa.html", "uqload": "https://uqload.vc/embed-bbb.html"},
"2": {"vidzy": "https://vidzy.cc/embed-ccc.html"},
},
"vostfr": {
"1": {"vidzy": "https://vidzy.cc/embed-ddd.html"},
"2": {"vidzy": "https://vidzy.cc/embed-eee.html"},
},
"vo": {},
"info": {
"1": {"title": "Traqués", "synopsis": "Walt et Jesse...", "poster": "https://img/ep1.jpg"},
"2": {"title": "Chasse à l'homme"},
},
}
FILM_API = {
"players": {
"vidzy": {
"default": "https://vidzy.live/embed-fff.html",
"vff": "https://vidzy.live/embed-fff.html",
"vostfr": "https://vidzy.live/embed-ggg.html",
}
}
}
LATEST_HTML = """
<div class="short"><div class="short-in nl">
<a class="short-poster img-box with-mask" href="/index.php?newsid=15137274" alt="The Drop - Saison 1">
<img src="https://image.tmdb.org/t/p/w300/drop.jpg">
</a>
</div></div>
<div class="short"><div class="short-in nl">
<a class="short-poster img-box with-mask" href="/index.php?newsid=15139056" alt="La Rumeur">
<img src="https://image.tmdb.org/t/p/w300/rumeur.jpg">
</a>
</div></div>
"""
def _soup(html: str):
from bs4 import BeautifulSoup
return BeautifulSoup(html, "lxml")
# ------------------------------------------------------------ source
async def test_search(monkeypatch):
async def fake_fetch(url, **kwargs):
assert url.endswith("/engine/ajax/search.php")
assert kwargs["data"] == {"query": "breaking bad", "page": "1"}
return SEARCH_HTML
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch", fake_fetch)
results = await FrenchStreamScraper().search("breaking bad")
assert len(results) == 2
serie, film = results
assert serie.source_id == "9562"
assert serie.media_type == "serie"
assert serie.title == "Breaking Bad - Saison 2"
assert serie.url == "https://french-stream.lat/9562-breaking-bad-saison-2-streaming-complet-vf-vostfr.html"
assert serie.image_url == "https://image.tmdb.org/t/p/w400/bb.jpg"
assert film.media_type == "film"
assert film.source_id == "1022"
async def test_latest(monkeypatch):
async def fake_fetch_soup(url, **kwargs):
assert url.endswith("/series/")
return _soup(LATEST_HTML)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch_soup", fake_fetch_soup)
results = await FrenchStreamScraper().latest()
assert [(r.source_id, r.media_type) for r in results] == [
("15137274", "serie"),
("15139056", "film"),
]
assert results[0].title == "The Drop - Saison 1"
async def test_browse(monkeypatch):
"""Parcours par genre : page du genre scrapée, type forcé sur les résultats."""
async def fake_fetch_soup(url, **kwargs):
assert url.endswith("/medical-series-/")
return _soup(LATEST_HTML)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch_soup", fake_fetch_soup)
results = await FrenchStreamScraper().browse("serie", "medical")
# LATEST_HTML contient un film sans « Saison » : le type est forcé à serie
assert [r.media_type for r in results] == ["serie", "serie"]
assert results[0].title == "The Drop - Saison 1"
async def test_browse_unknown_category():
with pytest.raises(ScrapeError, match="Catégorie inconnue"):
await FrenchStreamScraper().browse("film", "inexistant")
async def test_get_details_serie(monkeypatch):
async def fake_fetch_soup(url, **kwargs):
assert "newsid=9562" in url
return _soup(SERIE_DETAILS_HTML)
async def fake_fetch(url, **kwargs):
assert "ep-data.php?id=9562" in url
return json.dumps(EP_DATA)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch_soup", fake_fetch_soup)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch", fake_fetch)
details = await FrenchStreamScraper().get_details("9562")
assert details.media_type == "serie"
assert details.title == "Breaking Bad - Saison 2"
assert details.year == 2008
assert details.genres == ["Drame", "Crime"]
assert details.synopsis == "Walter et Jesse montent leur propre affaire."
assert details.image_url == "https://image.tmdb.org/t/p/w400/bb.jpg"
assert details.episode_count == 4
vf1 = details.episodes[0]
assert (vf1.season, vf1.number, vf1.version, vf1.title) == (2, 1.0, "vf", "Traqués")
assert vf1.url.endswith("#vf-1")
versions = {(e.number, e.version) for e in details.episodes}
assert versions == {(1.0, "vf"), (1.0, "vostfr"), (2.0, "vf"), (2.0, "vostfr")}
async def test_get_details_film(monkeypatch):
async def fake_fetch_soup(url, **kwargs):
return _soup(FILM_DETAILS_HTML)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch_soup", fake_fetch_soup)
details = await FrenchStreamScraper().get_details("1022")
assert details.media_type == "film"
assert details.title == "Inception - 2010"
assert details.year == 2010
assert details.genres == ["Action", "Science-Fiction"]
assert "Résumé du film" not in details.synopsis
assert details.episode_count == 1
assert details.episodes[0].url.endswith("#film")
async def test_list_episodes_film(monkeypatch):
async def fake_fetch_soup(url, **kwargs):
return _soup(FILM_DETAILS_HTML)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch_soup", fake_fetch_soup)
episodes = await FrenchStreamScraper().list_episodes("1022")
assert len(episodes) == 1
assert episodes[0].number == 1
async def test_extract_embed_links_episode(monkeypatch):
async def fake_fetch(url, **kwargs):
return json.dumps(EP_DATA)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch", fake_fetch)
links = await FrenchStreamScraper().extract_embed_links(
"https://french-stream.lat/index.php?newsid=9562#vf-1"
)
assert links == ["https://vidzy.cc/embed-aaa.html", "https://uqload.vc/embed-bbb.html"]
async def test_extract_embed_links_film_dedup(monkeypatch):
async def fake_fetch(url, **kwargs):
assert "film_api.php?id=1022" in url
return json.dumps(FILM_API)
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch", fake_fetch)
links = await FrenchStreamScraper().extract_embed_links(
"https://french-stream.lat/index.php?newsid=1022#film"
)
assert links == ["https://vidzy.live/embed-fff.html", "https://vidzy.live/embed-ggg.html"]
async def test_extract_embed_links_bad_fragment():
with pytest.raises(ScrapeError):
await FrenchStreamScraper().extract_embed_links(
"https://french-stream.lat/index.php?newsid=9562#nimportequoi"
)
async def test_extract_embed_links_invalid_json(monkeypatch):
async def fake_fetch(url, **kwargs):
return "<html>oops</html>"
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch", fake_fetch)
with pytest.raises(ScrapeError):
await FrenchStreamScraper().extract_embed_links(
"https://french-stream.lat/index.php?newsid=9562#vf-1"
)
async def test_unknown_detail_page(monkeypatch):
async def fake_fetch_soup(url, **kwargs):
return _soup("<html><body>404</body></html>")
monkeypatch.setattr("app.scrapers.sources.french_stream.fetch_soup", fake_fetch_soup)
with pytest.raises(ScrapeError):
await FrenchStreamScraper().get_details("999999999")
def test_registered():
import_all_scrapers()
source = get_source("french_stream")
assert source.media_types == ("serie", "film")