diff --git a/channels/channel.se/tv4se/chn_tv4se.json b/channels/channel.se/tv4se/chn_tv4se.json index 743b2a3c..0992f076 100644 --- a/channels/channel.se/tv4se/chn_tv4se.json +++ b/channels/channel.se/tv4se/chn_tv4se.json @@ -1,21 +1,5 @@ { "channels": [ - { - "guid": "98506F58-CD6F-11DE-99BA-187F55D89593", - "name": "TV4", - "description": { - "en": "Broadcasts from TV4 (TV4Play.se).", - "sv": "Sändningar från TV4 (TV4Play.se).", - "nl": "Uitzendingen van TV4 (TV4Play.se)." - }, - "icon": "tv4large.png", - "category": "National", - "channelcode": "tv4se", - "sortorder": 4, - "language": "se", - "fanart": "tv4playsefanart.jpg", - "ignore": true - }, { "guid": "68E5476E-DC92-4312-9C57-29FCE428EF20", "name": "TV4 Play", @@ -29,36 +13,6 @@ "sortorder": 3, "language": "se", "fanart": "tv4playsefanart.jpg" - }, - { - "guid": "66174057-D5E3-4102-AF46-B2D8E800E0D9", - "name": "Sjuan", - "description": { - "en": "Broadcasts and show from Sjuan (TV4Play.se).", - "sv": "Sändningar från Sjuan (TV4Play.se)." - }, - "icon": "tv7large.png", - "category": "National", - "channelcode": "tv7se", - "sortorder": 7, - "language": "se", - "fanart": "tv7fanart.png", - "ignore": true - }, - { - "guid": "F2D31759-71BD-4DD1-A1FB-FF8FD99CE03E", - "name": "TV12", - "description": { - "en": "Broadcasts and show from TV12 (TV4Play.se).", - "sv": "Sändningar från TV12 (TV4Play.se)." - }, - "icon": "tv12large.png", - "category": "National", - "channelcode": "tv12se", - "sortorder": 12, - "language": "se", - "fanart": "tv12playfanart.jpg", - "ignore": true } ], "settings": [ diff --git a/channels/channel.se/tv4se/chn_tv4se.py b/channels/channel.se/tv4se/chn_tv4se.py index b59952a9..cb91ef7e 100644 --- a/channels/channel.se/tv4se/chn_tv4se.py +++ b/channels/channel.se/tv4se/chn_tv4se.py @@ -1,5 +1,7 @@ # coding=utf-8 # NOSONAR # SPDX-License-Identifier: GPL-3.0-or-later +from random import randrange +from typing import Optional import pytz import datetime @@ -8,7 +10,7 @@ from resources.lib.helpers.datehelper import DateHelper from resources.lib.helpers.encodinghelper import EncodingHelper from resources.lib.mediaitem import MediaItem, FolderItem -from resources.lib.addonsettings import AddonSettings +from resources.lib.addonsettings import AddonSettings, LOCAL from resources.lib.helpers.jsonhelper import JsonHelper from resources.lib.helpers.htmlentityhelper import HtmlEntityHelper @@ -16,6 +18,7 @@ from resources.lib.logger import Logger from resources.lib.streams.mpd import Mpd from resources.lib.vault import Vault +from resources.lib.webdialogue import WebDialogue from resources.lib.xbmcwrapper import XbmcWrapper from resources.lib.streams.m3u8 import M3u8 from resources.lib.urihandler import UriHandler @@ -38,87 +41,82 @@ def __init__(self, channel_info): # ============== Actual channel setup STARTS here and should be overwritten from derived classes =============== self.__channelId = "tv4" - self.mainListUri = self.__get_api_query( - '{indexPage{panels{__typename,...on SwipeModule{title,subheading,cards{__typename,' - '... on ProgramCard{title,program{name,id,nid,description,image}}}}}}}') + self.__max_page_size = 2500 + self.__access_token = None - if self.channelCode == "tv4segroup": - self.noImage = "tv4image.png" + self.mainListUri = self.__get_api_url( + "MediaIndex", + "dba092c9af0e54e4e3e68dd84b16bb913a9e0e5fe83ff01cf59b6b453d0c75d4", + {"input": {"letterFilters": list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), "limit": self.__max_page_size + randrange(25) * 0, + "offset": 0}}) + self.httpHeaders = {"Content-Type": "application/json", "Client-Name": "tv4-web", "Client-Version": "4.0.0"} - elif self.channelCode == "tv4se": + if self.channelCode == "tv4segroup": self.noImage = "tv4image.png" - self.__channelId = "tv4" - - elif self.channelCode == "tv7se": - self.noImage = "tv7image.png" - self.__channelId = "sjuan" - - elif self.channelCode == "tv12se": - self.noImage = "tv12image.png" - self.__channelId = "tv12" - else: raise Exception("Invalid channel code") - # setup the urls - # self.mainListUri = "https://api.tv4play.se/play/programs?is_active=true&platform=tablet&per_page=1000" \ - # "&fl=nid,name,program_image&start=0" - - self.baseUrl = "http://www.tv4play.se" - self.swfUrl = "http://www.tv4play.se/flash/tv4playflashlets.swf" - self._add_data_parser(self.mainListUri, json=True, - name="GraphQL mainlist parser", - preprocessor=self.add_categories_and_specials, - parser=["data", "indexPage", "panels"], + parser=["data", "mediaIndex", "contentList", "items"], creator=self.create_api_typed_item) - # noinspection PyTypeChecker - self._add_data_parser("https://graphql.tv4play.se/graphql?query=query%7Btags%7D", - name="Tag overview", json=True, - parser=["data", "tags"], creator=self.create_api_tag) - - self._add_data_parser("https://graphql.tv4play.se/graphql?query=%7Bprogram%28nid", - name="GraphQL seasons/folders for program listing", json=True, - preprocessor=self.detect_single_folder, - parser=["data", "program", "videoPanels"], - creator=self.create_api_videopanel_type) - - self._add_data_parser("https://graphql.tv4play.se/graphql?query=%7BvideoPanel%28id%3A", - name="GraphQL single season/folder listing", json=True, - postprocessor=self.add_next_page, - parser=["data", "videoPanel", "videoList", "videoAssets"], - creator=self.create_api_video_asset_type) - - self._add_data_parsers(["https://graphql.tv4play.se/graphql?query=query%7BprogramSearch", - "https://graphql.tv4play.se/graphql?query=%7BprogramSearch"], - name="GraphQL search results and show listings", json=True, - parser=["data", "programSearch", "programs"], - creator=self.create_api_typed_item) - - self._add_data_parser("https://graphql.tv4play.se/graphql?operationName=LiveVideos", - name="GraphQL currently playing", json=True, - parser=["data", "liveVideos", "videoAssets"], - creator=self.create_api_typed_item) - - self._add_data_parser("http://tv4live-i.akamaihd.net/hls/live/", - updater=self.update_live_item) - self._add_data_parser("http://tv4events1-lh.akamaihd.net/i/EXTRAEVENT5_1", - updater=self.update_live_item) + # # setup the urls + # # self.mainListUri = "https://api.tv4play.se/play/programs?is_active=true&platform=tablet&per_page=1000" \ + # # "&fl=nid,name,program_image&start=0" + # + # self.baseUrl = "http://www.tv4play.se" + # self.swfUrl = "http://www.tv4play.se/flash/tv4playflashlets.swf" + # + # self._add_data_parser(self.mainListUri, json=True, + # name="GraphQL mainlist parser", + # preprocessor=self.add_categories_and_specials, + # parser=["data", "indexPage", "panels"], + # creator=self.create_api_typed_item) + # + # # noinspection PyTypeChecker + # self._add_data_parser("https://graphql.tv4play.se/graphql?query=query%7Btags%7D", + # name="Tag overview", json=True, + # parser=["data", "tags"], creator=self.create_api_tag) + # + # self._add_data_parser("https://graphql.tv4play.se/graphql?query=%7Bprogram%28nid", + # name="GraphQL seasons/folders for program listing", json=True, + # preprocessor=self.detect_single_folder, + # parser=["data", "program", "videoPanels"], + # creator=self.create_api_videopanel_type) + # + # self._add_data_parser("https://graphql.tv4play.se/graphql?query=%7BvideoPanel%28id%3A", + # name="GraphQL single season/folder listing", json=True, + # postprocessor=self.add_next_page, + # parser=["data", "videoPanel", "videoList", "videoAssets"], + # creator=self.create_api_video_asset_type) + # + # self._add_data_parsers(["https://graphql.tv4play.se/graphql?query=query%7BprogramSearch", + # "https://graphql.tv4play.se/graphql?query=%7BprogramSearch"], + # name="GraphQL search results and show listings", json=True, + # parser=["data", "programSearch", "programs"], + # creator=self.create_api_typed_item) + # + # self._add_data_parser("https://graphql.tv4play.se/graphql?operationName=LiveVideos", + # name="GraphQL currently playing", json=True, + # parser=["data", "liveVideos", "videoAssets"], + # creator=self.create_api_typed_item) + # + # self._add_data_parser("http://tv4live-i.akamaihd.net/hls/live/", + # updater=self.update_live_item) + # self._add_data_parser("http://tv4events1-lh.akamaihd.net/i/EXTRAEVENT5_1", + # updater=self.update_live_item) self._add_data_parser("*", updater=self.update_video_item, requires_logon=True) # =============================================================================================================== # non standard items - self.__maxPageSize = 100 # The Android app uses a page size of 20 - self.__program_fields = '{__typename,description,displayCategory,id,image,images{main16x9},name,nid,genres,videoPanels{id}}' - self.__season_count_meta = "season_count" self.__timezone = pytz.timezone("Europe/Stockholm") + # self.__maxPageSize = 100 # The Android app uses a page size of 20 + self.__program_fields = '{__typename,description,displayCategory,id,image,images{main16x9},name,nid,genres,videoPanels{id}}' + # self.__season_count_meta = "season_count" # =============================================================================================================== # Test cases: - # Batman - WideVine - # Antikdeckarna - Clips # ====================================== Actual channel setup STOPS here ======================================= return @@ -127,92 +125,69 @@ def __init__(self, channel_info): def log_on(self): """ Makes sure that we are logged on. """ + if self.__access_token: + return True + username = AddonSettings.get_setting("channel_tv4play_se_username") if not username: Logger.info("No user name for TV4 Play, not logging in") return False # Fetch an existing token - token_setting_id = "channel_tv4play_se_token" - token = AddonSettings.get_setting(token_setting_id) - if token and "|" in token: - token_username, token = token.split("|") - if token_username == username: - header, payload, signature = token.split(".") - payload_data = EncodingHelper.decode_base64(payload + '=' * (-len(payload) % 4)) - payload = JsonHelper(payload_data) - expires_at = payload.get_value("exp") - expire_date = DateHelper.get_date_from_posix(float(expires_at), tz=pytz.UTC) - if expire_date > datetime.datetime.now(tz=pytz.UTC).astimezone(tz=pytz.UTC): - Logger.info("Found existing valid TV4Play token (valid until: %s)", expire_date) - return True - Logger.warning("Found existing expired TV4Play token") - - Logger.info("Fetching a new TV4Play token") - data = None - - if not data or "vimond_session_token" not in data: - Logger.info("Authenticating based on username and password") - - v = Vault() - password = v.get_setting("channel_tv4play_se_password") - if not password: - XbmcWrapper.show_dialog( - title=None, - message=LanguageHelper.MissingCredentials, - ) - - # 2b: https://avod-auth-alb.a2d.tv/oauth/authorize - # Content-Type: application/x-www-form-urlencoded; charset=UTF-8 - params = { - "client_id": "tv4play-next", - "response_type": "token", - "credentials": { - "username": username, - "password": password - } - } - data = UriHandler.open( - "https://avod-auth-alb.a2d.tv/oauth/authorize", no_cache=True, json=params) - - if not data: - Logger.error("Error logging in") - return - - data = JsonHelper(data) - if "error" in data.json: - Logger.error(data.get_value("error")) - XbmcWrapper.show_notification(LanguageHelper.ErrorId, data.get_value("error", "message")) - AddonSettings.set_setting(token_setting_id, "") - return False - - # Extract the data we need - token = data.get_value("access_token") - AddonSettings.set_setting(token_setting_id, "{}|{}".format(username, token)) - return True - - def create_api_tag(self, result_set): - """ Creates a new MediaItem for tag listing items - - This method creates a new MediaItem from the Regular Expression or Json - results . The method should be implemented by derived classes - and are specific to the channel. - - :param str result_set: The result_set of the self.episodeItemRegex - - :return: A new MediaItem of type 'folder'. - :rtype: MediaItem|None - - """ - - Logger.trace(result_set) - query = 'query{programSearch(tag:"%s",per_page:1000){__typename,programs' \ - '%s,' \ - 'totalHits}}' % (result_set, self.__program_fields) - query = HtmlEntityHelper.url_encode(query) - url = "https://graphql.tv4play.se/graphql?query={}".format(query) - item = MediaItem(result_set, url) - return item + refresh_token_setting_id = "refresh_token" + token = AddonSettings.get_channel_setting(self, refresh_token_setting_id, store=LOCAL) + + if not token: + # Read the refresh token from a webpage + wd = WebDialogue() + token, cancelled = wd.input("Refresh Token", "Paste refresh token here.", time_out=60) + + if not token or cancelled: + return False + + AddonSettings.set_channel_setting(self, refresh_token_setting_id, token, store=LOCAL) + + header, payload, signature = token.split(".") + payload_data = EncodingHelper.decode_base64(payload + '=' * (-len(payload) % 4)) + payload = JsonHelper(payload_data) + expires_at = payload.get_value("exp") + expire_date = DateHelper.get_date_from_posix(float(expires_at), tz=pytz.UTC) + if expire_date < datetime.datetime.now(tz=pytz.UTC).astimezone(tz=pytz.UTC): + Logger.info("Found expired TV4Play token (valid until: %s)", expire_date) + AddonSettings.set_setting(refresh_token_setting_id, "", store=LOCAL) + # Retry + return self.log_on() + + Logger.info("Found existing valid TV4Play token (valid until: %s)", expire_date) + url = "https://avod-auth-alb.a2d.tv/oauth/refresh" + result = UriHandler.open( + url, json={"refresh_token": token, "client_id":"tv4-web"}, no_cache=True) + result = JsonHelper(result) + self.__access_token = result.get_value("access_token", fallback=None) + return bool(self.__access_token) + + # def create_api_tag(self, result_set): + # """ Creates a new MediaItem for tag listing items + # + # This method creates a new MediaItem from the Regular Expression or Json + # results . The method should be implemented by derived classes + # and are specific to the channel. + # + # :param str result_set: The result_set of the self.episodeItemRegex + # + # :return: A new MediaItem of type 'folder'. + # :rtype: MediaItem|None + # + # """ + # + # Logger.trace(result_set) + # query = 'query{programSearch(tag:"%s",per_page:1000){__typename,programs' \ + # '%s,' \ + # 'totalHits}}' % (result_set, self.__program_fields) + # query = HtmlEntityHelper.url_encode(query) + # url = "https://graphql.tv4play.se/graphql?query={}".format(query) + # item = MediaItem(result_set, url) + # return item def create_api_typed_item(self, result_set): """ Creates a new MediaItem based on the __typename attribute. @@ -229,392 +204,455 @@ def create_api_typed_item(self, result_set): """ api_type = result_set["__typename"] - Logger.trace("%s: %s", api_type, result_set) - - if api_type == "Program": - item = self.create_api_program_type(result_set) - elif api_type == "ProgramCard": - item = self.create_api_program_type(result_set.get("program")) - elif api_type == "VideoAsset": - item = self.create_api_video_asset_type(result_set) - elif api_type == "SwipeModule": - item = self.create_api_swipefolder_type(result_set) - elif api_type == "VideoPanel": - item = self.create_api_videopanel_type(result_set) + + if api_type == "MediaIndexMovieItem": + item = self.create_api_typed_item(result_set["movie"]) + elif api_type == "MediaIndexSeriesItem": + item = self.create_api_typed_item(result_set["series"]) + elif api_type == "Series": + item = self.create_api_series(result_set) + elif api_type == "Movie": + item = self.create_api_movie(result_set) + + # if api_type == "Program": + # item = self.create_api_program_type(result_set) + # elif api_type == "ProgramCard": + # item = self.create_api_program_type(result_set.get("program")) + # elif api_type == "VideoAsset": + # item = self.create_api_video_asset_type(result_set) + # elif api_type == "SwipeModule": + # item = self.create_api_swipefolder_type(result_set) + # elif api_type == "VideoPanel": + # item = self.create_api_videopanel_type(result_set) else: Logger.warning("Missing type: %s", api_type) return None return item - def create_api_program_type(self, result_set): - """ Creates a new MediaItem for an episode. - - This method creates a new MediaItem from the Regular Expression or Json - results . The method should be implemented by derived classes - and are specific to the channel. - - :param list[str]|dict result_set: The result_set of the self.episodeItemRegex - - :return: A new MediaItem of type 'folder'. - :rtype: MediaItem|None - - """ - - json = result_set - title = json["name"] - - # https://graphql.tv4play.se/graphql?operationName=cdp&variables={"nid":"100-penisar"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"255449d35b5679b2cb5a9b85e63afd532c68d50268ae2740ae82f24d83a84774"}} - program_id = json["nid"] - url = self.__get_api_query( - '{program(nid:"%s"){name,description,videoPanels{id,name,subheading,assetType}}}' % ( - program_id,)) - - item = FolderItem(title, url, content_type=contenttype.EPISODES) - item.tv_show_title = title - item.description = result_set.get("description", None) - - item.thumb = result_set.get("image") - # if item.thumb is not None and "://img.b17g.net/" in item.thumb: - # item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ - # "&quality=70&resize=520x293&source={}" \ - # .format(HtmlEntityHelper.url_encode(item.thumb)) - - item.fanart = result_set.get("image") - # if item.fanart is not None and "://img.b17g.net/" in item.fanart: - # item.fanart = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ - # "&quality=70&resize=1280x720&source={}" \ - # .format(HtmlEntityHelper.url_encode(item.fanart)) - - item.isPaid = result_set.get("is_premium", False) + def create_api_movie(self, result_set: dict) -> Optional[MediaItem]: + video_id: str = result_set["id"] + url = self.__get_video_url(video_id) + item = self.__create_base_typed_item(result_set, url, mediatype.MOVIE) return item - def create_api_videopanel_type(self, result_set): - """ Creates a new MediaItem for a folder listing - - This method creates a new MediaItem from the Regular Expression or Json - results . The method should be implemented by derived classes - and are specific to the channel. - - :param dict result_set: The result_set of the self.episodeItemRegex - - :return: A new MediaItem of type 'folder'. - :rtype: MediaItem|None - - """ - - title = result_set["name"] - folder_id = result_set["id"] - url = self.__get_api_folder_url(folder_id) - item = FolderItem(title, url, content_type=contenttype.EPISODES) - item.metaData["offset"] = 0 - item.metaData["folder_id"] = folder_id + def create_api_series(self, result_set: dict) -> Optional[MediaItem]: + item = self.__create_base_typed_item(result_set, "", mediatype.TVSHOW) return item - def create_api_swipefolder_type(self, result_set): - """ Creates a new MediaItem for a folder listing - - This method creates a new MediaItem from the Regular Expression or Json - results . The method should be implemented by derived classes - and are specific to the channel. + def create_api_video(self, result_set: dict) -> Optional[MediaItem]: + return None - :param dict result_set: The result_set of the self.episodeItemRegex - - :return: A new MediaItem of type 'folder'. - :rtype: MediaItem|None - - """ + def __create_base_typed_item(self, result_set: dict, url: str, media_type: Optional[str]) -> Optional[MediaItem]: + Logger.trace(result_set) title = result_set["title"] - - if title == "Sista chansen": - title = LanguageHelper.get_localized_string(LanguageHelper.LastChance) - elif title == "Mest sedda programmen": - title = LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes) - elif title.startswith("Popul"): - title = LanguageHelper.get_localized_string(LanguageHelper.Popular) - elif title.startswith("Nyheter"): - title = LanguageHelper.get_localized_string(LanguageHelper.LatestNews) - - item = FolderItem(title, "swipe://{}".format(HtmlEntityHelper.url_encode(title)), - content_type=contenttype.VIDEOS) - for card in result_set["cards"]: - child = self.create_api_typed_item(card) - if not child: - continue - - item.items.append(child) - - if not item.items: + if not title: return None - + url = url + item = MediaItem(title, url, media_type=media_type) + self.__set_art(item, result_set.get("images")) return item - def create_api_video_asset_type(self, result_set): - """ Creates a MediaItem of type 'video' using the result_set from the regex. - - This method creates a new MediaItem from the Regular Expression or Json - results . The method should be implemented by derived classes - and are specific to the channel. - - If the item is completely processed an no further data needs to be fetched - the self.complete property should be set to True. If not set to True, the - self.update_video_item method is called if the item is focussed or selected - for playback. - - :param list[str]|dict result_set: The result_set of the self.episodeItemRegex - - :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). - :rtype: MediaItem|None - - """ - - Logger.trace('starting FormatVideoItem for %s', self.channelName) - - program_id = result_set["id"] - url = "https://playback2.a2d.tv/play/{}?service=tv4" \ + def __get_video_url(self, program_id: str): + # https://playback2.a2d.tv/play/8d1eb26ad728c9125de8?service=tv4play&device=browser&protocol=hls%2Cdash&drm=widevine&browser=GoogleChrome&capabilities=live-drm-adstitch-2%2Cyospace3 + url = "https://playback2.a2d.tv/play/{}?service=tv4play" \ "&device=browser&browser=GoogleChrome" \ "&protocol=hls%2Cdash" \ "&drm=widevine" \ "&capabilities=live-drm-adstitch-2%2Cexpired_assets". \ format(program_id) - # url = "https://playback-api.b17g.net/media/{}?service=tv4&device=browser&protocol=dash". \ - - name = result_set["title"] - season = result_set.get("season", 0) - episode = result_set.get("episode", 0) - is_episodic = 0 < season < 1900 and not episode == 0 - is_live = result_set.get("live", False) - if is_episodic and not is_live: - episode_text = None - if " del " in name: - name, episode_text = name.split(" del ", 1) - episode_text = episode_text.lstrip("0123456789") - - if episode_text: - episode_text = episode_text.lstrip(" -") - name = episode_text - else: - name = "{} {}".format("Avsnitt", episode) - - item = MediaItem(name, url) - item.description = result_set.get("description") - if item.description is None: - item.description = item.name - - if is_episodic and not is_live: - item.set_season_info(season, episode) - - # premium_expire_date_time=2099-12-31T00:00:00+01:00 - expire_in_days = result_set.get("daysLeftInService", 0) - if 0 < expire_in_days < 10000: - item.set_expire_datetime( - timestamp=datetime.datetime.now() + datetime.timedelta(days=expire_in_days)) - - date = result_set["broadcastDateTime"] - broadcast_date = DateHelper.get_datetime_from_string(date, "%Y-%m-%dT%H:%M:%SZ", "UTC") - broadcast_date = broadcast_date.astimezone(self.__timezone) - item.set_date(broadcast_date.year, - broadcast_date.month, - broadcast_date.day, - broadcast_date.hour, - broadcast_date.minute, - 0) - - item.fanart = result_set.get("program_image", self.parentItem.fanart) - item.thumb = result_set.get("image", result_set.get("program_image")) - # some images need to come via a proxy: - # if item.thumb and "://img.b17g.net/" in item.thumb: - # item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ - # "&quality=70&resize=520x293&source={}" \ - # .format(HtmlEntityHelper.url_encode(item.thumb)) - - item.media_type = mediatype.EPISODE - item.complete = False - item.isGeoLocked = True - # For now, none are paid. - # item.isPaid = not result_set.get("freemium", False) - if "drmProtected" in result_set: - item.isDrmProtected = result_set["drmProtected"] - elif "is_drm_protected" in result_set: - item.isDrmProtected = result_set["is_drm_protected"] - - item.isLive = result_set.get("live", False) - if item.isLive: - item.name = "{:02d}:{:02d} - {}".format(broadcast_date.hour, broadcast_date.minute, - name) - item.url = "{0}&is_live=true".format(item.url) - if item.isDrmProtected: - item.url = "{}&drm=widevine&is_drm=true".format(item.url) - - item.set_info_label("duration", int(result_set.get("duration", 0))) - return item - - def add_next_page(self, data, items): - """ Performs post-process actions for data processing. - - Accepts an data from the process_folder_list method, BEFORE the items are - processed. Allows setting of parameters (like title etc) for the channel. - Inside this method the could be changed and additional items can - be created. - - The return values should always be instantiated in at least ("", []). - - :param str|JsonHelper data: The retrieve data that was loaded for the - current item and URL. - :param list[MediaItem] items: The currently available items - - :return: A tuple of the data and a list of MediaItems that were generated. - :rtype: list[MediaItem] - - """ - - Logger.info("Performing Post-Processing") - - total_hits = data.get_value('data', 'videoPanel', 'videoList', 'totalHits') - if total_hits > len(items): - Logger.debug("Adding items from next page") - offset = self.parentItem.metaData.get("offset", 0) + self.__maxPageSize - folder_id = self.parentItem.metaData.get("folder_id") - if not folder_id: - Logger.warning("Cannot find 'folder_id' in MediaItem") - return items - - url = self.__get_api_folder_url(folder_id, offset) - data = UriHandler.open(url) - json_data = JsonHelper(data) - extra_results = json_data.get_value("data", "videoPanel", "videoList", "videoAssets", - fallback=[]) - Logger.debug("Adding %d extra results from next page", len(extra_results or [])) - for result in extra_results: - item = self.create_api_video_asset_type(result) - if item: - items.append(item) - - Logger.debug("Post-Processing finished") - return items - - def detect_single_folder(self, data): - """ Performs pre-process actions and detect single folder items - - Accepts an data from the process_folder_list method, BEFORE the items are - processed. Allows setting of parameters (like title etc) for the channel. - Inside this method the could be changed and additional items can - be created. - - The return values should always be instantiated in at least ("", []). - - :param str data: The retrieve data that was loaded for the current item and URL. - - :return: A tuple of the data and a list of MediaItems that were generated. - :rtype: tuple[str|JsonHelper,list[MediaItem]] - - """ - - json_data = JsonHelper(data) - panels = json_data.get_value("data", "program", "videoPanels") - if len(panels) != 1: - return data, [] - - items = [] - item = self.create_api_videopanel_type(panels[0]) - data = UriHandler.open(item.url) - json_data = JsonHelper(data) - assets = json_data.get_value("data", "videoPanel", "videoList", "videoAssets") - for asset in assets: - item = self.create_api_video_asset_type(asset) - if item: - items.append(item) - - return "", items - - def add_categories_and_specials(self, data): - """ Performs pre-process actions for data processing. - - Accepts a data from the process_folder_list method, BEFORE the items are - processed. Allows setting of parameters (like title etc) for the channel. - Inside this method the could be changed and additional items can - be created. - - The return values should always be instantiated in at least ("", []). - - :param str data: The retrieve data that was loaded for the current item and URL. - - :return: A tuple of the data and a list of MediaItems that were generated. - :rtype: tuple[str|JsonHelper,list[MediaItem]] - - """ - - Logger.info("Performing Pre-Processing") - items = [] - - extras = { - LanguageHelper.get_localized_string(LanguageHelper.Search): ("searchSite", None, False), - LanguageHelper.get_localized_string(LanguageHelper.TvShows): ( - # "https://www.tv4play.se/alla-program", - self.__get_api_query("query{programSearch(per_page:1000){__typename,programs{" - "__typename,description,displayCategory,id,image," - "images{main16x9},name,nid,genres},totalHits}}"), - None, False - ), - LanguageHelper.get_localized_string(LanguageHelper.Categories): ( - "https://graphql.tv4play.se/graphql?query=query%7Btags%7D", None, False - ), - LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes): ( - self.__get_api_url("LiveVideos", - "800a3ef456fa19eaa1cfa23aab646c50fab91a1a8f82660f56ec03c9bf61c028"), - None, False - ) - } + return url - # No more extras - # today = datetime.datetime.now() - # days = [LanguageHelper.get_localized_string(LanguageHelper.Monday), - # LanguageHelper.get_localized_string(LanguageHelper.Tuesday), - # LanguageHelper.get_localized_string(LanguageHelper.Wednesday), - # LanguageHelper.get_localized_string(LanguageHelper.Thursday), - # LanguageHelper.get_localized_string(LanguageHelper.Friday), - # LanguageHelper.get_localized_string(LanguageHelper.Saturday), - # LanguageHelper.get_localized_string(LanguageHelper.Sunday)] - # for i in range(0, 7, 1): - # start_date = today - datetime.timedelta(i) - # end_date = start_date + datetime.timedelta(1) - # - # day = days[start_date.weekday()] - # if i == 0: - # day = LanguageHelper.get_localized_string(LanguageHelper.Today) - # elif i == 1: - # day = LanguageHelper.get_localized_string(LanguageHelper.Yesterday) - # - # Logger.trace("Adding item for: %s - %s", start_date, end_date) - # url = "https://api.tv4play.se/play/video_assets?exclude_node_nids=" \ - # "&platform=tablet&is_live=false&product_groups=2&type=episode&per_page=100" - # url = "%s&broadcast_from=%s&broadcast_to=%s&" % (url, start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")) - # extras[day] = (url, start_date, False) - # - # extras[LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes)] = ( - # "https://api.tv4play.se/play/video_assets?exclude_node_nids=&platform=tablet&" - # "is_live=true&product_groups=2&type=episode&per_page=100", None, False) - - # Actually add the extra items - for name in extras: - title = name - url, date, is_live = extras[name] # type: str, datetime.datetime, bool - item = FolderItem(title, url, content_type=contenttype.VIDEOS) - item.dontGroup = True - item.complete = True - item.HttpHeaders = self.httpHeaders - item.isLive = is_live + def __set_art(self, item: MediaItem, art_info: Optional[dict]): + if not art_info: + return - if date is not None: - item.set_date(date.year, date.month, date.day, 0, 0, 0, - text=date.strftime("%Y-%m-%d")) + for k,v in art_info.items(): + if isinstance(v, str): + continue - items.append(item) + encoded_url = v.get("sourceEncoded") + if not encoded_url: + continue - Logger.debug("Pre-Processing finished") - return data, items + url = HtmlEntityHelper.url_decode(encoded_url) + if k == "cover2x3": + item.set_artwork(poster=url) + elif k == "main16x9Annotated": + item.set_artwork(thumb=url, fanart=url) + else: + Logger.warning("Unknown image format") + + # def create_api_program_type(self, result_set): + # """ Creates a new MediaItem for an episode. + # + # This method creates a new MediaItem from the Regular Expression or Json + # results . The method should be implemented by derived classes + # and are specific to the channel. + # + # :param list[str]|dict result_set: The result_set of the self.episodeItemRegex + # + # :return: A new MediaItem of type 'folder'. + # :rtype: MediaItem|None + # + # """ + # + # json = result_set + # title = json["name"] + # + # # https://graphql.tv4play.se/graphql?operationName=cdp&variables={"nid":"100-penisar"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"255449d35b5679b2cb5a9b85e63afd532c68d50268ae2740ae82f24d83a84774"}} + # program_id = json["nid"] + # url = self.__get_api_query( + # '{program(nid:"%s"){name,description,videoPanels{id,name,subheading,assetType}}}' % ( + # program_id,)) + # + # item = FolderItem(title, url, content_type=contenttype.EPISODES) + # item.tv_show_title = title + # item.description = result_set.get("description", None) + # + # item.thumb = result_set.get("image") + # # if item.thumb is not None and "://img.b17g.net/" in item.thumb: + # # item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ + # # "&quality=70&resize=520x293&source={}" \ + # # .format(HtmlEntityHelper.url_encode(item.thumb)) + # + # item.fanart = result_set.get("image") + # # if item.fanart is not None and "://img.b17g.net/" in item.fanart: + # # item.fanart = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ + # # "&quality=70&resize=1280x720&source={}" \ + # # .format(HtmlEntityHelper.url_encode(item.fanart)) + # + # item.isPaid = result_set.get("is_premium", False) + # + # return item + # + # def create_api_videopanel_type(self, result_set): + # """ Creates a new MediaItem for a folder listing + # + # This method creates a new MediaItem from the Regular Expression or Json + # results . The method should be implemented by derived classes + # and are specific to the channel. + # + # :param dict result_set: The result_set of the self.episodeItemRegex + # + # :return: A new MediaItem of type 'folder'. + # :rtype: MediaItem|None + # + # """ + # + # title = result_set["name"] + # folder_id = result_set["id"] + # url = self.__get_api_folder_url(folder_id) + # item = FolderItem(title, url, content_type=contenttype.EPISODES) + # item.metaData["offset"] = 0 + # item.metaData["folder_id"] = folder_id + # return item + # + # def create_api_swipefolder_type(self, result_set): + # """ Creates a new MediaItem for a folder listing + # + # This method creates a new MediaItem from the Regular Expression or Json + # results . The method should be implemented by derived classes + # and are specific to the channel. + # + # :param dict result_set: The result_set of the self.episodeItemRegex + # + # :return: A new MediaItem of type 'folder'. + # :rtype: MediaItem|None + # + # """ + # + # title = result_set["title"] + # + # if title == "Sista chansen": + # title = LanguageHelper.get_localized_string(LanguageHelper.LastChance) + # elif title == "Mest sedda programmen": + # title = LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes) + # elif title.startswith("Popul"): + # title = LanguageHelper.get_localized_string(LanguageHelper.Popular) + # elif title.startswith("Nyheter"): + # title = LanguageHelper.get_localized_string(LanguageHelper.LatestNews) + # + # item = FolderItem(title, "swipe://{}".format(HtmlEntityHelper.url_encode(title)), + # content_type=contenttype.VIDEOS) + # for card in result_set["cards"]: + # child = self.create_api_typed_item(card) + # if not child: + # continue + # + # item.items.append(child) + # + # if not item.items: + # return None + # + # return item + # + # def create_api_video_asset_type(self, result_set): + # """ Creates a MediaItem of type 'video' using the result_set from the regex. + # + # This method creates a new MediaItem from the Regular Expression or Json + # results . The method should be implemented by derived classes + # and are specific to the channel. + # + # If the item is completely processed an no further data needs to be fetched + # the self.complete property should be set to True. If not set to True, the + # self.update_video_item method is called if the item is focussed or selected + # for playback. + # + # :param list[str]|dict result_set: The result_set of the self.episodeItemRegex + # + # :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). + # :rtype: MediaItem|None + # + # """ + # + # Logger.trace('starting FormatVideoItem for %s', self.channelName) + # + # program_id = result_set["id"] + # url = "https://playback2.a2d.tv/play/{}?service=tv4" \ + # "&device=browser&browser=GoogleChrome" \ + # "&protocol=hls%2Cdash" \ + # "&drm=widevine" \ + # "&capabilities=live-drm-adstitch-2%2Cexpired_assets". \ + # format(program_id) + # # url = "https://playback-api.b17g.net/media/{}?service=tv4&device=browser&protocol=dash". \ + # + # name = result_set["title"] + # season = result_set.get("season", 0) + # episode = result_set.get("episode", 0) + # is_episodic = 0 < season < 1900 and not episode == 0 + # is_live = result_set.get("live", False) + # if is_episodic and not is_live: + # episode_text = None + # if " del " in name: + # name, episode_text = name.split(" del ", 1) + # episode_text = episode_text.lstrip("0123456789") + # + # if episode_text: + # episode_text = episode_text.lstrip(" -") + # name = episode_text + # else: + # name = "{} {}".format("Avsnitt", episode) + # + # item = MediaItem(name, url) + # item.description = result_set.get("description") + # if item.description is None: + # item.description = item.name + # + # if is_episodic and not is_live: + # item.set_season_info(season, episode) + # + # # premium_expire_date_time=2099-12-31T00:00:00+01:00 + # expire_in_days = result_set.get("daysLeftInService", 0) + # if 0 < expire_in_days < 10000: + # item.set_expire_datetime( + # timestamp=datetime.datetime.now() + datetime.timedelta(days=expire_in_days)) + # + # date = result_set["broadcastDateTime"] + # broadcast_date = DateHelper.get_datetime_from_string(date, "%Y-%m-%dT%H:%M:%SZ", "UTC") + # broadcast_date = broadcast_date.astimezone(self.__timezone) + # item.set_date(broadcast_date.year, + # broadcast_date.month, + # broadcast_date.day, + # broadcast_date.hour, + # broadcast_date.minute, + # 0) + # + # item.fanart = result_set.get("program_image", self.parentItem.fanart) + # item.thumb = result_set.get("image", result_set.get("program_image")) + # # some images need to come via a proxy: + # # if item.thumb and "://img.b17g.net/" in item.thumb: + # # item.thumb = "https://imageproxy.b17g.services/?format=jpg&shape=cut" \ + # # "&quality=70&resize=520x293&source={}" \ + # # .format(HtmlEntityHelper.url_encode(item.thumb)) + # + # item.media_type = mediatype.EPISODE + # item.complete = False + # item.isGeoLocked = True + # # For now, none are paid. + # # item.isPaid = not result_set.get("freemium", False) + # if "drmProtected" in result_set: + # item.isDrmProtected = result_set["drmProtected"] + # elif "is_drm_protected" in result_set: + # item.isDrmProtected = result_set["is_drm_protected"] + # + # item.isLive = result_set.get("live", False) + # if item.isLive: + # item.name = "{:02d}:{:02d} - {}".format(broadcast_date.hour, broadcast_date.minute, + # name) + # item.url = "{0}&is_live=true".format(item.url) + # if item.isDrmProtected: + # item.url = "{}&drm=widevine&is_drm=true".format(item.url) + # + # item.set_info_label("duration", int(result_set.get("duration", 0))) + # return item + # + # def add_next_page(self, data, items): + # """ Performs post-process actions for data processing. + # + # Accepts an data from the process_folder_list method, BEFORE the items are + # processed. Allows setting of parameters (like title etc) for the channel. + # Inside this method the could be changed and additional items can + # be created. + # + # The return values should always be instantiated in at least ("", []). + # + # :param str|JsonHelper data: The retrieve data that was loaded for the + # current item and URL. + # :param list[MediaItem] items: The currently available items + # + # :return: A tuple of the data and a list of MediaItems that were generated. + # :rtype: list[MediaItem] + # + # """ + # + # Logger.info("Performing Post-Processing") + # + # total_hits = data.get_value('data', 'videoPanel', 'videoList', 'totalHits') + # if total_hits > len(items): + # Logger.debug("Adding items from next page") + # offset = self.parentItem.metaData.get("offset", 0) + self.__maxPageSize + # folder_id = self.parentItem.metaData.get("folder_id") + # if not folder_id: + # Logger.warning("Cannot find 'folder_id' in MediaItem") + # return items + # + # url = self.__get_api_folder_url(folder_id, offset) + # data = UriHandler.open(url) + # json_data = JsonHelper(data) + # extra_results = json_data.get_value("data", "videoPanel", "videoList", "videoAssets", + # fallback=[]) + # Logger.debug("Adding %d extra results from next page", len(extra_results or [])) + # for result in extra_results: + # item = self.create_api_video_asset_type(result) + # if item: + # items.append(item) + # + # Logger.debug("Post-Processing finished") + # return items + # + # def detect_single_folder(self, data): + # """ Performs pre-process actions and detect single folder items + # + # Accepts an data from the process_folder_list method, BEFORE the items are + # processed. Allows setting of parameters (like title etc) for the channel. + # Inside this method the could be changed and additional items can + # be created. + # + # The return values should always be instantiated in at least ("", []). + # + # :param str data: The retrieve data that was loaded for the current item and URL. + # + # :return: A tuple of the data and a list of MediaItems that were generated. + # :rtype: tuple[str|JsonHelper,list[MediaItem]] + # + # """ + # + # json_data = JsonHelper(data) + # panels = json_data.get_value("data", "program", "videoPanels") + # if len(panels) != 1: + # return data, [] + # + # items = [] + # item = self.create_api_videopanel_type(panels[0]) + # data = UriHandler.open(item.url) + # json_data = JsonHelper(data) + # assets = json_data.get_value("data", "videoPanel", "videoList", "videoAssets") + # for asset in assets: + # item = self.create_api_video_asset_type(asset) + # if item: + # items.append(item) + # + # return "", items + # + # def add_categories_and_specials(self, data): + # """ Performs pre-process actions for data processing. + # + # Accepts a data from the process_folder_list method, BEFORE the items are + # processed. Allows setting of parameters (like title etc) for the channel. + # Inside this method the could be changed and additional items can + # be created. + # + # The return values should always be instantiated in at least ("", []). + # + # :param str data: The retrieve data that was loaded for the current item and URL. + # + # :return: A tuple of the data and a list of MediaItems that were generated. + # :rtype: tuple[str|JsonHelper,list[MediaItem]] + # + # """ + # + # Logger.info("Performing Pre-Processing") + # items = [] + # + # extras = { + # LanguageHelper.get_localized_string(LanguageHelper.Search): ("searchSite", None, False), + # LanguageHelper.get_localized_string(LanguageHelper.TvShows): ( + # # "https://www.tv4play.se/alla-program", + # self.__get_api_query("query{programSearch(per_page:1000){__typename,programs{" + # "__typename,description,displayCategory,id,image," + # "images{main16x9},name,nid,genres},totalHits}}"), + # None, False + # ), + # LanguageHelper.get_localized_string(LanguageHelper.Categories): ( + # "https://graphql.tv4play.se/graphql?query=query%7Btags%7D", None, False + # ), + # LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes): ( + # self.__get_api_url("LiveVideos", + # "800a3ef456fa19eaa1cfa23aab646c50fab91a1a8f82660f56ec03c9bf61c028"), + # None, False + # ) + # } + # + # # No more extras + # # today = datetime.datetime.now() + # # days = [LanguageHelper.get_localized_string(LanguageHelper.Monday), + # # LanguageHelper.get_localized_string(LanguageHelper.Tuesday), + # # LanguageHelper.get_localized_string(LanguageHelper.Wednesday), + # # LanguageHelper.get_localized_string(LanguageHelper.Thursday), + # # LanguageHelper.get_localized_string(LanguageHelper.Friday), + # # LanguageHelper.get_localized_string(LanguageHelper.Saturday), + # # LanguageHelper.get_localized_string(LanguageHelper.Sunday)] + # # for i in range(0, 7, 1): + # # start_date = today - datetime.timedelta(i) + # # end_date = start_date + datetime.timedelta(1) + # # + # # day = days[start_date.weekday()] + # # if i == 0: + # # day = LanguageHelper.get_localized_string(LanguageHelper.Today) + # # elif i == 1: + # # day = LanguageHelper.get_localized_string(LanguageHelper.Yesterday) + # # + # # Logger.trace("Adding item for: %s - %s", start_date, end_date) + # # url = "https://api.tv4play.se/play/video_assets?exclude_node_nids=" \ + # # "&platform=tablet&is_live=false&product_groups=2&type=episode&per_page=100" + # # url = "%s&broadcast_from=%s&broadcast_to=%s&" % (url, start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")) + # # extras[day] = (url, start_date, False) + # # + # # extras[LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes)] = ( + # # "https://api.tv4play.se/play/video_assets?exclude_node_nids=&platform=tablet&" + # # "is_live=true&product_groups=2&type=episode&per_page=100", None, False) + # + # # Actually add the extra items + # for name in extras: + # title = name + # url, date, is_live = extras[name] # type: str, datetime.datetime, bool + # item = FolderItem(title, url, content_type=contenttype.VIDEOS) + # item.dontGroup = True + # item.complete = True + # item.HttpHeaders = self.httpHeaders + # item.isLive = is_live + # + # if date is not None: + # item.set_date(date.year, date.month, date.day, 0, 0, 0, + # text=date.strftime("%Y-%m-%d")) + # + # items.append(item) + # + # Logger.debug("Pre-Processing finished") + # return data, items def search_site(self, url=None): """ Creates an list of items by searching the site. @@ -772,7 +810,7 @@ def __get_api_url(self, operation, hash_value, variables=None): # NOSONAR final_vars = variables final_vars = HtmlEntityHelper.url_encode(JsonHelper.dump(final_vars, pretty_print=False)) - url = "https://graphql.tv4play.se/graphql?" \ + url = "https://client-gateway.tv4.a2d.tv/graphql?" \ "operationName={}&" \ "variables={}&" \ "extensions={}".format(operation, final_vars, extensions) @@ -782,12 +820,12 @@ def __get_api_query(self, query): return "https://graphql.tv4play.se/graphql?query={}".format( HtmlEntityHelper.url_encode(query)) - def __get_api_folder_url(self, folder_id, offset=0): - return self.__get_api_query( - '{videoPanel(id: "%s"){name,videoList(limit: %s, offset:%d, ' - 'sortOrder: "broadcastDateTime"){totalHits,videoAssets' - '{title,id,description,season,episode,daysLeftInService,broadcastDateTime,image,' - 'freemium,drmProtected,live,duration}}}}' % (folder_id, self.__maxPageSize, offset)) + # def __get_api_folder_url(self, folder_id, offset=0): + # return self.__get_api_query( + # '{videoPanel(id: "%s"){name,videoList(limit: %s, offset:%d, ' + # 'sortOrder: "broadcastDateTime"){totalHits,videoAssets' + # '{title,id,description,season,episode,daysLeftInService,broadcastDateTime,image,' + # 'freemium,drmProtected,live,duration}}}}' % (folder_id, self.__maxPageSize, offset)) def __update_dash_video(self, item, stream_info): """