Files
ViewIT/tests/test_moflix_plugin.py

712 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests für das Moflix-Stream-Plugin.
Mockt _get_json() auf Instance-Ebene um reale HTTP-Requests zu vermeiden.
Testet u.a. den Cross-Invocation-Cache-Miss-Bug (leere Instanz ohne Vorsuche).
"""
import asyncio
from addon.plugins.moflix_plugin import MoflixPlugin, GENRE_SLUGS, COLLECTION_SLUGS, _unpack_packer
# ---------------------------------------------------------------------------
# JSON-Fixtures (realistische Moflix-API-Antworten)
# ---------------------------------------------------------------------------
SEARCH_RESPONSE = {
"results": [
{
"id": "123",
"name": "Breaking Bad",
"is_series": True,
"description": "Chemie-Lehrer wird Drogenboss.",
"poster": "https://cdn.example.com/bb.jpg",
"backdrop": "https://cdn.example.com/bb-bg.jpg",
"model_type": "title",
},
{
"id": "456",
"name": "Inception",
"is_series": False,
"description": "Ein Traum im Traum.",
"poster": "https://cdn.example.com/inc.jpg",
"backdrop": "https://cdn.example.com/inc-bg.jpg",
"model_type": "title",
},
# Personen-Eintrag soll übersprungen werden
{"id": "789", "name": "Christopher Nolan", "model_type": "person"},
]
}
TITLE_RESPONSE_SERIES = {
"title": {
"id": "123",
"name": "Breaking Bad",
"description": "Chemie-Lehrer wird Drogenboss.",
"poster": "https://cdn.example.com/bb.jpg",
"backdrop": "https://cdn.example.com/bb-bg.jpg",
"rating": 9.5,
"release_date": "2008-01-20",
},
"seasons": {
"data": [
{"number": 2, "title_id": "1002"}, # absichtlich unsortiert
{"number": 1, "title_id": "1001"},
]
},
}
TITLE_RESPONSE_MOVIE = {
"title": {
"id": "456",
"name": "Inception",
"description": "Ein Traum im Traum.",
"poster": "https://cdn.example.com/inc.jpg",
"backdrop": "https://cdn.example.com/inc-bg.jpg",
"rating": 8.8,
"release_date": "2010-07-15",
"videos": [
# gupload.xyz wird übersprungen (_VIDEO_SKIP_DOMAINS)
{"quality": "1080p", "src": "https://gupload.xyz/data/e/deadbeef", "name": "Mirror 1"},
# vidara.to wird bevorzugt
{"quality": "1080p", "src": "https://vidara.to/e/inc7testXYZ", "name": "Mirror 2"},
],
},
"seasons": {"data": []},
}
EPISODES_RESPONSE = {
"pagination": {
"data": [
{"episode_number": 1, "name": "Pilot", "primary_video": {"id": 1}},
{"episode_number": 2, "name": "Cat's in the Bag", "primary_video": {"id": 2}},
# primary_video=None → überspringen
{"episode_number": 3, "name": "Kein Video", "primary_video": None},
]
}
}
# Episoden-Detail-Response (für stream_link_for, enthält videos[] mit src-URLs)
EPISODE_DETAIL_RESPONSE = {
"episode": {
"videos": [
# gupload.xyz wird übersprungen
{"quality": "1080p", "src": "https://gupload.xyz/data/e/ep1hash", "name": "Mirror 1"},
# vidara.to wird bevorzugt → dieser src wird zurückgegeben
{"quality": "1080p", "src": "https://vidara.to/e/ep1vidara", "name": "Mirror 2"},
# YouTube → immer überspringen
{"quality": None, "src": "https://youtube.com/watch?v=abc", "name": "Trailer"},
]
}
}
VIDARA_STREAM_RESPONSE = {
"filecode": "ep1vidara",
"streaming_url": "https://cdn.example.com/hls/ep1/master.m3u8",
"subtitles": None,
"thumbnail": "https://cdn.example.com/thumb.jpg",
"title": "",
}
# Minimales HTML mit p.a.c.k.e.r.-obfuskiertem JS (VidHide-Format).
# Packed-String kodiert:
# var links={"hls2":"https://cdn.example.com/hls/test/master.m3u8"};
# jwplayer("vplayer").setup({sources:[{file:links.hls2,type:"hls"}]});
# mit base=36 und keywords: var|links|hls2|jwplayer|vplayer|setup|sources|file|type
VIDHIDE_HTML = (
"<html><body><script>"
"eval(function(p,a,c,k,e,d){"
"e=function(c){return c.toString(36)};"
"if(!''.replace(/^/,String)){while(c--){d[c.toString(a)]=k[c]||c.toString(a)}"
"k=[function(e){return d[e]}];e=function(){return'\\\\w+'};c=1};"
"while(c--){if(k[c]){p=p.replace(new RegExp('\\\\b'+e(c)+'\\\\b','g'),k[c])}};"
"return p}"
"('0 1={\"2\":\"https://cdn.example.com/hls/test/master.m3u8\"};3(\"4\").5({6:[{7:1.2,8:\"hls\"}]});',"
"36,9,'var|links|hls2|jwplayer|vplayer|setup|sources|file|type'.split('|'),0,0))"
"</script></body></html>"
)
CHANNEL_RESPONSE = {
"channel": {
"content": {
"data": [
{
"id": "100",
"name": "Squid Game",
"is_series": True,
"description": "Spiele.",
"poster": "https://cdn.example.com/sq.jpg",
"backdrop": "",
},
{
"id": "200",
"name": "The Crown",
"is_series": True,
"description": "",
"poster": "",
"backdrop": "",
},
]
}
}
}
# ---------------------------------------------------------------------------
# Hilfsfunktion: URL-basiertes Mock-Routing
# ---------------------------------------------------------------------------
def make_json_router(**routes):
"""Erzeugt eine _get_json-Mock, die URL-abhängig antwortet.
Schlüssel = Substring der URL, Wert = zurückzugebende JSON-Daten.
Reihenfolge: spezifischere Schlüssel zuerst übergeben (dict-Reihenfolge).
"""
def _router(url, headers=None):
for key, response in routes.items():
if key in url:
return response
return None
return _router
# ---------------------------------------------------------------------------
# Tests: search_titles
# ---------------------------------------------------------------------------
def test_search_titles_returns_names(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: SEARCH_RESPONSE)
titles = asyncio.run(plugin.search_titles("breaking"))
assert "Breaking Bad" in titles
assert "Inception" in titles
# Person-Eintrag darf nicht auftauchen
assert "Christopher Nolan" not in titles
def test_search_populates_cache(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: SEARCH_RESPONSE)
asyncio.run(plugin.search_titles("breaking"))
# URL-Cache
assert "Breaking Bad" in plugin._title_to_url
assert "/api/v1/titles/123" in plugin._title_to_url["Breaking Bad"]
# is_series-Cache
assert plugin._is_series["Breaking Bad"] is True
assert plugin._is_series["Inception"] is False
# Metadaten-Cache
assert plugin._title_meta["Breaking Bad"][0] == "Chemie-Lehrer wird Drogenboss."
assert plugin._title_meta["Inception"][1] == "https://cdn.example.com/inc.jpg"
def test_search_empty_query_returns_empty():
plugin = MoflixPlugin()
titles = asyncio.run(plugin.search_titles(""))
assert titles == []
# ---------------------------------------------------------------------------
# Tests: seasons_for
# ---------------------------------------------------------------------------
def test_seasons_for_series_after_search(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", make_json_router(
search=SEARCH_RESPONSE,
titles=TITLE_RESPONSE_SERIES,
))
asyncio.run(plugin.search_titles("breaking"))
seasons = plugin.seasons_for("Breaking Bad")
# Staffeln korrekt sortiert
assert seasons == ["Staffel 1", "Staffel 2"]
def test_seasons_for_film_returns_film(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: SEARCH_RESPONSE)
asyncio.run(plugin.search_titles("inception"))
seasons = plugin.seasons_for("Inception")
assert seasons == ["Film"]
def test_seasons_for_caches_season_api_ids(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", make_json_router(
search=SEARCH_RESPONSE,
titles=TITLE_RESPONSE_SERIES,
))
asyncio.run(plugin.search_titles("breaking"))
plugin.seasons_for("Breaking Bad")
assert plugin._season_api_ids[("Breaking Bad", 1)] == "1001"
assert plugin._season_api_ids[("Breaking Bad", 2)] == "1002"
def test_seasons_for_cache_miss_triggers_resolve(monkeypatch):
"""Bug-Regression: seasons_for() ohne Vorsuche (leere Instanz = Kodi-Neuaufruf).
_resolve_title() muss automatisch eine Suche starten und den Cache befüllen.
"""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", make_json_router(
search=SEARCH_RESPONSE,
titles=TITLE_RESPONSE_SERIES,
))
# KEIN asyncio.run(search_titles(...)) simuliert leere Instanz
seasons = plugin.seasons_for("Breaking Bad")
assert seasons == ["Staffel 1", "Staffel 2"]
def test_seasons_for_unknown_title_returns_empty(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: {"results": []})
seasons = plugin.seasons_for("Unbekannter Titel XYZ")
assert seasons == []
# ---------------------------------------------------------------------------
# Tests: episodes_for
# ---------------------------------------------------------------------------
def test_episodes_for_series(monkeypatch):
plugin = MoflixPlugin()
# "/titles/123" matcht nur die Titel-Detail-URL (id=123), nicht die Episoden-URL (id=1001)
monkeypatch.setattr(plugin, "_get_json", make_json_router(
**{"search": SEARCH_RESPONSE, "/titles/123": TITLE_RESPONSE_SERIES, "episodes": EPISODES_RESPONSE}
))
asyncio.run(plugin.search_titles("breaking"))
plugin.seasons_for("Breaking Bad")
episodes = plugin.episodes_for("Breaking Bad", "Staffel 1")
assert episodes == ["Episode 1 Pilot", "Episode 2 Cat's in the Bag"]
# Episode ohne primary_video (Nr. 3) darf nicht enthalten sein
assert len(episodes) == 2
def test_episodes_for_film_returns_title():
plugin = MoflixPlugin()
result = plugin.episodes_for("Inception", "Film")
assert result == ["Inception"]
def test_episodes_cache_hit(monkeypatch):
"""Zweiter episodes_for()-Aufruf darf keine neuen _get_json-Calls auslösen."""
call_count = {"n": 0}
def counting_router(url, headers=None):
call_count["n"] += 1
return make_json_router(
search=SEARCH_RESPONSE,
titles=TITLE_RESPONSE_SERIES,
episodes=EPISODES_RESPONSE,
)(url)
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", counting_router)
asyncio.run(plugin.search_titles("breaking"))
plugin.seasons_for("Breaking Bad")
plugin.episodes_for("Breaking Bad", "Staffel 1")
calls_after_first = call_count["n"]
# Zweiter Aufruf kein neuer HTTP-Call
plugin.episodes_for("Breaking Bad", "Staffel 1")
assert call_count["n"] == calls_after_first
# ---------------------------------------------------------------------------
# Tests: stream_link_for
# ---------------------------------------------------------------------------
def test_stream_link_for_episode_returns_vidara_src(monkeypatch):
"""stream_link_for() für Episode gibt vidara.to-URL aus episode.videos[] zurück."""
plugin = MoflixPlugin()
# Reihenfolge: spezifischere Keys zuerst
# "episodes/1" matcht die Detail-URL .../episodes/1?...
# "episodes" matcht die Listen-URL .../episodes?...
monkeypatch.setattr(plugin, "_get_json", make_json_router(
**{
"search": SEARCH_RESPONSE,
"/titles/123": TITLE_RESPONSE_SERIES,
"episodes/1": EPISODE_DETAIL_RESPONSE,
"episodes": EPISODES_RESPONSE,
}
))
asyncio.run(plugin.search_titles("breaking"))
plugin.seasons_for("Breaking Bad")
plugin.episodes_for("Breaking Bad", "Staffel 1")
link = plugin.stream_link_for("Breaking Bad", "Staffel 1", "Episode 1 Pilot")
# gupload.xyz wird übersprungen, vidara.to bevorzugt
assert link == "https://vidara.to/e/ep1vidara"
def test_stream_link_for_episode_cache_miss(monkeypatch):
"""stream_link_for() funktioniert auch ohne Vorsuche (leere Instanz)."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", make_json_router(
**{
"search": SEARCH_RESPONSE,
"/titles/123": TITLE_RESPONSE_SERIES,
"episodes/1": EPISODE_DETAIL_RESPONSE,
"episodes": EPISODES_RESPONSE,
}
))
link = plugin.stream_link_for("Breaking Bad", "Staffel 1", "Episode 1 Pilot")
assert link == "https://vidara.to/e/ep1vidara"
def test_stream_link_for_movie(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", make_json_router(
search=SEARCH_RESPONSE,
titles=TITLE_RESPONSE_MOVIE,
))
asyncio.run(plugin.search_titles("inception"))
link = plugin.stream_link_for("Inception", "Film", "Inception")
# gupload.xyz übersprungen, vidara.to bevorzugt
assert link == "https://vidara.to/e/inc7testXYZ"
def test_stream_link_for_movie_cache_miss(monkeypatch):
"""Film-Stream auch ohne Vorsuche (leere Instanz via _resolve_title)."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", make_json_router(
search=SEARCH_RESPONSE,
titles=TITLE_RESPONSE_MOVIE,
))
link = plugin.stream_link_for("Inception", "Film", "Inception")
assert link == "https://vidara.to/e/inc7testXYZ"
# ---------------------------------------------------------------------------
# Tests: _best_src_from_videos
# ---------------------------------------------------------------------------
def test_best_src_prefers_vidara_over_fallback():
plugin = MoflixPlugin()
videos = [
{"src": "https://moflix-stream.link/e/abc", "quality": "1080p"},
{"src": "https://vidara.to/e/xyz789", "quality": "1080p"},
]
assert plugin._best_src_from_videos(videos) == "https://vidara.to/e/xyz789"
def test_best_src_skips_gupload():
plugin = MoflixPlugin()
videos = [
{"src": "https://gupload.xyz/data/e/hash", "quality": "1080p"},
{"src": "https://moflix-stream.link/e/abc", "quality": "1080p"},
]
# gupload übersprungen, moflix-stream.link als Fallback
assert plugin._best_src_from_videos(videos) == "https://moflix-stream.link/e/abc"
def test_best_src_skips_youtube():
plugin = MoflixPlugin()
videos = [
{"src": "https://youtube.com/watch?v=xyz", "quality": None},
{"src": "https://vidara.to/e/real123", "quality": "1080p"},
]
assert plugin._best_src_from_videos(videos) == "https://vidara.to/e/real123"
def test_best_src_all_skipped_returns_none():
plugin = MoflixPlugin()
videos = [
{"src": "https://gupload.xyz/data/e/hash"},
{"src": "https://youtube.com/watch?v=xyz"},
]
assert plugin._best_src_from_videos(videos) is None
def test_best_src_empty_returns_none():
plugin = MoflixPlugin()
assert plugin._best_src_from_videos([]) is None
assert plugin._best_src_from_videos(None) is None # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# Tests: resolve_stream_link / _resolve_vidara
# ---------------------------------------------------------------------------
def test_resolve_stream_link_vidara_returns_hls(monkeypatch):
"""resolve_stream_link() ruft vidara.to-API auf und gibt streaming_url zurück."""
plugin = MoflixPlugin()
def mock_get_json(url, headers=None):
if "vidara.to" in url:
return VIDARA_STREAM_RESPONSE
return None
monkeypatch.setattr(plugin, "_get_json", mock_get_json)
result = plugin.resolve_stream_link("https://vidara.to/e/ep1vidara")
assert result == "https://cdn.example.com/hls/ep1/master.m3u8"
def test_resolve_stream_link_vidara_api_fails_returns_none(monkeypatch):
"""Wenn vidara-API None zurückgibt und ResolveURL nicht klappt → None."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: None)
result = plugin.resolve_stream_link("https://vidara.to/e/broken123")
# Weder vidara-API noch ResolveURL → None (kein unauflösbarer Link)
assert result is None
def test_resolve_stream_link_non_vidhide_tries_resolveurl(monkeypatch):
"""Für sonstige URLs wird ResolveURL aufgerufen; ohne Installation → None."""
plugin = MoflixPlugin()
result = plugin.resolve_stream_link("https://moflix-stream.link/e/somefilm")
# Ohne ResolveURL-Installation → None
assert result is None
# ---------------------------------------------------------------------------
# Tests: Channel-Browse (popular, genre, collection)
# ---------------------------------------------------------------------------
def test_popular_series_returns_titles(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: CHANNEL_RESPONSE)
titles = plugin.popular_series()
assert titles == ["Squid Game", "The Crown"]
# Cache muss befüllt sein
assert "Squid Game" in plugin._title_to_url
def test_channel_empty_response_returns_empty(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: None)
assert plugin.popular_series() == []
assert plugin.latest_titles() == []
def test_channel_malformed_response_returns_empty(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: {"channel": {}})
assert plugin.popular_series() == []
def test_titles_for_genre(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: CHANNEL_RESPONSE)
titles = plugin.titles_for_genre("Action")
assert "Squid Game" in titles
def test_titles_for_unknown_genre_returns_empty():
plugin = MoflixPlugin()
assert plugin.titles_for_genre("Unbekanntes Genre XYZ") == []
def test_titles_for_collection(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: CHANNEL_RESPONSE)
titles = plugin.titles_for_collection("James Bond Collection")
assert "Squid Game" in titles
# ---------------------------------------------------------------------------
# Tests: genres / collections / capabilities
# ---------------------------------------------------------------------------
def test_genres_returns_sorted_list():
plugin = MoflixPlugin()
genres = plugin.genres()
assert genres == sorted(GENRE_SLUGS.keys())
assert "Action" in genres
assert "Horror" in genres
def test_collections_returns_sorted_list():
plugin = MoflixPlugin()
colls = plugin.collections()
assert colls == sorted(COLLECTION_SLUGS.keys())
assert "James Bond Collection" in colls
def test_capabilities():
plugin = MoflixPlugin()
caps = plugin.capabilities()
assert "popular_series" in caps
assert "latest_titles" in caps
assert "genres" in caps
assert "collections" in caps
# ---------------------------------------------------------------------------
# Tests: metadata_for
# ---------------------------------------------------------------------------
def test_metadata_from_cache(monkeypatch):
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", lambda url, headers=None: SEARCH_RESPONSE)
asyncio.run(plugin.search_titles("breaking"))
# Metadaten-Abruf darf jetzt keinen neuen HTTP-Call auslösen
call_count = {"n": 0}
def no_call(url, headers=None):
call_count["n"] += 1
return None
monkeypatch.setattr(plugin, "_get_json", no_call)
info, art, _ = plugin.metadata_for("Breaking Bad")
assert info.get("plot") == "Chemie-Lehrer wird Drogenboss."
assert art.get("poster") == "https://cdn.example.com/bb.jpg"
assert call_count["n"] == 0 # kein HTTP-Call
def test_metadata_api_fallback(monkeypatch):
"""Metadaten werden via API geladen wenn nicht im Cache."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_json", make_json_router(
search=SEARCH_RESPONSE,
titles=TITLE_RESPONSE_SERIES,
))
asyncio.run(plugin.search_titles("breaking"))
# Cache leeren um API-Fallback zu erzwingen
plugin._title_meta.clear()
info, art, _ = plugin.metadata_for("Breaking Bad")
assert info.get("plot") == "Chemie-Lehrer wird Drogenboss."
assert "year" in info
assert info["year"] == "2008"
def test_metadata_unknown_title_returns_empty():
plugin = MoflixPlugin()
info, art, streams = plugin.metadata_for("Unbekannt")
assert info == {"title": "Unbekannt"}
assert art == {}
assert streams is None
# ---------------------------------------------------------------------------
# Tests: _unpack_packer
# ---------------------------------------------------------------------------
def test_unpack_packer_basic():
"""_unpack_packer() entpackt ein p.a.c.k.e.r.-Fragment korrekt."""
packed = (
"eval(function(p,a,c,k,e,d){return p}"
"('0 1={\"2\":\"https://cdn.example.com/hls/test/master.m3u8\"};',"
"36,3,'var|links|hls2'.split('|'),0,0))"
)
result = _unpack_packer(packed)
assert 'var links={"hls2":"https://cdn.example.com/hls/test/master.m3u8"}' in result
def test_unpack_packer_preserves_url():
"""URLs in String-Literalen werden durch den Unpacker nicht korrumpiert."""
packed = (
"eval(function(p,a,c,k,e,d){return p}"
"('0 1={\"2\":\"https://cdn.example.com/hls/test/master.m3u8\"};',"
"36,3,'var|links|hls2'.split('|'),0,0))"
)
result = _unpack_packer(packed)
assert "https://cdn.example.com/hls/test/master.m3u8" in result
def test_unpack_packer_no_match_returns_input():
"""Wenn kein p.a.c.k.e.r.-Muster gefunden wird, wird der Input unverändert zurückgegeben."""
raw = "var x = 1; console.log(x);"
assert _unpack_packer(raw) == raw
def test_unpack_packer_full_vidhide_fixture():
"""Entpackt die VIDHIDE_HTML-Fixture und findet hls2-URL."""
result = _unpack_packer(VIDHIDE_HTML)
assert '"hls2":"https://cdn.example.com/hls/test/master.m3u8"' in result
assert "jwplayer" in result
assert "links.hls2" in result
# ---------------------------------------------------------------------------
# Tests: _resolve_vidhide / resolve_stream_link (VidHide)
# ---------------------------------------------------------------------------
def test_resolve_vidhide_extracts_hls_url(monkeypatch):
"""_resolve_vidhide() gibt den hls2-Stream-Link mit Kodi-Header-Suffix zurück."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_html", lambda url, headers=None, fresh_session=False: VIDHIDE_HTML)
result = plugin._resolve_vidhide("https://moflix-stream.click/embed/kqocffe8ipcf")
assert result is not None
assert result.startswith("https://cdn.example.com/hls/test/master.m3u8|")
assert "Referer=" in result
assert "User-Agent=" in result
def test_resolve_vidhide_no_packer_returns_none(monkeypatch):
"""_resolve_vidhide() gibt None zurück wenn kein p.a.c.k.e.r. in der Seite."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_html", lambda url, headers=None, fresh_session=False: "<html>no packer here</html>")
result = plugin._resolve_vidhide("https://moflix-stream.click/embed/abc")
assert result is None
def test_resolve_vidhide_html_fetch_fails_returns_none(monkeypatch):
"""_resolve_vidhide() gibt None zurück wenn _get_html() fehlschlägt."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_html", lambda url, headers=None, fresh_session=False: None)
result = plugin._resolve_vidhide("https://moflix-stream.click/embed/abc")
assert result is None
def test_resolve_stream_link_vidhide_returns_hls(monkeypatch):
"""resolve_stream_link() ruft _resolve_vidhide() auf und gibt HLS-URL mit Header-Suffix zurück."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_html", lambda url, headers=None, fresh_session=False: VIDHIDE_HTML)
result = plugin.resolve_stream_link("https://moflix-stream.click/embed/kqocffe8ipcf")
assert result is not None
assert result.startswith("https://cdn.example.com/hls/test/master.m3u8|")
assert "Referer=" in result
assert "User-Agent=" in result
def test_resolve_stream_link_vidhide_fallback_on_failure(monkeypatch):
"""Wenn VidHide-Resolver fehlschlägt, wird None zurückgegeben (kein unauflösbarer Link)."""
plugin = MoflixPlugin()
monkeypatch.setattr(plugin, "_get_html", lambda url, headers=None, fresh_session=False: None)
result = plugin.resolve_stream_link("https://moflix-stream.click/embed/broken")
# Kein VidHide-Ergebnis → None (Kodi zeigt "Kein Stream"-Dialog)
assert result is None
# ---------------------------------------------------------------------------
# Tests: _best_src_from_videos moflix-stream.click nicht mehr übersprungen
# ---------------------------------------------------------------------------
def test_best_src_vidhide_not_skipped():
"""moflix-stream.click ist nicht mehr in _VIDEO_SKIP_DOMAINS."""
plugin = MoflixPlugin()
videos = [
{"src": "https://moflix-stream.click/embed/abc123", "quality": "1080p"},
]
result = plugin._best_src_from_videos(videos)
assert result == "https://moflix-stream.click/embed/abc123"
def test_best_src_vidara_preferred_over_vidhide():
"""vidara.to hat Vorrang vor moflix-stream.click."""
plugin = MoflixPlugin()
videos = [
{"src": "https://moflix-stream.click/embed/abc123", "quality": "1080p"},
{"src": "https://vidara.to/e/xyz789", "quality": "1080p"},
]
result = plugin._best_src_from_videos(videos)
assert result == "https://vidara.to/e/xyz789"
def test_stream_link_for_movie_vidhide_only(monkeypatch):
"""Film mit nur moflix-stream.click Mirror: stream_link_for() gibt VidHide-src zurück."""
plugin = MoflixPlugin()
plugin._title_to_url["The Bluff"] = "https://moflix-stream.xyz/api/v1/titles/789?load=videos"
plugin._is_series["The Bluff"] = False
def mock_get_json(_url, _headers=None):
return {
"title": {
"videos": [
{"quality": "1080p", "src": "https://moflix-stream.click/embed/kqocffe8ipcf", "name": "Mirror 1"},
],
},
}
monkeypatch.setattr(plugin, "_get_json", mock_get_json)
link = plugin.stream_link_for("The Bluff", "Film", "The Bluff")
assert link == "https://moflix-stream.click/embed/kqocffe8ipcf"