diff --git a/.gitignore b/.gitignore index 69a1fc4..2077727 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,5 @@ config/ mediafusion.local* .lock kodi/build/ -kodi/*.zip \ No newline at end of file +kodi/*.zip +!kodi/**/lib/ \ No newline at end of file diff --git a/kodi/plugin.video.mediafusion/lib/__init__.py b/kodi/plugin.video.mediafusion/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kodi/plugin.video.mediafusion/lib/router.py b/kodi/plugin.video.mediafusion/lib/router.py new file mode 100644 index 0000000..817a3ff --- /dev/null +++ b/kodi/plugin.video.mediafusion/lib/router.py @@ -0,0 +1,301 @@ +import sys +from urllib import parse + +import xbmc +import xbmcgui +import xbmcplugin + +from .utils import ( + fetch_data, + build_url, + ADDON_HANDLE, + BASE_URL, + SECRET_STR, + add_context_menu_items, + log, +) + + +def list_categories(): + manifest_data = fetch_data(f"{BASE_URL}/{SECRET_STR}/manifest.json") + if not manifest_data: + return + + catalogs = manifest_data.get("catalogs", []) + if not catalogs: + log("No catalogs enabled. Make sure to configure correctly", xbmc.LOGERROR) + xbmcgui.Dialog().notification( + "MediaFusion", + "No catalogs enabled. Make sure to configure correctly", + xbmcgui.NOTIFICATION_ERROR, + ) + return + + for catalog in catalogs: + if any(extra["name"] == "search" for extra in catalog.get("extra", [])): + continue + + url = build_url( + "list_catalog", catalog_type=catalog["type"], catalog_id=catalog["id"] + ) + li = xbmcgui.ListItem(label=catalog["name"]) + xbmcplugin.addDirectoryItem( + handle=ADDON_HANDLE, url=url, listitem=li, isFolder=True + ) + xbmcplugin.endOfDirectory(ADDON_HANDLE) + + +def list_catalogs(params): + log(f"Loading {params['catalog_type']} videos...", xbmc.LOGINFO) + skip = int(params.get("skip", 0)) + url = f"{BASE_URL}/{SECRET_STR}/catalog/{params['catalog_type']}/{params['catalog_id']}/skip={skip}.json" + response = fetch_data(url) + if not response: + return + + videos = response.get("metas", []) + if not videos: + xbmcgui.Dialog().notification( + "MediaFusion", "No videos available", xbmcgui.NOTIFICATION_ERROR + ) + return + + content_type = "movies" if params["catalog_type"] == "movie" else "tvshows" + xbmcplugin.setContent(ADDON_HANDLE, content_type) + + for video in videos: + if params["catalog_type"] == "series": + url = build_url( + "list_seasons", + catalog_type=params["catalog_type"], + video_id=video["id"], + ) + else: + url = build_url( + "get_streams", video_id=video["id"], catalog_type=params["catalog_type"] + ) + li = xbmcgui.ListItem(label=video["name"]) + tags = li.getVideoInfoTag() + tags.setUniqueID( + video["id"], type="imdb" if video["id"].startswith("tt") else "mf" + ) + tags.setTitle(video["name"]) + tags.setPlot(video.get("description", "")) + tags.setRating(float(video.get("imdbRating", 0))) + if video.get("releaseInfo"): + tags.setYear(int(video.get("releaseInfo").strip("--"))) + tags.setGenres(video.get("genres", [])) + + li.setArt( + { + "thumb": video.get("poster", ""), + "poster": video.get("poster", ""), + "fanart": video.get("poster", ""), + "icon": video.get("poster", ""), + "banner": video.get("background", ""), + "landscape": video.get("background", ""), + } + ) + + add_context_menu_items( + li, video["id"], params["catalog_type"], params["catalog_id"] + ) + xbmcplugin.addDirectoryItem( + handle=ADDON_HANDLE, url=url, listitem=li, isFolder=True + ) + + next_url = build_url( + "list_catalog", + catalog_type=params["catalog_type"], + catalog_id=params["catalog_id"], + skip=skip + len(videos), + ) + li = xbmcgui.ListItem(label="Next Page") + xbmcplugin.addDirectoryItem( + handle=ADDON_HANDLE, url=next_url, listitem=li, isFolder=True + ) + + xbmcplugin.endOfDirectory(ADDON_HANDLE) + + +def list_seasons(params): + url = f"{BASE_URL}/{SECRET_STR}/meta/{params['catalog_type']}/{params['video_id']}.json" + response = fetch_data(url) + if not response: + return + + meta_data = response.get("meta", {}) + videos = meta_data.get("videos", []) + if not videos: + xbmcgui.Dialog().notification( + "MediaFusion", "No seasons available", xbmcgui.NOTIFICATION_ERROR + ) + return + + available_seasons = set(video["season"] for video in videos) + + for season in available_seasons: + url = build_url( + "list_episodes", + catalog_type=params["catalog_type"], + video_id=params["video_id"], + season=season, + ) + li = xbmcgui.ListItem(label=f"Season {season}") + tags = li.getVideoInfoTag() + tags.setUniqueID( + meta_data["id"], type="imdb" if meta_data["id"].startswith("tt") else "mf" + ) + tags.setTitle(meta_data["name"]) + tags.setPlot(meta_data.get("description", "")) + tags.setRating(float(meta_data.get("imdbRating", 0))) + if meta_data.get("releaseInfo"): + tags.setYear(int(meta_data.get("releaseInfo").strip("--"))) + tags.setGenres(meta_data.get("genres", [])) + tags.setTvShowTitle(meta_data["name"]) + tags.setSeason(season) + + li.setArt( + { + "thumb": meta_data.get("poster", ""), + "poster": meta_data.get("poster", ""), + "fanart": meta_data.get("poster", ""), + "icon": meta_data.get("poster", ""), + "banner": meta_data.get("background", ""), + "landscape": meta_data.get("background", ""), + } + ) + + add_context_menu_items(li, params["video_id"], params["catalog_type"]) + xbmcplugin.addDirectoryItem( + handle=ADDON_HANDLE, url=url, listitem=li, isFolder=True + ) + + xbmcplugin.endOfDirectory(ADDON_HANDLE) + + +def list_episodes(params): + url = f"{BASE_URL}/{SECRET_STR}/meta/{params['catalog_type']}/{params['video_id']}.json" + response = fetch_data(url) + if not response: + return + + meta_data = response.get("meta", {}) + videos = meta_data.get("videos", []) + if not videos: + xbmcgui.Dialog().notification( + "MediaFusion", "No episodes available", xbmcgui.NOTIFICATION_ERROR + ) + return + + for video in videos: + if video["season"] != int(params["season"]): + continue + + url = build_url( + "get_streams", + video_id=f"{params['video_id']}:{video['season']}:{video['episode']}", + catalog_type=params["catalog_type"], + ) + li = xbmcgui.ListItem(label=video["title"]) + tags = li.getVideoInfoTag() + tags.setUniqueID( + meta_data["id"], type="imdb" if meta_data["id"].startswith("tt") else "mf" + ) + tags.setTitle(video["title"]) + tags.setPlot(meta_data.get("description", "")) + tags.setRating(float(meta_data.get("imdbRating", 0))) + if meta_data.get("releaseInfo"): + tags.setYear(int(meta_data.get("releaseInfo").strip("--"))) + tags.setGenres(meta_data.get("genres", [])) + tags.setTvShowTitle(video["title"]) + tags.setSeason(int(video["season"])) + tags.setEpisode(int(video["episode"])) + + li.setArt( + { + "thumb": meta_data.get("poster", ""), + "poster": meta_data.get("poster", ""), + "fanart": meta_data.get("poster", ""), + "icon": meta_data.get("poster", ""), + "banner": meta_data.get("background", ""), + "landscape": meta_data.get("background", ""), + } + ) + + add_context_menu_items(li, params["video_id"], params["catalog_type"]) + xbmcplugin.addDirectoryItem( + handle=ADDON_HANDLE, url=url, listitem=li, isFolder=True + ) + + xbmcplugin.endOfDirectory(ADDON_HANDLE) + + +def get_streams(params): + url = f"{BASE_URL}/{SECRET_STR}/stream/{params['catalog_type']}/{params['video_id']}.json" + response = fetch_data(url) + if not response: + return + + streams = response.get("streams", []) + if not streams: + xbmcgui.Dialog().notification( + "MediaFusion", "No streams available", xbmcgui.NOTIFICATION_ERROR + ) + return + + for stream in streams: + li = xbmcgui.ListItem(label=stream["name"], offscreen=True) + tags = li.getVideoInfoTag() + tags.setTitle(stream["name"]) + tags.setPlot(stream.get("description", "")) + + li.setProperty("IsPlayable", "true") + + xbmcplugin.addDirectoryItem( + handle=ADDON_HANDLE, + url=build_url( + "play_video", + video_url=stream["url"], + headers=parse.urlencode( + stream.get("behaviorHints", {}) + .get("proxyHeaders", {}) + .get("request", {}) + ), + ), + listitem=li, + isFolder=False, + totalItems=len(streams), + ) + xbmcplugin.endOfDirectory(ADDON_HANDLE) + + +def play_video(params): + video_url = params["video_url"] + + li = xbmcgui.ListItem(path=video_url) + if "headers" in params: + li.setProperty("inputstream.adaptive.stream_headers", params["headers"]) + xbmcplugin.setResolvedUrl(ADDON_HANDLE, True, li) + + +def addon_router(): + param_string = sys.argv[2][1:] + if param_string: + params = dict(parse.parse_qsl(param_string)) + action = params.get("action") + + if action == "list_catalog": + list_catalogs(params) + elif action == "get_streams": + get_streams(params) + elif action == "list_seasons": + list_seasons(params) + elif action == "list_episodes": + list_episodes(params) + elif action == "play_video": + play_video(params) + else: + list_categories() + else: + list_categories() diff --git a/kodi/plugin.video.mediafusion/lib/utils.py b/kodi/plugin.video.mediafusion/lib/utils.py new file mode 100644 index 0000000..7dbbd0e --- /dev/null +++ b/kodi/plugin.video.mediafusion/lib/utils.py @@ -0,0 +1,94 @@ +import os +import sys +from urllib import parse + +import requests +import xbmc +import xbmcaddon +import xbmcgui + +ADDON_HANDLE = int(sys.argv[1]) +ADDON = xbmcaddon.Addon() +ADDON_PATH = sys.argv[0] +MANIFEST_URL = ADDON.getSetting("manifest_url") +ADDON_ID = ADDON.getAddonInfo("id") + +if not MANIFEST_URL: + xbmcgui.Dialog().notification( + "MediaFusion", + "Manifest URL is not set. Please configure the addon", + xbmcgui.NOTIFICATION_INFO, + ) + xbmc.executebuiltin(f"Addon.OpenSettings({ADDON_ID})") + sys.exit(0) + + +parsed_url = parse.urlparse(MANIFEST_URL) +BASE_URL = f"{parsed_url.scheme}://{parsed_url.netloc}" +try: + SECRET_STR = parsed_url.path.split("/")[1] +except IndexError: + xbmcgui.Dialog().notification( + "MediaFusion", + "Invalid manifest URL. Please configure the addon", + xbmcgui.NOTIFICATION_ERROR, + ) + xbmc.executebuiltin(f"Addon.OpenSettings({ADDON_ID})") + sys.exit(0) + + +def fetch_data(url): + try: + response = requests.get(url) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + if e.response.status_code == 401: + xbmc.log("Unauthorized request", xbmc.LOGERROR) + xbmcgui.Dialog().notification( + "MediaFusion", "Unauthorized request", xbmcgui.NOTIFICATION_ERROR + ) + elif e.response.status_code == 429: + xbmc.log("Too many requests, Try again in few seconds", xbmc.LOGERROR) + xbmcgui.Dialog().notification( + "MediaFusion", + "Too many requests, Try again in few seconds", + xbmcgui.NOTIFICATION_ERROR, + ) + else: + xbmc.log(f"Request failed: {e}", xbmc.LOGERROR) + xbmcgui.Dialog().notification( + "MediaFusion", "Request failed", xbmcgui.NOTIFICATION_ERROR + ) + return None + + +def build_url(action, **params): + query = parse.urlencode(params) + return f"{ADDON_PATH}?action={action}&{query}" + + +def add_context_menu_items(li, video_id, catalog_type, catalog_id=None): + context_menu = [ + ( + "Mark as Watched", + f"RunPlugin({build_url('mark_watched', video_id=video_id)})", + ), + ( + "Mark as Unwatched", + f"RunPlugin({build_url('mark_unwatched', video_id=video_id)})", + ), + ] + if catalog_id: + context_menu.insert( + 0, + ( + "Refresh", + f"Container.Refresh({build_url('list_catalog', catalog_type=catalog_type, catalog_id=catalog_id)})", + ), + ) + li.addContextMenuItems(context_menu) + + +def log(message, level=xbmc.LOGINFO): + xbmc.log(f"[MediaFusion] {message}", level)