diff --git a/CHANGELOG.md b/CHANGELOG.md index a71f076..a9c471b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 2.x +## [2.1.4](https://github.com/f-lawe/plugin.video.orange.fr/releases/tag/v2.1.4) - 2024-08-08 + +## Changed +- Better UI items management + +### Fixed +- InputStream Helper is now called properly ([#50](https://github.com/f-lawe/plugin.video.orange.fr/issues/50)) +- Avoid plugin to be runned twice on catchup TV videos ([#55](https://github.com/f-lawe/plugin.video.orange.fr/issues/55)) + ## [2.1.3](https://github.com/f-lawe/plugin.video.orange.fr/releases/tag/v2.1.3) - 2024-07-21 ### Fixed diff --git a/addon.xml b/addon.xml index 33534a6..b3dcaad 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index de3256e..3d2cf75 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1,13 +1,13 @@ # Kodi Media Center language file # Addon Name: Orange TV France # Addon id: plugin.video.orange.fr -# Addon Provider: BreizhReloaded +# Addon Provider: Flawe msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Last-Translator: BreizhReloaded\n" +"Last-Translator: Flawe\n" "Language-Team: English\n" "Language: en\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" @@ -23,17 +23,25 @@ msgid "Install IPTV Manager…" msgstr "" msgctxt "#30102" -msgid "Help 30104" -msgstr " +msgid "Help 30102" +msgstr "" msgctxt "#30103" -msgid "Go to IPTV Manager settings…" +msgid "Enable IPTV Manager integration" msgstr "" msgctxt "#30104" msgid "Help 30104" msgstr "" +msgctxt "#30105" +msgid "Go to IPTV Manager settings…" +msgstr "" + +msgctxt "#30106" +msgid "Help 30106" +msgstr "" + # Provider settings (from 30200 to 30299) msgctxt "#30200" @@ -103,7 +111,3 @@ msgstr "" msgctxt "#30901" msgid "InputStream cannot be loaded." msgstr "" - -msgctxt "#30902" -msgid "Orange TV France must be used from the Kodi TV section." -msgstr "" diff --git a/resources/language/resource.language.fr_fr/strings.po b/resources/language/resource.language.fr_fr/strings.po index 67bcd0e..d64a861 100644 --- a/resources/language/resource.language.fr_fr/strings.po +++ b/resources/language/resource.language.fr_fr/strings.po @@ -1,13 +1,13 @@ # Kodi Media Center language file # Addon Name: Orange TV France # Addon id: plugin.video.orange.fr -# Addon Provider: BreizhReloaded +# Addon Provider: Flawe msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Last-Translator: BreizhReloaded\n" +"Last-Translator: Flawe\n" "Language-Team: Français\n" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" @@ -23,17 +23,25 @@ msgid "Install IPTV Manager…" msgstr "Installer IPTV Manager…" msgctxt "#30102" -msgid "Help 30104" +msgid "Help 30102" msgstr "" msgctxt "#30103" -msgid "Go to IPTV Manager settings…" -msgstr "Ouvrir les paramètres de IPTV Manager…" +msgid "Enable IPTV Manager integration" +msgstr "Activer l'intégration avec IPTV Manager" msgctxt "#30104" msgid "Help 30104" msgstr "" +msgctxt "#30105" +msgid "Go to IPTV Manager settings…" +msgstr "Ouvrir les paramètres de IPTV Manager…" + +msgctxt "#30106" +msgid "Help 30106" +msgstr "" + # Provider settings (from 30200 to 30299) msgctxt "#30200" @@ -63,8 +71,8 @@ msgid "Proxy" msgstr "Proxy" msgctxt "#30301" -msgid "Activer" -msgstr "" +msgid "Enable" +msgstr "Activer" msgctxt "#30302" msgid "Help 30302" @@ -103,7 +111,3 @@ msgstr "Cette chaîne ne fait pas partie de votre abonnement." msgctxt "#30901" msgid "InputStream cannot be loaded." msgstr "InputStream n'a pas pu être chargé." - -msgctxt "#30902" -msgid "Orange TV France must be used from the Kodi TV section." -msgstr "Orange TV France doit être utilisé depuis la section TV de Kodi." diff --git a/resources/lib/managers/catchup_manager.py b/resources/lib/managers/catchup_manager.py index 86cf859..c786540 100644 --- a/resources/lib/managers/catchup_manager.py +++ b/resources/lib/managers/catchup_manager.py @@ -1,12 +1,10 @@ """Catchup TV Manager.""" -import xbmc import xbmcplugin from lib.providers import get_provider from lib.router import router -from lib.utils.gui import create_directory_items -from lib.utils.kodi import build_addon_url +from lib.utils.gui import create_list_item class CatchupManager: @@ -19,36 +17,35 @@ def __init__(self): def get_channels(self) -> list: """Return channels available for catchup TV.""" channels = self.provider.get_catchup_channels() - directory_items = create_directory_items(channels) - succeeded = xbmcplugin.addDirectoryItems(router.handle, directory_items, len(directory_items)) - xbmcplugin.endOfDirectory(router.handle, succeeded) + for channel in channels: + xbmcplugin.addDirectoryItem(router.handle, channel["path"], create_list_item(channel, True), True) + + xbmcplugin.endOfDirectory(router.handle) def get_categories(self, catchup_channel_id: str) -> list: """Return content categories for the required channel.""" categories = self.provider.get_catchup_categories(catchup_channel_id) - directory_items = create_directory_items(categories) - succeeded = xbmcplugin.addDirectoryItems(router.handle, directory_items, len(directory_items)) - xbmcplugin.endOfDirectory(router.handle, succeeded) + for category in categories: + xbmcplugin.addDirectoryItem(router.handle, category["path"], create_list_item(category, True), True) + + xbmcplugin.endOfDirectory(router.handle) def get_articles(self, catchup_channel_id: str, category_id: str) -> list: """Return content (TV show, movie, etc) for the required channel and category.""" articles = self.provider.get_catchup_articles(catchup_channel_id, category_id) - directory_items = create_directory_items(articles) - succeeded = xbmcplugin.addDirectoryItems(router.handle, directory_items, len(directory_items)) - xbmcplugin.endOfDirectory(router.handle, succeeded) + for article in articles: + xbmcplugin.addDirectoryItem(router.handle, article["path"], create_list_item(article, True), True) + + xbmcplugin.endOfDirectory(router.handle) def get_videos(self, catchup_channel_id: str, article_id: str) -> list: """Return the video list for the required show.""" videos = self.provider.get_catchup_videos(catchup_channel_id, article_id) - directory_items = create_directory_items(videos) - succeeded = xbmcplugin.addDirectoryItems(router.handle, directory_items, len(directory_items)) - xbmcplugin.endOfDirectory(router.handle, succeeded) + for video in videos: + xbmcplugin.addDirectoryItem(router.handle, video["path"], create_list_item(video)) - def play_video(self, video_id: str): - """Play catchup video.""" - player = xbmc.Player() - player.play(build_addon_url(f"/catchup-streams/{video_id}")) + xbmcplugin.endOfDirectory(router.handle) diff --git a/resources/lib/managers/stream_manager.py b/resources/lib/managers/stream_manager.py index f753c6f..0c1c919 100644 --- a/resources/lib/managers/stream_manager.py +++ b/resources/lib/managers/stream_manager.py @@ -1,12 +1,11 @@ """Video stream manager.""" import inputstreamhelper -import xbmcgui import xbmcplugin from lib.providers import get_provider from lib.router import router -from lib.utils.gui import create_video_item +from lib.utils.gui import create_play_item from lib.utils.kodi import localize, ok_dialog @@ -17,32 +16,29 @@ def __init__(self): """Initialize Stream Manager object.""" self.provider = get_provider() - def load_live_stream(self, stream_id: str) -> xbmcgui.ListItem: + def load_live_stream(self, stream_id: str) -> None: """Load live TV stream.""" stream_info = self.provider.get_live_stream_info(stream_id) - if not stream_info: - ok_dialog(localize(30900)) - return - - is_helper = inputstreamhelper.Helper(stream_info["manifest_type"], drm=stream_info["drm"]) - if not is_helper.check_inputstream(): - ok_dialog(localize(30901)) - return + self._load_stream(stream_info) - list_item = create_video_item(stream_info) - xbmcplugin.setResolvedUrl(router.handle, True, list_item) - - def load_chatchup_stream(self, stream_id: str) -> xbmcgui.ListItem: + def load_chatchup_stream(self, stream_id: str) -> None: """Load catchup TV stream.""" stream_info = self.provider.get_catchup_stream_info(stream_id) - if not stream_info: + self._load_stream(stream_info) + + def _load_stream(self, stream_info: dict = None) -> None: + """Load stream.""" + if stream_info is None: ok_dialog(localize(30900)) + xbmcplugin.setResolvedUrl(router.handle, False) return - is_helper = inputstreamhelper.Helper(stream_info["manifest_type"], drm=stream_info["drm"]) - if not is_helper.check_inputstream(): - ok_dialog(localize(30901)) + is_helper = inputstreamhelper.Helper(stream_info["manifest_type"], drm=stream_info["license_type"]) + + if is_helper.check_inputstream(): + play_item = create_play_item(stream_info, is_helper.inputstream_addon) + xbmcplugin.setResolvedUrl(router.handle, True, play_item) return - list_item = create_video_item(stream_info) - xbmcplugin.setResolvedUrl(router.handle, True, list_item) + ok_dialog(localize(30901)) + xbmcplugin.setResolvedUrl(router.handle, False) diff --git a/resources/lib/providers/abstract_orange_provider.py b/resources/lib/providers/abstract_orange_provider.py index 9972346..3e27d49 100644 --- a/resources/lib/providers/abstract_orange_provider.py +++ b/resources/lib/providers/abstract_orange_provider.py @@ -13,7 +13,7 @@ from lib.providers.abstract_provider import AbstractProvider from lib.utils.kodi import build_addon_url, get_drm, get_global_setting, log -from lib.utils.request import build_request, get_random_ua +from lib.utils.request import build_request, get_random_ua, open_request _PROGRAMS_ENDPOINT = "https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?period={period}&epgIds=all&mco={mco}" _CATCHUP_CHANNELS_ENDPOINT = "https://rp-ott-mediation-tv.woopic.com/api-gw/catchup/v4/applications/PC/channels" @@ -43,9 +43,7 @@ def get_catchup_stream_info(self, stream_id: str) -> dict: def get_streams(self) -> list: """Load stream data from Orange and convert it to JSON-STREAMS format.""" req = build_request(_CHANNELS_ENDPOINT) - - with urlopen(req) as res: - channels = list(json.loads(res.read())["channels"]) + channels = dict(open_request(req, {"channels": {}}))["channels"] log(f"{len(channels)} channels found", xbmc.LOGINFO) channels.sort(key=lambda channel: channel["displayOrder"]) @@ -121,15 +119,12 @@ def get_epg(self) -> dict: def get_catchup_channels(self) -> list: """Load available catchup channels.""" req = build_request(_CATCHUP_CHANNELS_ENDPOINT) - - with urlopen(req) as res: - channels = list(json.loads(res.read())) + channels = open_request(req, []) log(f"{len(channels)} catchup channels found", xbmc.LOGINFO) return [ { - "is_folder": True, "label": str(channel["name"]).upper(), "path": build_addon_url(f"/channels/{channel['id']}/categories"), "art": {"thumb": channel["logos"]["ref_millenials_partner_white_logo"]}, @@ -140,13 +135,10 @@ def get_catchup_channels(self) -> list: def get_catchup_categories(self, catchup_channel_id: str) -> list: """Return a list of catchup categories for the specified channel id.""" req = build_request(_CATCHUP_CHANNELS_ENDPOINT + "/" + catchup_channel_id) - - with urlopen(req) as res: - categories = list(json.loads(res.read())["categories"]) + categories = dict(open_request(req, {"categories": {}}))["categories"] return [ { - "is_folder": True, "label": category["name"][0].upper() + category["name"][1:], "path": build_addon_url(f"/channels/{catchup_channel_id}/categories/{category['id']}/articles"), } @@ -155,18 +147,16 @@ def get_catchup_categories(self, catchup_channel_id: str) -> list: def get_catchup_articles(self, catchup_channel_id: str, category_id: str) -> list: """Return a list of catchup groups for the specified channel id and category id.""" - req = build_request( - _CATCHUP_ARTICLES_ENDPOINT.format(catchup_channel_id=catchup_channel_id, category_id=category_id) - ) + url = _CATCHUP_ARTICLES_ENDPOINT.format(catchup_channel_id=catchup_channel_id, category_id=category_id) + req = build_request(url) - with urlopen(req) as res: - articles = list(json.loads(res.read())["articles"]) + articles = dict(open_request(req, {"articles": {}}))["articles"] return [ { - "is_folder": True, "label": article["title"], "path": build_addon_url(f"/channels/{catchup_channel_id}/articles/{article['id']}/videos"), + "art": {"poster": article["covers"]["ref_16_9"]}, } for article in articles ] @@ -174,16 +164,20 @@ def get_catchup_articles(self, catchup_channel_id: str, category_id: str) -> lis def get_catchup_videos(self, catchup_channel_id: str, article_id: str) -> list: """Return a list of catchup videos for the specified channel id and article id.""" req = build_request(_CATCHUP_VIDEOS_ENDPOINT.format(group_id=article_id)) - - with urlopen(req) as res: - videos = list(json.loads(res.read())["videos"]) + videos = dict(open_request(req, {"videos": {}}))["videos"] return [ { - "is_folder": False, "label": video["title"], - "path": build_addon_url(f"/videos/{video['id']}"), - "art": {"thumb": video["covers"]["ref_4_3"]}, + "path": build_addon_url(f"/catchup-streams/{video['id']}"), + "art": {"poster": video["covers"]["ref_16_9"]}, + "info": { + "duration": int(video["duration"]) * 60, + "genres": video["genres"], + "plot": video["longSummary"], + "premiered": datetime.fromtimestamp(int(video["broadcastDate"]) / 1000).strftime("%Y-%m-%d"), + "year": int(video["productionDate"]), + }, } for video in videos ] @@ -197,7 +191,7 @@ def _get_stream_info(self, stream_type: str, version: str, item_type: str, strea item_type=item_type, stream_id=stream_id, ) - req, tv_token = self._build_request(url, auth_url=auth_url) + req, tv_token = self._build_auth_request(url, auth_url=auth_url) try: with urlopen(req) as res: @@ -232,7 +226,6 @@ def _get_stream_info(self, stream_type: str, version: str, item_type: str, strea "path": stream_info["url"], "mime_type": "application/xml+dash", "manifest_type": "mpd", - "drm": drm.name.lower(), "license_type": drm.value, "license_key": f"{license_server_url}|{headers}|{post_data}|{response}", } @@ -240,7 +233,7 @@ def _get_stream_info(self, stream_type: str, version: str, item_type: str, strea log(stream_info, xbmc.LOGDEBUG) return stream_info - def _build_request(self, url: str, additional_headers: dict = None, auth_url: str = None) -> (Request, str): + def _build_auth_request(self, url: str, additional_headers: dict = None, auth_url: str = None) -> (Request, str): """Build HTTP request.""" tv_token = None diff --git a/resources/lib/router.py b/resources/lib/router.py index b713830..7216e98 100644 --- a/resources/lib/router.py +++ b/resources/lib/router.py @@ -10,5 +10,5 @@ def init_router(): """Init addon router.""" + log("Initializing addon router", xbmc.LOGDEBUG) router.run() - log("Addon router initialized", xbmc.LOGDEBUG) diff --git a/resources/lib/routes.py b/resources/lib/routes.py index e74f32b..a24830a 100644 --- a/resources/lib/routes.py +++ b/resources/lib/routes.py @@ -35,13 +35,6 @@ def channel_article_videos(catchup_channel_id: str, article_id: str): CatchupManager().get_videos(catchup_channel_id, article_id) -@router.route("/videos/") -def video(video_id: str): - """Return catchup video listitem.""" - log(f"Loading catchup video {video_id}", xbmc.LOGINFO) - CatchupManager().play_video(video_id) - - @router.route("/live-streams/") def live_stream(stream_id: str): """Load live stream for the required channel id.""" diff --git a/resources/lib/utils/gui.py b/resources/lib/utils/gui.py index 17a7825..1f6838d 100644 --- a/resources/lib/utils/gui.py +++ b/resources/lib/utils/gui.py @@ -3,33 +3,47 @@ from xbmcgui import ListItem -def create_directory_items(data: list) -> list: - """Create a list of directory items from data.""" - items = [] +def create_list_item(item_data: dict, is_folder: bool = False) -> ListItem: + """Create a list item from data.""" + list_item = ListItem(label=item_data.get("label"), path=item_data.get("path")) + + if "art" in item_data: + item_art_data: dict = item_data.get("art", {}) + list_item.setArt( + { + "poster": item_art_data.get("poster"), + "thumb": item_art_data.get("thumb"), + } + ) + + if not is_folder: + list_item.setProperties( + { + "IsPlayable": "true", + } + ) + + if "info" in item_data: + item_info_data: dict = item_data.get("info", {}) + video_info_tag = list_item.getVideoInfoTag() + video_info_tag.setDuration(item_info_data.get("duration")) + video_info_tag.setGenres(item_info_data.get("genres")) + video_info_tag.setPlot(item_info_data.get("plot")) + video_info_tag.setYear(item_info_data.get("year")) - for d in data: - list_item = ListItem(label=d["label"], path=d["path"]) - - # list_item.setLabel2("LABEL2") - # list_item.setInfo("INFO", {"INFO1": "INFO1", "INFO2": "INFO2"}) - - if "art" in d and "thumb" in d["art"]: - list_item.setArt({"thumb": d["art"]["thumb"]}) - - items.append((d["path"], list_item, bool(d["is_folder"]))) + return list_item - return items +def create_play_item(stream_info: dict, inputstream_addon: str) -> ListItem: + """Create a play item from stream data.""" + play_item = ListItem(path=stream_info["path"]) + play_item.setContentLookup(False) + play_item.setMimeType(stream_info["mime_type"]) -def create_video_item(stream_info: dict) -> ListItem: - """Create a video item from stream data.""" - list_item = ListItem(path=stream_info["path"]) - list_item.setMimeType(stream_info["mime_type"]) - list_item.setContentLookup(False) - list_item.setProperty("inputstream", "inputstream.adaptive") - list_item.setProperty("inputstream.adaptive.play_timeshift_buffer", "true") - list_item.setProperty("inputstream.adaptive.manifest_config", '{"timeshift_bufferlimit":14400}') - list_item.setProperty("inputstream.adaptive.license_type", stream_info["license_type"]) - list_item.setProperty("inputstream.adaptive.license_key", stream_info["license_key"]) + play_item.setProperty("inputstream", inputstream_addon) + # play_item.setProperty("inputstream.adaptive.play_timeshift_buffer", "true") + # play_item.setProperty("inputstream.adaptive.manifest_config", '{"timeshift_bufferlimit":14400}') + play_item.setProperty("inputstream.adaptive.license_type", stream_info["license_type"]) + play_item.setProperty("inputstream.adaptive.license_key", stream_info["license_key"]) - return list_item + return play_item diff --git a/resources/lib/utils/request.py b/resources/lib/utils/request.py index 0f61e22..5f487a4 100644 --- a/resources/lib/utils/request.py +++ b/resources/lib/utils/request.py @@ -1,12 +1,17 @@ """Request utils.""" +import gzip +import json from random import randint +from urllib.error import HTTPError, URLError from urllib.parse import urlparse -from urllib.request import Request +from urllib.request import Request, urlopen + +import xbmc # from socks import SOCKS5 # from sockshandler import SocksiPyHandler -from lib.utils.kodi import get_addon_setting +from lib.utils.kodi import get_addon_setting, log, ok_dialog _USER_AGENTS = [ # Chrome @@ -43,6 +48,27 @@ def build_request(url: str, additional_headers: dict = None) -> Request: return Request(url, headers={"User-Agent": get_random_ua(), "Host": urlparse(url).netloc, **additional_headers}) +def open_request(req: Request, value=None): + """Open HTTP request and handle errors.""" + try: + res = urlopen(req) + except HTTPError as e: + log(e.code, xbmc.LOGERROR) + ok_dialog("HTTPError") + return value + except URLError as e: + log(e.reason, xbmc.LOGERROR) + ok_dialog("URLError") + return value + else: + content = res.read() + + if res.headers.get("Content-Encoding") == "gzip": + content = gzip.decompress(content) + + return json.loads(content) + + def install_proxy() -> None: """Install proxy server for the next requests.""" if get_addon_setting("proxy.enabled") != "true": diff --git a/resources/settings.xml b/resources/settings.xml index 404d524..dfd35fe 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,11 +1,11 @@ - - + +