718 lines
27 KiB
Python
718 lines
27 KiB
Python
"""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: _hosters_from_videos
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_hosters_skips_gupload():
|
||
plugin = MoflixPlugin()
|
||
videos = [
|
||
{"src": "https://gupload.xyz/data/e/hash", "name": "GUpload"},
|
||
{"src": "https://moflix-stream.link/e/abc", "name": "Mirror-HDCloud"},
|
||
]
|
||
hosters = plugin._hosters_from_videos(videos)
|
||
assert "https://gupload.xyz/data/e/hash" not in hosters.values()
|
||
assert "https://moflix-stream.link/e/abc" in hosters.values()
|
||
|
||
|
||
def test_hosters_skips_youtube():
|
||
plugin = MoflixPlugin()
|
||
videos = [
|
||
{"src": "https://youtube.com/watch?v=xyz", "name": "YouTube"},
|
||
{"src": "https://vidara.to/e/real123", "name": "Vidara"},
|
||
]
|
||
hosters = plugin._hosters_from_videos(videos)
|
||
assert len(hosters) == 1
|
||
assert "https://vidara.to/e/real123" in hosters.values()
|
||
|
||
|
||
def test_hosters_all_skipped_returns_empty():
|
||
plugin = MoflixPlugin()
|
||
videos = [
|
||
{"src": "https://gupload.xyz/data/e/hash"},
|
||
{"src": "https://youtube.com/watch?v=xyz"},
|
||
]
|
||
assert plugin._hosters_from_videos(videos) == {}
|
||
|
||
|
||
def test_hosters_empty_returns_empty():
|
||
plugin = MoflixPlugin()
|
||
assert plugin._hosters_from_videos([]) == {}
|
||
|
||
|
||
def test_available_hosters_for_returns_names():
|
||
plugin = MoflixPlugin()
|
||
videos = [
|
||
{"src": "https://vidara.to/e/xyz", "name": "Vidara-720"},
|
||
{"src": "https://moflix-stream.click/e/abc", "name": "Mirror-HDCloud"},
|
||
]
|
||
# Mock _videos_for um direkt zu testen
|
||
plugin._videos_for = lambda *a, **kw: videos # type: ignore[assignment]
|
||
names = plugin.available_hosters_for("Test", "Film", "Test")
|
||
assert len(names) == 2
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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.new_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 "new_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_hosters_vidhide_not_skipped():
|
||
"""moflix-stream.click ist nicht mehr in _VIDEO_SKIP_DOMAINS."""
|
||
plugin = MoflixPlugin()
|
||
videos = [
|
||
{"src": "https://moflix-stream.click/embed/abc123", "name": "Mirror-VidHide"},
|
||
]
|
||
hosters = plugin._hosters_from_videos(videos)
|
||
assert "https://moflix-stream.click/embed/abc123" in hosters.values()
|
||
|
||
|
||
def test_hosters_vidara_present():
|
||
"""vidara.to wird korrekt als Hoster erkannt."""
|
||
plugin = MoflixPlugin()
|
||
videos = [
|
||
{"src": "https://moflix-stream.click/embed/abc123", "name": "Mirror-VidHide"},
|
||
{"src": "https://vidara.to/e/xyz789", "name": "Vidara-720"},
|
||
]
|
||
hosters = plugin._hosters_from_videos(videos)
|
||
assert len(hosters) == 2
|
||
assert "https://vidara.to/e/xyz789" in hosters.values()
|
||
|
||
|
||
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"
|