dev: bump to 0.1.71-dev – neue Plugins (Moflix, KKiste, HDFilme, Netzkino), SerienStream A-Z, VidHide-Fix

This commit is contained in:
2026-03-04 22:29:49 +01:00
parent ff30548811
commit 58da715723
7 changed files with 2460 additions and 3 deletions

711
tests/test_moflix_plugin.py Normal file
View File

@@ -0,0 +1,711 @@
"""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"