Ajout de la source French-Stream (french-stream.lat) : séries et films
- Scraper DataLife Engine : recherche AJAX, fiches par saison, épisodes via ep-data.php (VF/VOSTFR), films via film_api.php, nouveautés /series/ - Config YAML externalisée (sélecteurs réparables sans coder) - Domaines étendus : uqload.vc, vidzy.cc/.live - 11 tests (126 au total)
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
"""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&xf=Action">Action</a><a href="/index.php?do=xfsearch&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_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")
|
||||
Reference in New Issue
Block a user