From 4954552c1f6bb493e649f439c9ad8af21e3d285c Mon Sep 17 00:00:00 2001 From: Bas Rieter Date: Sun, 21 Jul 2024 13:32:51 +0200 Subject: [PATCH] [plugin.video.retrospect] v5.7.14 --- plugin.video.retrospect/addon.xml | 19 +- .../channels/channel.be/vier/chn_vier.json | 37 +- .../channels/channel.be/vier/chn_vier.py | 917 +++++++++--------- .../channels/channel.be/vtmbe/chn_vtmbe.py | 20 +- .../channels/channel.mtg/tvse/chn_tvse.py | 21 +- .../channel.mtg/viafree/chn_viafree.py | 20 +- .../channels/channel.no/nrkno/chn_nrkno.py | 19 +- .../channels/channel.nos/bvntv/chn_bvntv.py | 20 +- .../channel.nos/nos2010/chn_nos2010.py | 40 +- .../channels/channel.rtlnl/rtl/chn_rtl.py | 19 +- .../channel.sbsnl/kijknl/chn_kijknl.py | 16 +- .../channel.se/oppetarkiv/chn_oppetarkiv.py | 20 +- .../channels/channel.se/sbs/chn_sbs.py | 40 +- .../channels/channel.se/svt/chn_svt.json | 1 + .../channels/channel.se/svt/chn_svt.py | 21 +- .../channels/channel.se/tv4se/chn_tv4se.py | 24 +- .../channel.se/urplay/chn_urplay.json | 1 + .../channels/channel.se/urplay/chn_urplay.py | 175 ++-- .../videolandnl/chn_videolandnl.py | 20 +- .../channel.videos/dumpert/chn_dumpert.py | 20 +- .../tweedekamer/chn_tweedekamer.py | 20 +- .../resource.language.en_gb/strings.po | 8 +- .../resource.language.es_es/strings.po | 44 +- .../resource.language.nl_nl/strings.po | 6 +- .../resource.language.uk_ua/strings.po | 10 +- .../resources/lib/actions/__init__.py | 2 +- .../resources/lib/actions/action.py | 1 + .../resources/lib/actions/actionparser.py | 33 +- .../resources/lib/actions/folderaction.py | 15 +- .../resources/lib/actions/keyword.py | 1 + .../resources/lib/actions/searchaction.py | 109 +++ .../resources/lib/chn_class.py | 66 +- .../resources/lib/helpers/languagehelper.py | 1 + .../resources/lib/mediaitem.py | 47 +- .../resources/lib/plugin.py | 5 + .../resources/settings.xml | 10 +- 36 files changed, 1084 insertions(+), 764 deletions(-) create mode 100644 plugin.video.retrospect/resources/lib/actions/searchaction.py diff --git a/plugin.video.retrospect/addon.xml b/plugin.video.retrospect/addon.xml index 3233cab847..89760ad138 100644 --- a/plugin.video.retrospect/addon.xml +++ b/plugin.video.retrospect/addon.xml @@ -1,6 +1,6 @@ @@ -123,20 +123,23 @@ all GPL-3.0-or-later en nl de sv no lt lv fi - [B]Retrospect v5.7.13 - Changelog - 2024-06-14[/B] + [B]Retrospect v5.7.14 - Changelog - 2024-07-21[/B] -Fixed a number of Swedish channel issues. +This version introduces enhanced searching and keeps track of previous searches, so you can reuse them. It also includes a number of fixes for channels, such as GoPlay and UR Play. And finally some artwork was updated. [B]Framework related[/B] -* Added: Fallback format option to DateHelper. +* Added: New search functionality with search history options (Fixes #1805). [B]GUI/Settings/Language related[/B] _None_ [B]Channel related[/B] -* Fixed: ONS TV season/episode issue. -* Fixed: Playback of some SVT streams (Fixes #1801). -* Fixed: Some timestamps are with and some without milliseconds (Fixes #1800). +* Updated: UR Play artwork and fixed categories. +* Updated: SVT poster and icon. +* Fixed: SVT HTML based videos (Fixes #1808). +* Fixed: GoPlay (Vier, Vijf, Zes, Zeven) channels broken (Fixes #1809). +* Fixed: UR Play listings (Fixes #1812). + resources/media/icon.png @@ -150,7 +153,7 @@ _None_ Mit Retrospect kann man Wiederholungen / vorherige Folgen von Serien, die von ihren ofiziellen Sendern zur Verfügung gestellt wurden, ansehen. Retrospect allows you to watch re-runs/catch-ups of TV shows made available via their official broadcasters. Retrospect allows you to watch re-runs/catch-ups of TV shows made available via their official broadcasters. - Retrospect te permite ver reposiciones de programas de televisión disponibles a través de sus emisoras oficiales. + Retrospect le permite ver reposiciones de programas de televisión disponibles a través de sus emisoras oficiales. Retrospect te permite ver repeticiones/actualizaciones de series disponibles desde sus emisoras oficiales. Retrospect ti consente di guardare e recuperare le serie TV replicate rese disponibili tramite le loro emittenti ufficiali. Retrospect maakt het mogelijk om afleveringen te bekijken van TV series die door de officiële zenders beschikbaar worden gesteld. diff --git a/plugin.video.retrospect/channels/channel.be/vier/chn_vier.json b/plugin.video.retrospect/channels/channel.be/vier/chn_vier.json index ef6d1ab870..d65922d5a5 100644 --- a/plugin.video.retrospect/channels/channel.be/vier/chn_vier.json +++ b/plugin.video.retrospect/channels/channel.be/vier/chn_vier.json @@ -1,5 +1,21 @@ { "channels": [ + { + "guid": "E108F736-8375-4B5D-B74A-7B70BF52043E", + "name": "GoPlay", + "description": { + "en": "Online video on demand from van goplay.be.\n\nA username and password can be configured for this channel. This enables you to watch all shows to which you are eligible.", + "nl": "Online uitzendingen van goplay.be.\n\nHet is mogelijk een gebruikersnaam en wachtwoord te configuren voor dit kanaal. Zo is het mogelijk om alle content te bekijken waarvoor het account is gerechtigd." + }, + "icon": "goplayicon.png", + "poster": "goplayposter.png", + "category": "National", + "channelcode": "goplay", + "sortorder": 1, + "language": "be", + "fanart": "goplayfanart.png", + "adaptiveAddonSelectable": true + }, { "guid": "50E04907-8983-4552-A775-E70F3D91FB99", "name": "Vier", @@ -8,6 +24,7 @@ "nl": "Online uitzendingen van www.vier.be.\n\nHet is mogelijk een gebruikersnaam en wachtwoord te configuren voor dit kanaal. Zo is het mogelijk om alle content te bekijken waarvoor het account is gerechtigd." }, "icon": "viericon.png", + "poster": "vierposter.png", "category": "National", "channelcode": null, "sortorder": 4, @@ -23,6 +40,7 @@ "nl": "Online uitzendingen van www.vijf.be.\n\nHet is mogelijk een gebruikersnaam en wachtwoord te configuren voor dit kanaal. Zo is het mogelijk om alle content te bekijken waarvoor het account is gerechtigd." }, "icon": "vijficon.png", + "poster": "vijfposter.png", "category": "National", "channelcode": "vijfbe", "sortorder": 5, @@ -38,12 +56,29 @@ "nl": "Online uitzendingen van www.zestv.be.\n\nHet is mogelijk een gebruikersnaam en wachtwoord te configuren voor dit kanaal. Zo is het mogelijk om alle content te bekijken waarvoor het account is gerechtigd." }, "icon": "zesiconlight.png", + "poster": "zesposter.png", "category": "National", "channelcode": "zesbe", "sortorder": 6, "language": "be", "fanart": "zesfanart.jpg", "adaptiveAddonSelectable": true + }, + { + "guid": "3C96398D-22E5-4C61-9308-092FC34A19C0", + "name": "Zeven", + "description": { + "en": "Online Online video on demand from www.zeventv.be.\n\nA username and password can be configured for this channel. This enables you to watch all shows to which you are eligible.", + "nl": "Online uitzendingen van www.goplay.be.\n\nHet is mogelijk een gebruikersnaam en wachtwoord te configuren voor dit kanaal. Zo is het mogelijk om alle content te bekijken waarvoor het account is gerechtigd." + }, + "icon": "zeveniconlight.png", + "poster": "zevenposter.jpg", + "category": "National", + "channelcode": "zevenbe", + "sortorder": 6, + "language": "be", + "fanart": "zevenfanart.jpg", + "adaptiveAddonSelectable": true } ], "settings": [ @@ -68,4 +103,4 @@ "value": "id=\"viervijfzes_refresh_token\" default=\"\"" } ] -} \ No newline at end of file +} diff --git a/plugin.video.retrospect/channels/channel.be/vier/chn_vier.py b/plugin.video.retrospect/channels/channel.be/vier/chn_vier.py index 2579bc3437..d28f8b34c2 100644 --- a/plugin.video.retrospect/channels/channel.be/vier/chn_vier.py +++ b/plugin.video.retrospect/channels/channel.be/vier/chn_vier.py @@ -1,25 +1,46 @@ # SPDX-License-Identifier: GPL-3.0-or-later - import datetime +from typing import Tuple, List, Optional, Union + +import pytz +# noinspection PyUnresolvedReferences +from awsidp import AwsIdp from resources.lib import chn_class, contenttype, mediatype -from resources.lib.helpers.htmlentityhelper import HtmlEntityHelper -from resources.lib.helpers.htmlhelper import HtmlHelper +from resources.lib.actions import keyword, action +from resources.lib.addonsettings import AddonSettings +from resources.lib.helpers.datehelper import DateHelper from resources.lib.helpers.jsonhelper import JsonHelper -from resources.lib.mediaitem import MediaItem +from resources.lib.helpers.languagehelper import LanguageHelper from resources.lib.logger import Logger +from resources.lib.mediaitem import MediaItem, MediaItemResult, FolderItem from resources.lib.regexer import Regexer -from resources.lib.urihandler import UriHandler -from resources.lib.parserdata import ParserData +from resources.lib.retroconfig import Config from resources.lib.streams.m3u8 import M3u8 from resources.lib.streams.mpd import Mpd -from resources.lib.helpers.datehelper import DateHelper -from resources.lib.addonsettings import AddonSettings -from resources.lib.xbmcwrapper import XbmcWrapper -from resources.lib.helpers.languagehelper import LanguageHelper +from resources.lib.urihandler import UriHandler from resources.lib.vault import Vault -# noinspection PyUnresolvedReferences -from awsidp import AwsIdp +from resources.lib.xbmcwrapper import XbmcWrapper + + +class NextJsParser: + def __init__(self, regex: str): + self.__regex = regex + + def __call__(self, data: str) -> Tuple[JsonHelper, List[MediaItem]]: + nextjs_regex = self.__regex + try: + nextjs_data = Regexer.do_regex(nextjs_regex, data)[0] + except: + Logger.debug(f"RAW NextJS: {data}") + raise + + Logger.trace(f"NextJS: {nextjs_data}") + nextjs_json = JsonHelper(nextjs_data) + return nextjs_json, [] + + def __str__(self): + return f"NextJS parser: {self.__regex}" class Channel(chn_class.Channel): @@ -39,142 +60,141 @@ def __init__(self, channel_info): chn_class.Channel.__init__(self, channel_info) - # https://www.goplay.be/api/programs/popular/vier - # https://www.goplay.be/api/epg/vier/2021-01-28 - # setup the main parsing data self.baseUrl = "https://www.goplay.be" + self.httpHeaders = {"rsc": "1"} if self.channelCode == "vijfbe": - self.noImage = "vijfimage.png" - self.mainListUri = "https://www.goplay.be/programmas/play5" + self.noImage = "vijffanart.png" + self.mainListUri = "https://www.goplay.be/programmas/play-5" self.__channel_brand = "play5" self.__channel_slug = "vijf" elif self.channelCode == "zesbe": - self.noImage = "zesimage.png" - self.mainListUri = "https://www.goplay.be/programmas/play6" + self.noImage = "zesfanart.png" + self.mainListUri = "https://www.goplay.be/programmas/play-6" self.__channel_brand = "play6" self.__channel_slug = "zes" + elif self.channelCode == "zevenbe": + self.noImage = "zevenfanart.png" + self.mainListUri = "https://www.goplay.be/programmas/play-7" + self.__channel_brand = "play7" + self.__channel_slug = "zeven" + + elif self.channelCode == "goplay": + self.noImage = "goplayfanart.png" + # self.mainListUri = "https://www.goplay.be/programmas/" + self.mainListUri = "#goplay" + self.__channel_brand = None else: - self.noImage = "vierimage.png" - self.mainListUri = "https://www.goplay.be/programmas/play4" + self.noImage = "vierfanart.png" + self.mainListUri = "https://www.goplay.be/programmas/play-4" self.__channel_brand = "play4" self.__channel_slug = "vier" - episode_regex = r'(data-program)="([^"]+)"' - self._add_data_parser(self.mainListUri, match_type=ParserData.MatchExact, - preprocessor=self.add_specials, - parser=episode_regex, - creator=self.create_episode_item) - - self._add_data_parser("*", match_type=ParserData.MatchExact, - name="Json video items", json=True, - preprocessor=self.extract_hero_data, - parser=["data", "playlists", 0, "episodes"], - creator=self.create_video_item_api) - - video_regex = r']+data-background-image="(?[^"]+)")?[^>]+href="' \ - r'(?/video/[^"]+)"[^>]*>(?:\s+]+>\s+
]+' \ - r'data-background-image="(?[^"]+)")?[\w\W]{0,1000}?' \ - r']*>(?:)?(?[^<]+)(?:</span>)?</h3>(?:\s+' \ - r'(?:<div[^>]*>\s+)?<div[^>]*>[^<]+</div>\s+<div[^>]+data-timestamp=' \ - r'"(?<timestamp>\d+)")?' - video_regex = Regexer.from_expresso(video_regex) - self._add_data_parser("*", match_type=ParserData.MatchExact, - name="Normal video items", - parser=video_regex, - creator=self.create_video_item) - - self._add_data_parser("https://www.goplay.be/api/programs/popular", - name="Special lists", json=True, - parser=[], creator=self.create_episode_item_api) - - self._add_data_parser("#tvguide", name="TV Guide recents", - preprocessor=self.add_recent_items) - - self._add_data_parser("https://www.goplay.be/api/epg/", json=True, - name="EPG items", + self._add_data_parser("#goplay", preprocessor=self.add_specials) + + self._add_data_parser("#recent", preprocessor=self.add_recent_items) + + self._add_data_parser("https://www.goplay.be/programmas/", json=True, + preprocessor=NextJsParser( + r"{\"brand\":\".+?\",\"results\":(.+),\"categories\":")) + + self._add_data_parser("https://www.goplay.be/programmas/", json=True, + preprocessor=self.add_recents, + parser=[], creator=self.create_typed_nextjs_item) + + self._add_data_parser("https://www.goplay.be/", json=True, name="Main show parser", + preprocessor=NextJsParser(r"{\"playlists\":(.+)}\]}\]\]$"), + parser=[], creator=self.create_season_item, + postprocessor=self.show_single_season) + + self._add_data_parser("https://www.goplay.be/tv-gids/", json=True, name="TV Guides", + preprocessor=NextJsParser( + r"children\":(\[\[\"\$\",[^{]+{\"program.+\])}\]\]}\]"), parser=[], creator=self.create_epg_item) - # Generic updater with login + self._add_data_parser("https://api.goplay.be/web/v1/search", json=True, + name="Search results parser", + parser=["hits", "hits"], creator=self.create_search_result) + self._add_data_parser("https://api.goplay.be/web/v1/videos/long-form/", updater=self.update_video_item_with_id) - self._add_data_parser("*", updater=self.update_video_item) + self._add_data_parser("https://www.goplay.be/video/", + updater=self.update_video_item_from_nextjs) + self._add_data_parser("https://www.goplay.be/", + updater=self.update_video_item) # ========================================================================================== # Channel specific stuff self.__idToken = None - self.__meta_playlist = "current_playlist" - self.__no_clips = False + self.__tz = pytz.timezone("Europe/Brussels") # ========================================================================================== # Test cases: - # Documentaire: pages (has http://www.canvas.be/tag/.... url) - # Not-Geo locked: Kroost # ====================================== Actual channel setup STOPS here =================== return - def log_on(self): - """ Logs on to a website, using an url. - - First checks if the channel requires log on. If so and it's not already - logged on, it should handle the log on. That part should be implemented - by the specific channel. - - More arguments can be passed on, but must be handled by custom code. - - After a successful log on the self.loggedOn property is set to True and - True is returned. - - :return: indication if the login was successful. - :rtype: bool - - """ - - if self.__idToken: - return True - - # check if there is a refresh token - # refresh token: viervijfzes_refresh_token - refresh_token = AddonSettings.get_setting("viervijfzes_refresh_token") - client = AwsIdp("eu-west-1_dViSsKM5Y", "6s1h851s8uplco5h6mqh1jac8m", - logger=Logger.instance()) - if refresh_token: - id_token = client.renew_token(refresh_token) - if id_token: - self.__idToken = id_token - return True - else: - Logger.info("Extending token for VierVijfZes failed.") - - # username: viervijfzes_username - username = AddonSettings.get_setting("viervijfzes_username") - # password: viervijfzes_password - v = Vault() - password = v.get_setting("viervijfzes_password") - if not username or not password: - XbmcWrapper.show_dialog( - title=None, - message=LanguageHelper.get_localized_string(LanguageHelper.MissingCredentials), - ) - return False - - id_token, refresh_token = client.authenticate(username, password) - if not id_token or not refresh_token: - Logger.error("Error getting a new token. Wrong password?") - return False - - self.__idToken = id_token - AddonSettings.set_setting("viervijfzes_refresh_token", refresh_token) - return True - - def add_recent_items(self, data): + def add_specials(self, data: JsonHelper) -> Tuple[JsonHelper, List[MediaItem]]: + if self.channelCode != "goplay": + return data, [] + + search_title = LanguageHelper.get_localized_string(LanguageHelper.Search) + search_item = FolderItem(search_title, self.search_url, content_type=contenttype.VIDEOS) + + url_format = f"plugin://{Config.addonId}/?{keyword.CHANNEL}={{}}&{keyword.ACTION}={action.LIST_FOLDER}" + vier = FolderItem("4: Vier", url_format.format("channel.be.vier"), + content_type=contenttype.TVSHOWS) + vier.set_artwork( + poster=self.get_image_location("vierposter.png"), + fanart=self.get_image_location("vierfanart.jpg"), + icon=self.get_image_location("viericon.png") + ) + + vijf = FolderItem("5: Vijf", url_format.format("channel.be.vier-vijfbe"), + content_type=contenttype.TVSHOWS) + vijf.set_artwork( + poster=self.get_image_location("vijfposter.png"), + fanart=self.get_image_location("vijffanart.jpg"), + icon=self.get_image_location("vijficon.png") + ) + + zes = FolderItem("6: Zes", url_format.format("channel.be.vier-zesbe"), + content_type=contenttype.TVSHOWS) + zes.set_artwork( + poster=self.get_image_location("zesposter.png"), + fanart=self.get_image_location("zesfanart.jpg"), + icon=self.get_image_location("zesicon.png") + ) + + zeven = FolderItem("7: Zeven", url_format.format("channel.be.vier-zevenbe"), + content_type=contenttype.TVSHOWS) + zeven.set_artwork( + poster=self.get_image_location("zevenposter.jpg"), + fanart=self.get_image_location("zevenfanart.jpg"), + icon=self.get_image_location("zevenicon.png") + ) + + all_items = FolderItem( + LanguageHelper.get_localized_string(LanguageHelper.TvShows), + "https://www.goplay.be/programmas/", + content_type=contenttype.TVSHOWS + ) + return data, [search_item, vier, vijf, zes, zeven, all_items] + + def add_recents(self, data: JsonHelper) -> Tuple[JsonHelper, List[MediaItem]]: + title = LanguageHelper.get_localized_string(LanguageHelper.Recent) + recent = FolderItem(title, "#recent", content_type=contenttype.TVSHOWS) + recent.dontGroup = True + recent.name = f".: {title} :." + return data, [recent] + + def add_recent_items(self, data: JsonHelper) -> Tuple[JsonHelper, List[MediaItem]]: """ Performs pre-process actions for data processing. - Accepts an data from the process_folder_list method, BEFORE the items are + 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 <data> could be changed and additional items can be created. @@ -203,324 +223,303 @@ def add_recent_items(self, data): day = LanguageHelper.get_localized_string(LanguageHelper.Yesterday) title = "%04d-%02d-%02d - %s" % (air_date.year, air_date.month, air_date.day, day) - url = "https://www.goplay.be/api/epg/{}/{:04d}-{:02d}-{:02d}".\ + url = "https://www.goplay.be/tv-gids/{}/{:04d}-{:02d}-{:02d}".\ format(self.__channel_slug, air_date.year, air_date.month, air_date.day) extra = MediaItem(title, url) extra.complete = True extra.dontGroup = True + extra.HttpHeaders = self.httpHeaders extra.set_date(air_date.year, air_date.month, air_date.day, text="") extra.content_type = contenttype.VIDEOS items.append(extra) return data, items - def add_specials(self, data): - """ Performs pre-process actions for data processing. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - 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 <data> could be changed and additional items can - be created. + This method is called when and item with `self.search_url` is opened. The channel + calling this should implement the search functionality. This could also include + showing of an input keyboard and following actions. - The return values should always be instantiated in at least ("", []). + The %s the url will be replaced with a URL encoded representation of the + text to search for. - :param str data: The retrieve data that was loaded for the current item and URL. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. - :return: A tuple of the data and a list of MediaItems that were generated. - :rtype: tuple[str|JsonHelper,list[MediaItem]] + :return: A list with search results as MediaItems. """ - items = [] - - specials = { - "https://www.goplay.be/api/programs/popular/{}".format(self.__channel_slug): ( - LanguageHelper.get_localized_string(LanguageHelper.Popular), - contenttype.TVSHOWS - ), - "#tvguide": ( - LanguageHelper.get_localized_string(LanguageHelper.Recent), - contenttype.FILES - ) - } - - for url, (title, content) in specials.items(): - item = MediaItem("\a.: {} :.".format(title), url) - item.content_type = content - items.append(item) - - return data, items - - def create_episode_item(self, result_set): - """ Creates a new MediaItem for an episode. + if not needle: + raise ValueError("No needle present") - This method creates a new MediaItem from the Regular Expression or Json - results <result_set>. The method should be implemented by derived classes - and are specific to the channel. + url = f"https://api.goplay.be/web/v1/search" + payload = {"mode": "byDate", "page": 0, "query": needle} + temp = MediaItem("Search", url, mediatype.FOLDER) + temp.postJson = payload + return self.process_folder_list(temp) - :param list[str] result_set: The result_set of the self.episodeItemRegex + def create_typed_nextjs_item(self, result_set: dict) -> MediaItemResult: + item_type = result_set["type"] + item_sub_type = result_set["subtype"] - :return: A new MediaItem of type 'folder'. - :rtype: MediaItem|None - - """ + if item_type == "program": + return self.create_program_typed_item(result_set) + else: + Logger.warning(f"Unknown type: {item_type}:{item_sub_type}") + return None - json_data = result_set[1].replace(""", "\"") - result_set = JsonHelper(json_data) - result_set = result_set.json - return self.create_episode_item_api(result_set) + def create_program_typed_item(self, result_set: dict) -> MediaItemResult: + item_sub_type = result_set["subtype"] + data = result_set.get("data") - def create_episode_item_api(self, result_set): - """ Creates a new MediaItem for an episode. + if not data: + return None - This method creates a new MediaItem from the Regular Expression or Json - results <result_set>. The method should be implemented by derived classes - and are specific to the channel. + brand = data["brandName"].lower() + if self.__channel_brand and brand != self.__channel_brand: + return None - :param list[str] result_set: The result_set of the self.episodeItemRegex + title = data["title"] + path = data["path"] + url = f"{self.baseUrl}{path}" - :return: A new MediaItem of type 'folder'. - :rtype: MediaItem|None + if item_sub_type == "movie": + item = MediaItem(title, url, media_type=mediatype.MOVIE) + # item.metaData["retrospect:parser"] = "movie" + else: + item = FolderItem(title, url, content_type=contenttype.EPISODES) - """ + if "brandName" in data: + item.metaData["brand"] = data["brandName"] + if "categoryName" in data: + item.metaData["category"] = data["categoryName"] + if "parentalRating" in data: + item.metaData["parental"] = data["parentalRating"] - if not isinstance(result_set, dict): - json_data = result_set[1].replace(""", "\"") - result_set = JsonHelper(json_data) - result_set = result_set.json + self.__extract_artwork(item, data.get("images")) + return item - brand = result_set["pageInfo"]["brand"].lower() - if brand != self.__channel_brand: + def create_season_item(self, result_set): + season_item = None + season = result_set.get("season", 0) + + if season: + title = f"{LanguageHelper.get_localized_string(LanguageHelper.SeasonId)} {season}" + season_item = FolderItem(title, result_set["uuid"], content_type=contenttype.EPISODES) + + videos = [] + video_info: dict + for video_info in result_set.get("videos", []): + title = video_info["title"] + url = f"{self.baseUrl}{video_info['path']}" + video_date = video_info["dateCreated"] + description = video_info["description"] + # video_id = video_info["uuid"] + episode = video_info.get("episodeNumber", 0) + + item = MediaItem(title, url, media_type=mediatype.EPISODE) + item.description = description + + self.__extract_artwork(item, video_info.get("images"), set_fanart=False) + + if episode and season: + item.set_season_info(season, episode) + + date_stamp = DateHelper.get_date_from_posix(int(video_date), tz=self.__tz) + item.set_date(date_stamp.year, date_stamp.month, date_stamp.day, date_stamp.hour, + date_stamp.minute, date_stamp.second) + + self.__extract_stream_collection(item, video_info) + videos.append(item) + if season_item: + season_item.items.append(item) + + if season_item: + return season_item + return videos + + # noinspection PyUnusedLocal + def show_single_season(self, data: Union[str, JsonHelper], items: List[MediaItem]) -> List[MediaItem]: + if len(items) == 1 and len(items[0].items) > 0: + Logger.info("Showing the full listing of a single season.") + return items[0].items + return items + + def create_search_result(self, result_set: dict) -> MediaItemResult: + data = result_set["_source"] + + title = data["title"] + url = data["url"] + description = data["intro"] + thumb = data["img"] + video_date = data["created"] + item_type = data["bundle"] + + if item_type == "video": + item = MediaItem(title, url, media_type=mediatype.VIDEO) + elif item_type == "program": + item = FolderItem(title, url, content_type=contenttype.EPISODES) + else: + Logger.warning("Unknown search result type.") return None - title = result_set["title"] - url = "{}{}".format(self.baseUrl, result_set["link"]) - item = MediaItem(title, url) - item.description = result_set.get("description") - item.isGeoLocked = True - - images = result_set["images"] - item.poster = HtmlEntityHelper.convert_html_entities(images.get("poster")) - item.thumb = HtmlEntityHelper.convert_html_entities(images.get("teaser")) + item.description = description + item.set_artwork(thumb=thumb) + date_stamp = DateHelper.get_date_from_posix(int(video_date), tz=self.__tz) + item.set_date(date_stamp.year, date_stamp.month, date_stamp.day, date_stamp.hour, + date_stamp.minute, date_stamp.second) return item - def extract_hero_data(self, data): - """ Extacts the Hero json data - - 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 <data> 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[JsonHelper,list[MediaItem]] - - """ - - Logger.info("Performing Pre-Processing") - items = [] - - hero_data = Regexer.do_regex(r'data-hero="([^"]+)', data)[0] - hero_data = HtmlEntityHelper.convert_html_entities(hero_data) - Logger.trace(hero_data) - hero_json = JsonHelper(hero_data) - hero_playlists = hero_json.get_value("data", "playlists") - if not hero_playlists: - # set an empty object - hero_json.json = {} - - current = self.parentItem.metaData.get("current_playlist", None) - if current == "clips": - Logger.debug("Found 'clips' metadata, only listing clips") - hero_json.json = {} - return hero_json, items - - if current is None: - # Add clips folder - clip_title = LanguageHelper.get_localized_string(LanguageHelper.Clips) - clips = MediaItem("\a.: %s :." % (clip_title,), self.parentItem.url) - clips.metaData[self.__meta_playlist] = "clips" - self.__no_clips = True - items.append(clips) - - # See if there are seasons to show - if len(hero_playlists) == 1: - # first items, list all, except if there is only a single season - Logger.debug("Only one folder playlist found. Listing that one") - return hero_json, items - - if current is None: - # list all folders - for playlist in hero_playlists: - folder = self.create_folder_item(playlist) - items.append(folder) - # clear the json item to prevent further listing - hero_json.json = {} - return hero_json, items - - # list the correct folder - current_list = [lst for lst in hero_playlists if lst["id"] == current] - if current_list: - # we are listing a subfolder, put that one on index 0 and then also - hero_playlists.insert(0, current_list[0]) - self.__no_clips = True - - Logger.debug("Pre-Processing finished") - return hero_json, items - - def create_folder_item(self, result_set): - """ Creates a MediaItem of type 'page' using the result_set from the regex. - - This method creates a new MediaItem from the Regular Expression or Json - results <result_set>. The method should be implemented by derived classes - and are specific to the channel. - - :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex - - :return: A new MediaItem of type 'page'. - :rtype: MediaItem|None - - """ - - folder = MediaItem(result_set["title"], self.parentItem.url) - folder.metaData["current_playlist"] = result_set["id"] - return folder - - def create_video_item_api(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 <result_set>. 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 dict[str,] 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 - - """ + def create_epg_item(self, result_set: dict) -> MediaItemResult: + data = result_set[-1]["program"] + if not data["video"]: + return None - # Could be: title = result_set['episodeTitle'] - title = result_set['title'] - url = "https://api.goplay.be/web/v1/videos/long-form/{}".format(result_set['videoUuid']) - item = MediaItem(title, url) - item.media_type = mediatype.EPISODE - item.description = HtmlHelper.to_text(result_set.get("description").replace(">\r\n", ">")) - item.thumb = result_set["image"] - item.isGeoLocked = result_set.get("isProtected") - - date_time = DateHelper.get_date_from_posix(int(result_set["createdDate"])) - item.set_date(date_time.year, date_time.month, date_time.day, date_time.hour, - date_time.minute, - date_time.second) - - item.set_info_label("duration", result_set["duration"]) - if "episodeNumber" in result_set and "seasonNumber" in result_set: - item.set_season_info(result_set["seasonNumber"], result_set["episodeNumber"]) + title = data["programTitle"] + episode_title = data["episodeTitle"] + time_value = data["timeString"] + if episode_title: + title = f"{time_value} - {title} - {episode_title}" + else: + title = f"{time_value} - {title}" + + description = data["contentEpisode"] + time_stamp = data["timestamp"] + duration = data["duration"] + + video_info = data["video"]["data"] + path = video_info["path"] + video_type = video_info["type"] + if video_type != "video": + Logger.warning(f"Unknown EPG type: {video_type}") + + item = MediaItem(title, f"{self.baseUrl}{path}", media_type=mediatype.EPISODE) + item.description = description + item.set_info_label(MediaItem.LabelDuration, duration) + + # Setting this messes up the sorting. + # episode = data["episodeNr"] + # season = data["season"] + # item.set_season_info(season, episode) + + date_stamp = DateHelper.get_date_from_posix(time_stamp, tz=self.__tz) + item.set_date(date_stamp.year, date_stamp.month, date_stamp.day, date_stamp.hour, + date_stamp.minute, date_stamp.second) + self.__extract_artwork(item, video_info["images"], set_fanart=False) return item - def create_epg_item(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 <result_set>. 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. + # def add_specials(self, data): + # """ Performs pre-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 <data> 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]] + # + # """ + # + # items = [] + # + # specials = { + # "https://www.goplay.be/api/programs/popular/{}".format(self.__channel_slug): ( + # LanguageHelper.get_localized_string(LanguageHelper.Popular), + # contenttype.TVSHOWS + # ), + # "#tvguide": ( + # LanguageHelper.get_localized_string(LanguageHelper.Recent), + # contenttype.FILES + # ) + # } + # + # for url, (title, content) in specials.items(): + # item = MediaItem("\a.: {} :.".format(title), url) + # item.content_type = content + # items.append(item) + # + # return data, items - :param dict[str,] 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 + def log_on(self): + """ Logs on to a website, using an url. - """ + First checks if the channel requires log on. If so and it's not already + logged on, it should handle the log on. That part should be implemented + by the specific channel. - if "video_node" not in result_set: - return None + More arguments can be passed on, but must be handled by custom code. - # Could be: title = result_set['episodeTitle'] - program_title = result_set['program_title'] - episode_title = result_set['episode_title'] - time_value = result_set["time_string"] - if episode_title: - title = "{}: {} - {}".format(time_value, program_title, episode_title) - else: - title = "{}: {}".format(time_value, program_title) - video_info = result_set["video_node"] - url = "{}{}".format(self.baseUrl, video_info["url"]) - - item = MediaItem(title, url) - item.media_type = mediatype.EPISODE - item.description = video_info["description"] - item.thumb = video_info["image"] - item.isGeoLocked = result_set.get("isProtected") - item.set_info_label("duration", video_info["duration"]) - - # 2021-01-27 - time_stamp = DateHelper.get_date_from_string(result_set["date_string"], date_format="%Y-%m-%d") - item.set_date(*time_stamp[0:6]) - - item.set_info_label("duration", result_set["duration"]) - if "episode_nr" in result_set and "season" in result_set \ - and result_set["season"] and "-" not in result_set["season"]: - item.set_season_info(result_set["season"], result_set["episode_nr"]) - return item + After a successful log on the self.loggedOn property is set to True and + True is returned. - def create_video_item(self, result_set): - """ Creates a MediaItem of type 'video' using the result_set from the regex. + :return: indication if the login was successful. + :rtype: bool - This method creates a new MediaItem from the Regular Expression or Json - results <result_set>. 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. + if self.__idToken: + return True - :param list[str]|dict[str,str] result_set: The result_set of the self.episodeItemRegex + # check if there is a refresh token + refresh_token = AddonSettings.get_setting("viervijfzes_refresh_token") + client = AwsIdp("eu-west-1_dViSsKM5Y", "6s1h851s8uplco5h6mqh1jac8m", + logger=Logger.instance()) + if refresh_token: + id_token = client.renew_token(refresh_token) + if id_token: + self.__idToken = id_token + return True + else: + Logger.info("Extending token for VierVijfZes failed.") - :return: A new MediaItem of type 'video' or 'audio' (despite the method's name). - :rtype: MediaItem|None + username = AddonSettings.get_setting("viervijfzes_username") + v = Vault() + password = v.get_setting("viervijfzes_password") + if not username or not password: + XbmcWrapper.show_dialog( + title=None, + message=LanguageHelper.get_localized_string(LanguageHelper.MissingCredentials), + ) + return False - """ + id_token, refresh_token = client.authenticate(username, password) + if not id_token or not refresh_token: + Logger.error("Error getting a new token. Wrong password?") + return False - if self.__no_clips: - return None + self.__idToken = id_token + AddonSettings.set_setting("viervijfzes_refresh_token", refresh_token) + return True - item = chn_class.Channel.create_video_item(self, result_set) + def update_video_item(self, item: MediaItem) -> MediaItem: + data = UriHandler.open(item.url, additional_headers=self.httpHeaders) + list_id = Regexer.do_regex(r"listId\":\"([^\"]+)\"", data)[0] + item.url = f"https://api.goplay.be/web/v1/videos/long-form/{list_id}" + return self.update_video_item_with_id(item) - # Set the correct url - # videoId = resultSet["videoid"] - # item.url = "https://api.goplay.be/web/v1/videos/long-form/%s" % (videoId, ) - time_stamp = result_set.get("timestamp") - if time_stamp: - date_time = DateHelper.get_date_from_posix(int(result_set["timestamp"])) - item.set_date(date_time.year, date_time.month, date_time.day, date_time.hour, - date_time.minute, - date_time.second) + def update_video_item_from_nextjs(self, item: MediaItem) -> MediaItem: + data = UriHandler.open(item.url, additional_headers=self.httpHeaders) + json_data = Regexer.do_regex(r"({\"video\":{.+?})\]}\],", data)[0] + nextjs_json = JsonHelper(json_data) - if not item.thumb and "thumburl2" in result_set and result_set["thumburl2"]: - item.thumb = result_set["thumburl2"] + # See if the NextJS data has stream info. + self.__extract_stream_collection(item, nextjs_json.get_value("video")) - if item.thumb and item.thumb != self.noImage: - item.thumb = HtmlEntityHelper.strip_amp(item.thumb) + # if not streams were included, perhaps this is a drm protected sstream. + if not item.has_streams(): + return self.update_video_item_with_id(item) return item - def update_video_item(self, item): + def update_video_item_with_id(self, item: MediaItem) -> MediaItem: """ Updates an existing MediaItem with more data. Used to update none complete MediaItems (self.complete = False). This @@ -544,77 +543,36 @@ def update_video_item(self, item): Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) - # https://api.goplay.be/web/v1/videos/long-form/c58996a6-9e3d-4195-9ecf-9931194c00bf - # videoId = item.url.split("/")[-1] - # url = "%s/video/v3/embed/%s" % (self.baseUrl, videoId,) - url = item.url - data = UriHandler.open(url) - return self.__update_video(item, data) - - def update_video_item_with_id(self, item): - """ Updates an existing MediaItem with more data. - - Used to update none complete MediaItems (self.complete = False). This - could include opening the item's URL to fetch more data and then process that - data or retrieve it's real media-URL. + # We need to log in + if not self.loggedOn: + self.log_on() - The method should at least: - * cache the thumbnail to disk (use self.noImage if no thumb is available). - * set at least one MediaStream. - * set self.complete = True. - - if the returned item does not have a MediaSteam then the self.complete flag - will automatically be set back to False. - - :param MediaItem item: the original MediaItem that needs updating. + # add authorization header + authentication_header = { + "authorization": "Bearer {}".format(self.__idToken), + "content-type": "application/json" + } - :return: The original item with more data added to it's properties. - :rtype: MediaItem + data = UriHandler.open(item.url, additional_headers=authentication_header, no_cache=True) + json_data = JsonHelper(data) - """ + if json_data.get_value("ssai") is not None: + return self.__get_ssai_streams(item, json_data) - Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) + m3u8_url = json_data.get_value("manifestUrls", "hls") - data = None - return self.__update_video(item, data) + # # If there's no m3u8 URL, try to use a SSAI stream instead + # if m3u8_url is None and json_data.get_value("ssai") is not None: + # return self.__get_ssai_streams(item, json_data) - def __update_video(self, item, data): - if not item.url.startswith("https://api.goplay.be/web/v1/videos/long-form/"): - regex = 'data-video-*id="([^"]+)' - m3u8_url = Regexer.do_regex(regex, data)[-1] - # we either have an URL now or an uuid - else: - m3u8_url = item.url.rsplit("/", 1)[-1] - - if ".m3u8" not in m3u8_url: - Logger.info("Not a direct M3u8 file. Need to log in") - url = "https://api.goplay.be/web/v1/videos/long-form/%s" % (m3u8_url, ) - - # We need to log in - if not self.loggedOn: - self.log_on() - - # add authorization header - authentication_header = { - "authorization": "Bearer {}".format(self.__idToken), - "content-type": "application/json" - } - data = UriHandler.open(url, additional_headers=authentication_header) - json_data = JsonHelper(data) - m3u8_url = json_data.get_value("manifestUrls", "hls") - - # If there's no m3u8 URL, try to use a SSAI stream instead - if m3u8_url is None and json_data.get_value("ssai") is not None: - return self.__get_ssai_streams(item, json_data) - - elif m3u8_url is None and json_data.get_value('message') is not None: - error_message = json_data.get_value('message') - if error_message == "Locked": - # set it for the error statistics - item.isGeoLocked = True - Logger.info("No stream manifest found: {}".format(error_message)) - item.complete = False - return item + if m3u8_url is None and json_data.get_value('message') is not None: + error_message = json_data.get_value('message') + if error_message == "Locked": + # set it for the error statistics + item.isGeoLocked = True + Logger.info("No stream manifest found: {}".format(error_message)) + item.complete = False + return item # Geo Locked? if "/geo/" in m3u8_url.lower(): @@ -624,24 +582,87 @@ def __update_video(self, item, data): item.complete = M3u8.update_part_with_m3u8_streams( item, m3u8_url, channel=self, encrypted=False) + def __extract_stream_collection(self, item, video_info): + stream_collection = video_info.get("streamCollection", {}) + prefer_widevine = True # video_info.get("videoType") != "longForm" + + if stream_collection: + drm_key = stream_collection["drmKey"] + video_id = video_info["uuid"] + if drm_key: + Logger.info(f"Found DRM enabled item: {item.name}") + item.url = f"https://api.goplay.be/web/v1/videos/long-form/{video_id}" + item.isGeoLocked = True + item.isDrmProtected = True + item.metaData["drm"] = drm_key + return + + streams = stream_collection["streams"] + duration = stream_collection["duration"] + if duration: + item.set_info_label(MediaItem.LabelDuration, duration) + for stream in streams: + proto = stream["protocol"] + stream_url = stream["url"] + if proto == "dash": + stream = item.add_stream(stream_url, 1550 if prefer_widevine else 1450) + Mpd.set_input_stream_addon_input(stream) + elif proto == "hls": + stream = item.add_stream(stream_url, 1500) + M3u8.set_input_stream_addon_input(stream) + + if "/geo" in stream_url: + item.isGeoLocked = True + + item.complete = item.has_streams() + + def __extract_artwork(self, item: MediaItem, images: dict, set_fanart: bool = True): + if not images: + return + + if "poster" in images: + item.poster = images["poster"] + if "default" in images: + item.thumb = images["default"] + if set_fanart: + item.fanart = images["default"] + elif "posterLandscape" in images: + item.thumb = images["posterLandscape"] + if set_fanart: + item.fanart = images["posterLandscape"] + def __get_ssai_streams(self, item, json_data): Logger.info("No stream data found, trying SSAI data") content_source_id = json_data.get_value("ssai", "contentSourceID") video_id = json_data.get_value("ssai", "videoID") + drm_header = json_data.get_value("drmXml", fallback=None) streams_url = 'https://dai.google.com/ondemand/dash/content/{}/vid/{}/streams'.format( content_source_id, video_id) + # streams_url = "https://pubads.g.doubleclick.net/ondemand/dash/content/{}/vid/{}/streams".format( + # content_source_id, video_id) + streams_input_data = { "api-key": "null" + # "api-key": item.metaData.get("drm", "null") } streams_headers = { "content-type": "application/json" } - data = UriHandler.open(streams_url, data=streams_input_data, additional_headers=streams_headers) + data = UriHandler.open(streams_url, data=streams_input_data, + additional_headers=streams_headers, no_cache=True) json_data = JsonHelper(data) mpd_url = json_data.get_value("stream_manifest") stream = item.add_stream(mpd_url, 0) - Mpd.set_input_stream_addon_input(stream) + + if drm_header: + header = {"customdata": drm_header, "content-type": "application/octet-stream"} + license_key = Mpd.get_license_key( + "https://wv-keyos.licensekeyserver.com/", key_type="R", + key_headers=header) + Mpd.set_input_stream_addon_input(stream, license_key=license_key) + else: + Mpd.set_input_stream_addon_input(stream) item.complete = True return item diff --git a/plugin.video.retrospect/channels/channel.be/vtmbe/chn_vtmbe.py b/plugin.video.retrospect/channels/channel.be/vtmbe/chn_vtmbe.py index 09f0533c9e..0a9d7848e5 100644 --- a/plugin.video.retrospect/channels/channel.be/vtmbe/chn_vtmbe.py +++ b/plugin.video.retrospect/channels/channel.be/vtmbe/chn_vtmbe.py @@ -4,6 +4,7 @@ import random import time import datetime +from typing import Optional, List from resources.lib import chn_class, mediatype from resources.lib.helpers.htmlhelper import HtmlHelper @@ -359,7 +360,7 @@ def stievie_menu(self, data): # programs.dontGroup = True # items.append(programs) # - # search = MediaItem("Zoeken", "searchSite") + # search = MediaItem("Zoeken", self.search_url) # search.complete = True # search.dontGroup = True # items.append(search) @@ -647,26 +648,29 @@ def stievie_create_episode(self, result_set): return item # endregion - def search_site(self, url=None): # @UnusedVariable - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + # nieuws url = "https://vod.medialaan.io/vod/v2/programs?query=%s" - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def add_live_channel_and_fetch_all_data(self, data): """ Preprocesses that data and adds live channels and fetches al related data via extra diff --git a/plugin.video.retrospect/channels/channel.mtg/tvse/chn_tvse.py b/plugin.video.retrospect/channels/channel.mtg/tvse/chn_tvse.py index 0f9feea539..b0a9e6debc 100644 --- a/plugin.video.retrospect/channels/channel.mtg/tvse/chn_tvse.py +++ b/plugin.video.retrospect/channels/channel.mtg/tvse/chn_tvse.py @@ -1,4 +1,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later +from typing import Optional, List + from resources.lib import chn_class, mediatype from resources.lib.mediaitem import MediaItem @@ -136,7 +138,7 @@ def add_search(self, data): title = "\a.: %s :." % (self.searchInfo.get(self.language, self.searchInfo["se"])[1], ) Logger.trace("Adding search item: %s", title) - search_item = MediaItem(title, "searchSite") + search_item = MediaItem(title, self.search_url) search_item.dontGroup = True items.append(search_item) @@ -214,28 +216,31 @@ def update_video_item(self, item): return item - def search_site(self, url=None): - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str|None url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + # https://tvplay.tv3.lt/paieska/Lietuvos%20talentai%20/ # https://tvplay.tv3.ee/otsi/test%20test%20/ url = self.__get_search_url() url = "{0}/%s/".format(url) - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def __get_search_url(self): """ Generates a search url for the channel using the information for that channel. diff --git a/plugin.video.retrospect/channels/channel.mtg/viafree/chn_viafree.py b/plugin.video.retrospect/channels/channel.mtg/viafree/chn_viafree.py index 88e12d30ba..8e459a94f6 100644 --- a/plugin.video.retrospect/channels/channel.mtg/viafree/chn_viafree.py +++ b/plugin.video.retrospect/channels/channel.mtg/viafree/chn_viafree.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import datetime +from typing import Optional, List from resources.lib import chn_class, mediatype @@ -378,30 +379,33 @@ def add_search(self, data): title = "\a.: %s :." % (self.searchInfo.get(self.language, self.searchInfo["se"])[1], ) Logger.trace("Adding search item: %s", title) - search_item = MediaItem(title, "searchSite") + search_item = MediaItem(title, self.search_url) search_item.dontGroup = True items.append(search_item) Logger.debug("Pre-Processing finished") return data, items - def search_site(self, url=None): - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str|None url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + # https://playapi.mtgx.tv/v3/search?term=nyheter&limit=20&columns=formats&with=format&device=web&include_prepublished=1&country=se&page=1 url = "https://playapi.mtgx.tv/v3/search?term=%s&limit=50&columns=formats&with=format" \ @@ -414,7 +418,7 @@ def search_site(self, url=None): # url = "%s/api/playClient;isColumn=true;query=%s;resource=search?returnMeta=true" % (baseUrl, query) Logger.debug("Using search url: %s", url) - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def create_page_item(self, result_set): """ Creates a MediaItem of type 'page' using the result_set from the regex. diff --git a/plugin.video.retrospect/channels/channel.no/nrkno/chn_nrkno.py b/plugin.video.retrospect/channels/channel.no/nrkno/chn_nrkno.py index 396b236130..e19a55d46c 100644 --- a/plugin.video.retrospect/channels/channel.no/nrkno/chn_nrkno.py +++ b/plugin.video.retrospect/channels/channel.no/nrkno/chn_nrkno.py @@ -181,7 +181,7 @@ def create_main_list(self, data): "Recent": "https://psapi.nrk.no/medium/tv/recentlysentprograms?maxnumber=100&startRow=0&apiKey={}".format(self.__api_key), "Categories": "https://psapi.nrk.no/tv/pages?apiKey={}".format(self.__api_key), "A - Å": "https://psapi.nrk.no/medium/tv/letters?apiKey={}".format(self.__api_key), - "Søk": "#searchSite" + "Søk": self.search_url } for name, url in links.items(): item = MediaItem(name, url) @@ -192,25 +192,28 @@ def create_main_list(self, data): Logger.debug("Pre-Processing finished") return data, items - def search_site(self, url=None): - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + url = "https://psapi.nrk.no/tv/titleSearch/global?q=%s&apiKey={}".format(self.__api_key) - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def create_alpha_item(self, result_set): """ Creates a MediaItem of type 'folder' using the Alpha chars available. It uses diff --git a/plugin.video.retrospect/channels/channel.nos/bvntv/chn_bvntv.py b/plugin.video.retrospect/channels/channel.nos/bvntv/chn_bvntv.py index b02e142a7e..02fd2482d3 100644 --- a/plugin.video.retrospect/channels/channel.nos/bvntv/chn_bvntv.py +++ b/plugin.video.retrospect/channels/channel.nos/bvntv/chn_bvntv.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later from resources.lib import chn_class, mediatype -from resources.lib.helpers.datehelper import DateHelper from resources.lib.helpers.jsonhelper import JsonHelper from resources.lib.helpers.languagehelper import LanguageHelper from resources.lib.logger import Logger @@ -32,13 +31,18 @@ def __init__(self, channel_info): self.poster = "bvntvposter.png" self.mainListUri = "https://www.bvn.tv/programmas/" - episode_regex = r'<a[^>]*href="(?<url>[^"]+)[^>]*>\W*<img[^>]*data-src="(?<thumburl>[^"]+)"[^>]*>\W*<div[^>]*>(?<title>[^<]+)<' + episode_regex = (r'<a[^>]*href="(?<url>[^"]+)[^>]*>\W*<figure class="media">\W*<img[^>]*' + r'data-src="(?<thumburl>[^"]+)"[\w\W]{0,500}?<h4[^>]+>\W+' + r'(?<title>[^<]+?)[\W]+<') episode_regex = Regexer.from_expresso(episode_regex) self._add_data_parser(self.mainListUri, name="Mainlist and live", preprocessor=self.add_live_streams, parser=episode_regex, creator=self.create_episode_item) - video_regex = r'<a[^>]+href="(?<url>[^"]+/(?<pow>[^"]+))"[^>]*>\W*<div[^>]+>\W*<img[^>]+data-src="(?<thumburl>[^"]+)"[\w\W]{0,1000}?title">(?<title>[^<]+)<[^<]+<[^>]+>(?<subtitle>[^<]*)<[^<]+<[^>]+datetime="(?<datetime>[^"]+)"' + video_regex = (r'<a[^>]+href="(?<url>[^"]+/(?<pow>[^"]+))"[^>]*>\W*<figure[^>]+>\W+' + r'<img[^>]+src="(?<thumburl>[^"]+)"[\w\W]{0,1000}?<h4[^>]*>\W+' + r'(?<title>[^<]+?)\W+</h4>\W*<p.+class="time">\W+\w+ (?<datetime>[^"]+?)' + r'\W+</p>') video_regex = Regexer.from_expresso(video_regex) self._add_data_parser("https://www.bvn.tv/programma/", name="Main video listings for shows", preprocessor=self.extract_episode_section, @@ -59,8 +63,8 @@ def extract_episode_section(self, data): Logger.info("Performing Pre-Processing") items = [] - data = data.split("slick-missed-program", 1)[1] - data = data.split("</section>", 1)[0] + data = data.split("<!-- episodes -->", 1)[1] + data = data.split("<!-- footer -->", 1)[0] Logger.debug("Pre-Processing finished") return data, items @@ -109,9 +113,9 @@ def create_video_item(self, result_set): if not item.url.endswith("/"): item.url = "{}/".format(item.url) - date_value = result_set["datetime"] - date_time = DateHelper.get_date_from_string(date_value, "%Y-%m-%d %H:%M:%S") - item.set_date(*date_time[0:6]) + # date_value = result_set["datetime"] + # date_time = DateHelper.get_date_from_string(date_value, "% Y-%m-%d %H:%M:%S") + # item.set_date(*date_time[0:6]) item.thumb = "{}{}".format(self.baseUrl, result_set["thumburl"]) item.metaData["pow"] = result_set["pow"] diff --git a/plugin.video.retrospect/channels/channel.nos/nos2010/chn_nos2010.py b/plugin.video.retrospect/channels/channel.nos/nos2010/chn_nos2010.py index 45dceee4d1..77bb784275 100644 --- a/plugin.video.retrospect/channels/channel.nos/nos2010/chn_nos2010.py +++ b/plugin.video.retrospect/channels/channel.nos/nos2010/chn_nos2010.py @@ -317,7 +317,7 @@ def add_item(language_id: int, url: str, content_type: str, items.append(item) return item - add_item(LanguageHelper.Search, "searchSite", contenttype.EPISODES, + add_item(LanguageHelper.Search, self.search_url, contenttype.EPISODES, headers={"X-Requested-With": "XMLHttpRequest"}) # Favorite items that require login @@ -498,6 +498,7 @@ def create_api_program_item(self, result_set: dict) -> Optional[MediaItem]: url = f"https://npo.nl/start/api/domain/programs-by-series?seriesGuid={guid}&limit=20&sort=-firstBroadcastDate" item = FolderItem(title, url, content_type=contenttype.EPISODES) + # Store the series GUID as we need it later one. item.metaData["guid"] = guid if "images" in result_set and result_set["images"]: image_data = result_set["images"][0] @@ -562,8 +563,8 @@ def create_api_episode_item_with_data(self, result_set: dict) -> Optional[MediaI return self.create_api_episode_item(result_set, True) - def create_api_episode_item(self, result_set: dict, show_info: bool = False) -> Optional[ - MediaItem]: + def create_api_episode_item(self, result_set: dict, show_info: bool = False) -> ( + Optional)[MediaItem]: title = result_set["title"] poms = result_set["productId"] serie_info = result_set.get("series") or {} @@ -624,7 +625,7 @@ def create_api_episode_item(self, result_set: dict, show_info: bool = False) -> return item - + # noinspection PyUnusedLocal def create_api_live_tv(self, result_set: dict, show_info: bool = False) -> Optional[MediaItem]: """ Creates a MediaItem for a live item of type 'video' using the result_set from the regex. @@ -652,6 +653,7 @@ def create_api_live_tv(self, result_set: dict, show_info: bool = False) -> Optio item = MediaItem(name, url, media_type=mediatype.VIDEO) item.metaData["poms"] = poms item.metaData["live_pid"] = poms + # Store the series GUID as we need it later. item.metaData["guid"] = guid item.isLive = True item.isGeoLocked = True @@ -697,6 +699,7 @@ def create_epg_days(self, data: Union[str, JsonHelper]) -> Tuple[Union[str, Json return data, items + # noinspection PyUnusedLocal def load_all_epg_channels(self, data: Union[str, JsonHelper]) -> Tuple[Union[str, JsonHelper], List[MediaItem]]: channels = self.parentItem.metaData["channels"] date = self.parentItem.metaData["date"] @@ -795,35 +798,31 @@ def update_epg_series_item(self, item: MediaItem) -> MediaItem: return self.__update_video_item(item, product_id) # noinspection PyUnusedLocal - def search_site(self, url=None) -> List[MediaItem]: # @UnusedVariable - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + shows_url = "https://npo.nl/start/api/domain/search-results?searchType=series&query=%s&subscriptionType=anonymous" videos_url = "https://npo.nl/start/api/domain/search-results?searchType=broadcasts&query=%s&subscriptionType=anonymous" items = [] - needle = XbmcWrapper.show_key_board() - if not needle: - return [] - - Logger.debug("Searching for '%s'", needle) - # convert to HTML needle = HtmlEntityHelper.url_encode(needle) - search_url = shows_url % (needle, ) temp = MediaItem("Search", search_url, mediatype.FOLDER) items += self.process_folder_list(temp) @@ -1062,8 +1061,6 @@ def create_live_radio(self, result_set: dict) -> Optional[MediaItem]: item.metaData["retrospect:parser"] = "liveRadio" return item - - def update_live_radio(self, item: MediaItem) -> MediaItem: # First fetch the Javascript data file www_data = UriHandler.open(item.url) @@ -1245,9 +1242,8 @@ def create_iptv_epg(self, parameter_parser): iptv_epg_item["image"] = JsonHelper.get_from(item, "images")[0].get("url") if media_item is not None: - iptv_epg_item["stream"] = parameter_parser.create_action_url(self, action=action.PLAY_VIDEO, - item=media_item, - store_id=parent.guid) + iptv_epg_item["stream"] = parameter_parser.create_action_url( + self, action=action.PLAY_VIDEO, item=media_item, store_id=parent.guid) media_items.append(media_item) iptv_epg[livestream["guid"]].append(iptv_epg_item) diff --git a/plugin.video.retrospect/channels/channel.rtlnl/rtl/chn_rtl.py b/plugin.video.retrospect/channels/channel.rtlnl/rtl/chn_rtl.py index bea9ae41ad..bb3b25d6a0 100644 --- a/plugin.video.retrospect/channels/channel.rtlnl/rtl/chn_rtl.py +++ b/plugin.video.retrospect/channels/channel.rtlnl/rtl/chn_rtl.py @@ -142,7 +142,7 @@ def add_recent_items(self, data): recent.items.append(extra) - news = FolderItem("\a .: Zoeken :.", "#searchSite", content_type=contenttype.NONE) + news = FolderItem("\a .: Zoeken :.", self.search_url, content_type=contenttype.NONE) news.complete = True news.dontGroup = True items.append(news) @@ -303,25 +303,28 @@ def create_video_item(self, result_set: Dict[str, Any], recent_listing: bool = F return item - def search_site(self, url: Optional[str]=None) -> List[MediaItem]: - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + url = "https://api.rtl.nl/rtlxl/search/api/search?query=%s" - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def update_video_item(self, item): """ Updates an existing MediaItem with more data. diff --git a/plugin.video.retrospect/channels/channel.sbsnl/kijknl/chn_kijknl.py b/plugin.video.retrospect/channels/channel.sbsnl/kijknl/chn_kijknl.py index 9f46a9d831..eee50fe016 100644 --- a/plugin.video.retrospect/channels/channel.sbsnl/kijknl/chn_kijknl.py +++ b/plugin.video.retrospect/channels/channel.sbsnl/kijknl/chn_kijknl.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later import datetime +from typing import List, Optional + import pytz import urllib.parse @@ -121,20 +123,20 @@ def __init__(self, channel_info): return # noinspection PyUnusedLocal - def search_site(self, url=None): # @UnusedVariable + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ @@ -144,7 +146,7 @@ def search_site(self, url=None): # @UnusedVariable "imageMedia{url,label},type,sources{type,file,drm},seasonNumber," "tvSeasonEpisodeNumber,series{title},lastPubDate}}" ).replace("%", "%%").replace("----", "%s") - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def list_dates(self, data): """ Generates a list of the past week days. @@ -509,7 +511,7 @@ def add_graphql_extras(self, data): items.append(recent) search_title = LanguageHelper.get_localized_string(LanguageHelper.Search) - search = FolderItem("\a.: {} :.".format(search_title), "#searchSite", + search = FolderItem("\a.: {} :.".format(search_title), self.search_url, content_type=contenttype.EPISODES) search.dontGroup = True items.append(search) diff --git a/plugin.video.retrospect/channels/channel.se/oppetarkiv/chn_oppetarkiv.py b/plugin.video.retrospect/channels/channel.se/oppetarkiv/chn_oppetarkiv.py index f71f92d04c..6af8b4d735 100644 --- a/plugin.video.retrospect/channels/channel.se/oppetarkiv/chn_oppetarkiv.py +++ b/plugin.video.retrospect/channels/channel.se/oppetarkiv/chn_oppetarkiv.py @@ -3,6 +3,7 @@ import os import re +from typing import Optional, List from resources.lib import chn_class from resources.lib.retroconfig import Config @@ -87,7 +88,7 @@ def add_search_and_genres(self, data): Logger.debug("Parsing a specific genre: %s", self.__genre) return data, items - search_item = MediaItem("\a.: Sök :.", "searchSite") + search_item = MediaItem("\a.: Sök :.", self.search_url) search_item.complete = True search_item.dontGroup = True # search_item.set_date(2099, 1, 1, text="") @@ -115,25 +116,28 @@ def add_search_and_genres(self, data): Logger.debug("Pre-Processing finished") return data, items - def search_site(self, url=None): # @UnusedVariable - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + url = "http://www.oppetarkiv.se/sok/?q=%s" - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def create_page_item(self, result_set): """ Creates a MediaItem of type 'page' using the result_set from the regex. diff --git a/plugin.video.retrospect/channels/channel.se/sbs/chn_sbs.py b/plugin.video.retrospect/channels/channel.se/sbs/chn_sbs.py index 7f68d046a5..f1021a59b7 100644 --- a/plugin.video.retrospect/channels/channel.se/sbs/chn_sbs.py +++ b/plugin.video.retrospect/channels/channel.se/sbs/chn_sbs.py @@ -4,6 +4,7 @@ import uuid import time import datetime +from typing import Optional, List from resources.lib import chn_class, mediatype from resources.lib.mediaitem import MediaItem @@ -290,7 +291,7 @@ def load_programs(self, data): live.isLive = True items.append(live) - search = MediaItem("\a.: Sök :.", "searchSite") + search = MediaItem("\a.: Sök :.", self.search_url) search.dontGroup = True items.append(search) @@ -381,23 +382,26 @@ def create_page_item(self, result_set): item = MediaItem(title, url) return item - def search_site(self, url=None): - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + if self.primaryChannelId: shows_url = "https://{0}/content/shows?" \ "include=genres%%2Cimages%%2CprimaryChannel.images&" \ @@ -421,21 +425,17 @@ def search_site(self, url=None): "page%%5Bsize%%5D={1}&query=%s" \ .format(self.baseUrlApi, self.videoPageSize) - needle = XbmcWrapper.show_key_board() - if needle: - Logger.debug("Searching for '%s'", needle) - needle = HtmlEntityHelper.url_encode(needle) - - search_url = videos_url % (needle, ) - temp = MediaItem("Search", search_url) - episodes = self.process_folder_list(temp) + Logger.debug("Searching for '%s'", needle) + needle = HtmlEntityHelper.url_encode(needle) - search_url = shows_url % (needle, ) - temp = MediaItem("Search", search_url) - shows = self.process_folder_list(temp) - return shows + episodes + search_url = videos_url % (needle, ) + temp = MediaItem("Search", search_url) + episodes = self.process_folder_list(temp) - return [] + search_url = shows_url % (needle, ) + temp = MediaItem("Search", search_url) + shows = self.process_folder_list(temp) + return shows + episodes def create_video_item_with_show_title(self, result_set): """ Creates a MediaItem of type 'video' using the result_set from the regex. These items diff --git a/plugin.video.retrospect/channels/channel.se/svt/chn_svt.json b/plugin.video.retrospect/channels/channel.se/svt/chn_svt.json index 2124bb4b70..bcd96bb743 100644 --- a/plugin.video.retrospect/channels/channel.se/svt/chn_svt.json +++ b/plugin.video.retrospect/channels/channel.se/svt/chn_svt.json @@ -8,6 +8,7 @@ "sv": "Sändningar från Sveriges Television (SVTPlay.se)." }, "icon": "svtlarge.png", + "poster": "svtposter.png", "category": "National", "channelcode": "svt", "sortorder": 0, diff --git a/plugin.video.retrospect/channels/channel.se/svt/chn_svt.py b/plugin.video.retrospect/channels/channel.se/svt/chn_svt.py index 2919939708..447ae61965 100644 --- a/plugin.video.retrospect/channels/channel.se/svt/chn_svt.py +++ b/plugin.video.retrospect/channels/channel.se/svt/chn_svt.py @@ -197,7 +197,7 @@ def add_live_items_and_genres(self, data): False), LanguageHelper.get_localized_string(LanguageHelper.Search): ( - "searchSite", False), + self.search_url, False), LanguageHelper.get_localized_string(LanguageHelper.Recent): ( self.__get_api_url( @@ -1071,23 +1071,26 @@ def create_channel_item(self, channel): channel["episodeThumbnailIds"][0],) return channel_item - def search_site(self, url=None): # @UnusedVariable - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + url = self.__get_api_url( "AutoCompleteSearch", "8989b62115022fda8a6129d0f512b94f4d2de3bf2110415647c09c4316ce4a91", {"querystring": "----", "searchClickHistory": []} @@ -1097,7 +1100,7 @@ def search_site(self, url=None): # @UnusedVariable "https://contento-search.svt.se/graphql") url = url.replace("%", "%%") url = url.replace("----", "%s") - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def extract_json_data(self, data): """ Extracts JSON data from the HTML for __svtplay and __reduxStore json data. @@ -1217,7 +1220,7 @@ def update_video_html_item(self, item): """ data = UriHandler.open(item.url) - video_id = Regexer.do_regex(r'play-button"[^>]+href="/video/[^?]+\?id=([^"]+)', data)[0] + video_id = Regexer.do_regex(r'play-button"[^>]+href="/video/([^/]+)/', data)[0] item.url = "https://api.svt.se/video/{}".format(video_id) return self.update_video_api_item(item) diff --git a/plugin.video.retrospect/channels/channel.se/tv4se/chn_tv4se.py b/plugin.video.retrospect/channels/channel.se/tv4se/chn_tv4se.py index c9afd0d08a..a6890c5636 100644 --- a/plugin.video.retrospect/channels/channel.se/tv4se/chn_tv4se.py +++ b/plugin.video.retrospect/channels/channel.se/tv4se/chn_tv4se.py @@ -201,7 +201,7 @@ def __create_item(lang_id: int, url: str, json: Optional[dict] = None): use_get=True) items.append(__create_item(LanguageHelper.TvShows, tvshow_url, tvshow_data)) - recent_url, recent_data = self.__get_api_query("Panel", {"panelId": "1pDPvWRfhEg0wa5SvlP28N", "limit": self.__max_page_size, "offset": 0}) + recent_url, recent_data = self.__get_api_query("Panel", {"panelId": "5qGBwTBSPdxO55EzbpOeNg", "limit": self.__max_page_size, "offset": 0}) items.append(__create_item(LanguageHelper.Recent, recent_url, recent_data)) popular_url, popular_data = self.__get_api_query("Panel", {"panelId": "4nNp00Z12bjEiNboaW2uxB", "limit": self.__max_page_size, "offset": 0}) @@ -213,7 +213,7 @@ def __create_item(lang_id: int, url: str, json: Optional[dict] = None): category_url, json_data = self.__get_api_query("PageList", {"pageListId": "categories"}) items.append(__create_item(LanguageHelper.Categories, category_url, json_data)) - items.append(__create_item(LanguageHelper.Search, "searchSite")) + items.append(__create_item(LanguageHelper.Search, self.search_url)) return data, items def fetch_mainlist_pages(self, data: str) -> Tuple[str, List[MediaItem]]: @@ -516,31 +516,31 @@ def check_for_seasons(self, data: JsonHelper, items: List[MediaItem]) -> List[Me self.parentItem.postJson = data return self.process_folder_list(self.parentItem) - def search_site(self, url: Optional[str] = None) -> List[MediaItem]: + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str|None url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ - needle = XbmcWrapper.show_key_board() if not needle: - return [] + raise ValueError("No needle present") variables = {"input": {"query": needle}, "limit": 10, "offset": 0, - "shouldFetchMovieSeries": True, "shouldFetchMovieSeriesUpsell": True, - "shouldFetchClip": True, "shouldFetchPage": True, "shouldFetchSportEvent": True, - "shouldFetchSportEventUpsell": True} + "shouldFetchMovieSeries": True, "shouldFetchMovieSeriesUpsell": True, + "shouldFetchClip": True, "shouldFetchPage": True, + "shouldFetchSportEvent": True, + "shouldFetchSportEventUpsell": True} url, data = self.__get_api_query("PanelSearch", variables) search = MediaItem("Search", url, mediatype.FOLDER) diff --git a/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.json b/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.json index bfb39ee947..317286f3b3 100644 --- a/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.json +++ b/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.json @@ -8,6 +8,7 @@ "sv": "Sändningar från Sveriges Utbildningsradio (URPlay.se)." }, "icon": "urplaylarge.png", + "poster": "urplayposter.jpg", "category": "National", "channelcode": null, "sortorder": 2, diff --git a/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.py b/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.py index a47638a119..fbb67c29f4 100644 --- a/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.py +++ b/plugin.video.retrospect/channels/channel.se/urplay/chn_urplay.py @@ -1,5 +1,7 @@ # coding=utf-8 # NOSONAR # SPDX-License-Identifier: GPL-3.0-or-later +from typing import Optional, List, Tuple + import pytz from resources.lib import chn_class, mediatype, contenttype @@ -7,7 +9,7 @@ from resources.lib.helpers.languagehelper import LanguageHelper from resources.lib.helpers.subtitlehelper import SubtitleHelper -from resources.lib.mediaitem import MediaItem, FolderItem +from resources.lib.mediaitem import MediaItem, FolderItem, MediaItemResult from resources.lib.parserdata import ParserData from resources.lib.regexer import Regexer from resources.lib.logger import Logger @@ -28,10 +30,13 @@ def __init__(self, channel_info): """ + # https://media-api.urplay.se/config-streaming/v1/urplay/sources/194128 + + self.__build_version = None chn_class.Channel.__init__(self, channel_info) # ==== Actual channel setup STARTS here and should be overwritten from derived classes ==== - self.noImage = "urplayimage.png" + self.noImage = "urplayimage.jpg" # setup the urls self.mainListUri = "#mainlist_merge" @@ -44,34 +49,36 @@ def __init__(self, channel_info): match_type=ParserData.MatchExact, preprocessor=self.merge_add_categories_and_search) - self._add_data_parser("#tvshows", preprocessor=self.merge_tv_show, - name="Main listing of merged TV Shows.", json=True, - parser=["results"], creator=self.create_episode_json_item) - - self._add_data_parser("https://urplay.se/api/v1/series", json=True, - name="Main serie content parser", - parser=["programs"], creator=self.create_video_item_json) - - self._add_data_parser("https://urplay.se/api/v1/series", json=True, - name="Main serie season parser", - parser=["seasonLabels"], creator=self.create_season_item, + self._add_data_parser("#tvshows", json=True, match_type=ParserData.MatchExact, + name="Main listing of merged TV Shows.", + preprocessor=self.load_az_listing, + parser=["pageProps", "alphabeticProductGroups"], + creator=self.create_az_items) + + self._add_data_parser("/serie/", json=True, match_type=ParserData.MatchContains, + name="Processor of shows in a TV serie", + parser=["pageProps", "accessibleEpisodes"], + creator=self.create_video_item) + + self._add_data_parser("/serie/", json=True, match_type=ParserData.MatchContains, + name="Processor of seasons for a TV serie", + parser=["pageProps", "superSeriesSeasons"], + creator=self.create_season_item, postprocessor=self.check_seasons) - # Match Videos (programs) self._add_data_parser("https://urplay.se/api/v1/search?product_type=program", name="Most viewed", json=True, - parser=["results"], creator=self.create_video_item_json_with_show_title) + parser=["results"], creator=self.create_video_item_with_show_title) - self._add_data_parser("*", json=True, - name="Json based video parser", - parser=["accessibleEpisodes"], - creator=self.create_video_item_json) + # self._add_data_parser("*", json=True, + # name="Json based video parser", + # parser=["accessibleEpisodes"], + # creator=self.create_video_item_json) self._add_data_parser("*", updater=self.update_video_item) # Categories - cat_reg = r'<a[^>]+href="(?<url>/blad[^"]+/(?<slug>[^"]+))"[^>]*>' \ - r'(?:<svg[\w\W]{0,2000}?</svg>)?(?<title>[^<]+)<' + cat_reg = r'<a[^>]+href="(?<url>/blad[^"]+/(?<slug>[^"]+))"[^>]*><div[^>]+>(?<title>[^<]+)<' cat_reg = Regexer.from_expresso(cat_reg) self._add_data_parser("https://urplay.se/", name="Category parser", match_type=ParserData.MatchExact, @@ -97,6 +104,7 @@ def __init__(self, channel_info): #=========================================================================================== # non standard items self.__videoItemFound = False + self.__build_version = None # There is either a slug lookup or an url lookup self.__cateogory_slugs = { @@ -290,6 +298,32 @@ def __init__(self, channel_info): # ====================================== Actual channel setup STOPS here =================== return + @property + def build_version(self) -> str: + if not self.__build_version: + data = UriHandler.open("https://urplay.se") + build_version = Regexer.do_regex(r"<script src=\"[^\"]+/([^/]+)/_buildManifest.js\"", data)[0] + Logger.info(f"Found build version: {build_version}") + self.__build_version = build_version + + return self.__build_version + + # noinspection PyUnusedLocal + def load_az_listing(self, data: str) -> Tuple[str, List[MediaItem]]: + # Load it here, to prevent the `self.build_version` to start unwanted. + data = UriHandler.open(f"https://urplay.se/_next/data/{self.build_version}/bladdra/alla-program.json") + return data, [] + + def create_az_items(self, result_set: dict) -> MediaItemResult: + items = [] + for k, result_sets in result_set.items(): + for r in result_sets: + item = self.create_episode_item(r) + if item: + items.append(item) + + return items + def merge_category_items(self, data): """ Merge the multipage category result items into a single list. @@ -365,7 +399,7 @@ def merge_add_categories_and_search(self, data): LanguageHelper.MostRecentEpisodes: "https://urplay.se/api/v1/search?product_type=program&rows={}&start=0&view=published".format(max_items_per_page), LanguageHelper.LastChance: "https://urplay.se/api/v1/search?product_type=program&rows={}&start=0&view=last_chance".format(max_items_per_page), LanguageHelper.Categories: "https://urplay.se/", - LanguageHelper.Search: "searchSite", + LanguageHelper.Search: self.search_url, LanguageHelper.TvShows: "#tvshows" } @@ -379,52 +413,55 @@ def merge_add_categories_and_search(self, data): Logger.debug("Pre-Processing finished") return data, items - def merge_tv_show(self, data): - """ Adds some generic items such as search and categories to the main listing. - - 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]] - - """ - - # merge the main list items: - # https://urplay.se/api/v1/search?product_type=series&response_type=limited&rows=20&sort=title&start=20 - max_items_per_page = 20 - main_list_pages = int(self._get_setting("mainlist_pages")) - data = self.__iterate_results( - "https://urplay.se/api/v1/search?product_type=series&response_type=limited&rows={}&sort=published&start={}", - results_per_page=max_items_per_page, - max_iterations=main_list_pages, - use_pb=True - ) - - return data, [] - - def search_site(self, url=None): - """ Creates an list of items by searching the site. - - This method is called when the URL of an item is "searchSite". The channel + # def merge_tv_show(self, data): + # """ Adds some generic items such as search and categories to the main listing. + # + # 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]] + # + # """ + # + # # merge the main list items: + # # https://urplay.se/api/v1/search?product_type=series&response_type=limited&rows=20&sort=title&start=20 + # max_items_per_page = 20 + # main_list_pages = int(self._get_setting("mainlist_pages")) + # data = self.__iterate_results( + # "https://urplay.se/api/v1/search?product_type=series&response_type=limited&rows={}&sort=published&start={}", + # results_per_page=max_items_per_page, + # max_iterations=main_list_pages, + # use_pb=True + # ) + # + # return data, [] + + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. + + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str|None url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + url = "https://urplay.se/api/v1/search?query=%s" - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) - def create_episode_json_item(self, result_set): + def create_episode_item(self, result_set: dict) -> MediaItemResult: """ Creates a new MediaItem for an episode. This method creates a new MediaItem from the Regular Expression or Json @@ -441,7 +478,7 @@ def create_episode_json_item(self, result_set): Logger.trace(result_set) title = "%(title)s" % result_set - url = "https://urplay.se/api/v1/series?id={}".format(result_set["id"]) + url = f"https://urplay.se/_next/data/{self.build_version}{result_set['link']}.json" fanart = "https://assets.ur.se/id/%(id)s/images/1_hd.jpg" % result_set thumb = "https://assets.ur.se/id/%(id)s/images/1_l.jpg" % result_set item = MediaItem(title, url) @@ -451,7 +488,7 @@ def create_episode_json_item(self, result_set): item.dontGroup = True return item - def create_season_item(self, result_set): + def create_season_item(self, result_set: dict) -> MediaItemResult: """ Creates a new MediaItem for a season. This method creates a new MediaItem from the Regular Expression or Json @@ -471,7 +508,7 @@ def create_season_item(self, result_set): return None title = "%(label)s" % result_set - url = "https://urplay.se/api/v1/series?id={}".format(result_set["id"]) + url = f"https://urplay.se/_next/data/{self.build_version}{result_set['link']}.json" fanart = "https://assets.ur.se/id/%(id)s/images/1_hd.jpg" % result_set thumb = "https://assets.ur.se/id/%(id)s/images/1_l.jpg" % result_set item = FolderItem(title, url, content_type=contenttype.EPISODES, media_type=mediatype.FOLDER) @@ -481,10 +518,11 @@ def create_season_item(self, result_set): item.metaData["season"] = True return item - def check_seasons(self, data, items): + # noinspection PyUnusedLocal + def check_seasons(self, data: JsonHelper, items: List[MediaItem]) -> List[MediaItem]: """ Performs post-process actions for data processing. - Accepts an data from the process_folder_list method, BEFORE the items are + 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 <data> could be changed and additional items can be created. @@ -504,14 +542,18 @@ def check_seasons(self, data, items): # check if there are seasons, if so, filter all the videos out seasons = [i for i in items if i.metaData.get("season", False)] - if seasons: + if seasons and len(seasons) > 1: Logger.debug("Seasons found, skipping any videos.") return seasons + if seasons and len(seasons) == 1: + Logger.debug("Remove the season entry") + return [i for i in items if not i.metaData.get("season", False)] + Logger.debug("Post-Processing finished") return items - def create_video_item_json_with_show_title(self, result_set): + def create_video_item_with_show_title(self, result_set: dict) -> MediaItemResult: """ 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 @@ -530,9 +572,9 @@ def create_video_item_json_with_show_title(self, result_set): """ - return self.create_video_item_json(result_set, include_show_title=True) + return self.create_video_item(result_set, include_show_title=True) - def create_video_item_json(self, result_set, include_show_title=False): + def create_video_item(self, result_set: dict, include_show_title: bool = False) -> MediaItemResult: """ 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 @@ -727,9 +769,8 @@ def __create_search_result(self, result_set, result_type): """ # Logger.trace(result_set) - if result_type == "series": - url = "https://urplay.se/api/v1/series?id={}".format(result_set["id"]) + url = f"https://urplay.se/_next/data/{self.build_version}{result_set['link']}.json" item = FolderItem(result_set["title"], url, contenttype.EPISODES, media_type=mediatype.FOLDER) else: url = "https://urplay.se/{}/{}".format(result_type, result_set["slug"]) diff --git a/plugin.video.retrospect/channels/channel.videoland/videolandnl/chn_videolandnl.py b/plugin.video.retrospect/channels/channel.videoland/videolandnl/chn_videolandnl.py index 8feb423b45..7f6bb5ee0a 100644 --- a/plugin.video.retrospect/channels/channel.videoland/videolandnl/chn_videolandnl.py +++ b/plugin.video.retrospect/channels/channel.videoland/videolandnl/chn_videolandnl.py @@ -93,7 +93,7 @@ def __init__(self, channel_info): def add_others_and_check_correct_url(self, data: str) -> Tuple[JsonHelper, List[MediaItem]]: items = [] search = FolderItem(LanguageHelper.get_localized_string(LanguageHelper.Search), - "#searchSite", content_type=contenttype.TVSHOWS) + self.search_url, content_type=contenttype.TVSHOWS) items.append(search) extras: Dict[int, Tuple[str, str]] = { @@ -163,8 +163,7 @@ def add_others_and_check_correct_url(self, data: str) -> Tuple[JsonHelper, List[ return json_data, items - def create_mainlist_item(self, result_set: Union[str, dict]) -> Union[ - MediaItem, List[MediaItem], None]: + def create_mainlist_item(self, result_set: Union[str, dict]) -> Union[MediaItem, List[MediaItem], None]: if not result_set["title"]: return None title = result_set["title"].get("long", result_set["title"].get("short")) @@ -304,26 +303,25 @@ def create_program_item(self, result_set: dict) -> Union[MediaItem, List[MediaIt return item - def search_site(self, url=None): - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ - needle = XbmcWrapper.show_key_board() if not needle: - return [] + raise ValueError("Needle missing.") url = "https://nhacvivxxk-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(4.14.2)%3B%20Browser" data = {"requests": [{ diff --git a/plugin.video.retrospect/channels/channel.videos/dumpert/chn_dumpert.py b/plugin.video.retrospect/channels/channel.videos/dumpert/chn_dumpert.py index a7492de8db..eb9dee79be 100644 --- a/plugin.video.retrospect/channels/channel.videos/dumpert/chn_dumpert.py +++ b/plugin.video.retrospect/channels/channel.videos/dumpert/chn_dumpert.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: GPL-3.0-or-later +from typing import Optional, List from resources.lib import chn_class, mediatype @@ -67,7 +68,7 @@ def get_main_list_items(self, data): item = MediaItem("Filmpjes - Pagina %s" % (page + 1, ), url_pattern % ('latest', page)) items.append(item) - item = MediaItem("Zoeken", "searchSite") + item = MediaItem("Zoeken", self.search_url) items.append(item) return data, items @@ -135,25 +136,28 @@ def create_json_video_item(self, result_set): # NOSONAR item.complete = True return item - def search_site(self, url=None): - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str|None url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + url = "https://api-live.dumpert.nl/mobile_api/json/search/%s/0/" - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def __ignore_cookie_law(self): """ Accepts the cookies from UZG in order to have the site available """ diff --git a/plugin.video.retrospect/channels/channel.videos/tweedekamer/chn_tweedekamer.py b/plugin.video.retrospect/channels/channel.videos/tweedekamer/chn_tweedekamer.py index 52d1d6e318..f2fce87396 100644 --- a/plugin.video.retrospect/channels/channel.videos/tweedekamer/chn_tweedekamer.py +++ b/plugin.video.retrospect/channels/channel.videos/tweedekamer/chn_tweedekamer.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: GPL-3.0-or-later +from typing import Optional, List from resources.lib import chn_class, mediatype, contenttype @@ -98,7 +99,7 @@ def create_main_index(self, data): cats = { "Debatten terugkijken": "https://cdn.debatdirect.tweedekamer.nl/api/agenda", "Livestreams": "https://cdn.debatdirect.tweedekamer.nl/api/app", - LanguageHelper.get_localized_string(LanguageHelper.Search): 'searchSite', + LanguageHelper.get_localized_string(LanguageHelper.Search): self.search_url, } for cat in cats: @@ -292,25 +293,28 @@ def update_video_item(self, item): Logger.debug('Finished update_video_item: %s', item.name) return item - def search_site(self, url=None): # @UnusedVariable - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ + if not needle: + raise ValueError("No needle present") + url = "https://cdn.debatdirect.tweedekamer.nl/search?q=%s&status[0]=geweest&sortering=relevant&vanaf=0" - return chn_class.Channel.search_site(self, url) + return chn_class.Channel.search_site(self, url, needle) def create_video_items_from_search(self, result_set): """ Creates a list of MediaItems for the debates in the search results. diff --git a/plugin.video.retrospect/resources/language/resource.language.en_gb/strings.po b/plugin.video.retrospect/resources/language/resource.language.en_gb/strings.po index 51f59c417e..776b2cd863 100644 --- a/plugin.video.retrospect/resources/language/resource.language.en_gb/strings.po +++ b/plugin.video.retrospect/resources/language/resource.language.en_gb/strings.po @@ -770,7 +770,11 @@ msgctxt "#30371" msgid "Trending" msgstr "" -# empty strings from id 30371 to 30400 +msgctxt "#30372" +msgid "New Search" +msgstr "" + +# empty strings from id 30373 to 30400 msgctxt "#30401" msgid "Dutch" @@ -1252,4 +1256,4 @@ msgstr "" msgctxt "#30609" msgid "Error" -msgstr "" \ No newline at end of file +msgstr "" diff --git a/plugin.video.retrospect/resources/language/resource.language.es_es/strings.po b/plugin.video.retrospect/resources/language/resource.language.es_es/strings.po index ece887d4e1..395520cd70 100644 --- a/plugin.video.retrospect/resources/language/resource.language.es_es/strings.po +++ b/plugin.video.retrospect/resources/language/resource.language.es_es/strings.po @@ -4,19 +4,19 @@ msgstr "" "Project-Id-Version: Retrospect Add-on\n" "Report-Msgid-Bugs-To: translations@kodi.tv\n" "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2024-01-20 04:13+0000\n" -"Last-Translator: José Antonio Alvarado <jalvarado0.eses@gmail.com>\n" +"PO-Revision-Date: 2024-04-27 22:42+0000\n" +"Last-Translator: roliverosc <roliverosc@hotmail.com>\n" "Language-Team: Spanish (Spain) <https://kodi.weblate.cloud/projects/kodi-add-ons-video/plugin-video-retrospect/es_es/>\n" "Language: es_es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.3\n" +"X-Generator: Weblate 5.4.3\n" msgctxt "Addon Summary" msgid "Retrospect allows you to watch re-runs/catch-ups of TV shows made available via their official broadcasters." -msgstr "Retrospect te permite ver reposiciones de programas de televisión disponibles a través de sus emisoras oficiales." +msgstr "Retrospect le permite ver reposiciones de programas de televisión disponibles a través de sus emisoras oficiales." msgctxt "Addon Description" msgid "Retrospect uses the official websites and freely available streams of different broadcasting companies (mainly Dutch, Belgian, British, Norwegian and Swedish) to make their re-run/catch-up episodes available on the Kodi platform. [CR][CR]Some channels that are included are: NPO Start, Kijk.nl (SBS6, NET5, Veronica, SBS9), RTL XL, NOS, Nickelodeon, AT5, Omroep Flevoland, L1, RTV Drenthe, RTV Oost, RTV Noord, RTV Noord-Holland, RTV Rijnmond, Omroep West, Omroep Gelderland, Omroep Brabant, Omrop Fryslân, WOS, DTV, Omroep Venlo Omroep Horst aan de Maas, Studio 040, RTV Utrecht, Omroep Zeeland, Eén, Vier, VRT.nu, VTM, Stieve, Ketnet, DPlay, SVT, ViaFree, Viasat, UR Play, MTV, NRK, BBC, Dumpert, Fox Sports, Pathé Nederland, Hardware.info and Ons.[CR][CR]More information can be found at https://github.com/retrospect-addon/plugin.video.retrospect or the Retrospect wiki at https://github.com/retrospect-addon/plugin.video.retrospect/wiki/." @@ -32,11 +32,11 @@ msgstr "Calidad de la transmisión" msgctxt "#30001" msgid "Low" -msgstr "Bajo" +msgstr "Baja" msgctxt "#30002" msgid "Medium" -msgstr "Medio" +msgstr "Media" msgctxt "#30003" msgid "High" @@ -100,11 +100,11 @@ msgstr "Fecha" msgctxt "#30019" msgid "Show DRM/Paid warning before playback" -msgstr "Mostrar aviso de DRM/Pago antes de la reproducción" +msgstr "Mostrar aviso de DRM/De pago antes de la reproducción" msgctxt "#30020" msgid "Maximum Stream Bitrate [COLOR=gray](0=disabled)[/COLOR]" -msgstr "Máxima Tasa de Bits de Transmisión[COLOR=gray](0=desactivado)[/COLOR]" +msgstr "Máxima tasa de bits de transmisión[COLOR=gray](0=desactivado)[/COLOR]" msgctxt "#30021" msgid "Enable subtitles by default when available" @@ -188,7 +188,7 @@ msgstr "Canales" msgctxt "#30041" msgid "Show \"All Favourites\" in main Channel listing" -msgstr "Mostrar \"Todos los favoritos\" en la lista principal de canales" +msgstr "Mostrar \"Todos los favoritos\" en la lista de canales principal" msgctxt "#30042" msgid "Enabled" @@ -282,7 +282,7 @@ msgstr "Grupo Proxy" msgctxt "#30065" msgid "Country Settings" -msgstr "Ajustes del país" +msgstr "Ajustes de país" msgctxt "#30066" msgid "DNS" @@ -306,7 +306,7 @@ msgstr "Dejar que Kodi determine la tasa de bits si es posible" msgctxt "#30071" msgid "Set the proxy and local IP to all channels related to this country" -msgstr "Ajustar el proxy y la IP local a todos los canales relacionados con este país" +msgstr "Ajustar el proxy y la IP local para todos los canales relacionados con este país" msgctxt "#30072" msgid "Proxy Type" @@ -314,7 +314,7 @@ msgstr "Tipo de proxy" msgctxt "#30073" msgid "- Geo-blocked = º, DRM protected = ^ and Premium = ª." -msgstr "- Geobloqueado = º, Protegido con DRM = ^ y Premium = ª." +msgstr "- Geobloqueo = º, Protegido con DRM = ^ y Premium = ª." msgctxt "#30074" msgid "Disabled" @@ -386,7 +386,7 @@ msgstr "La 'Bóveda de contraseñas' protege la contraseña almacenada con cifra msgctxt "#30091" msgid "Change Vault Pin" -msgstr "Cambiar Pin de Bóveda" +msgstr "Cambiar PIN de Bóveda" msgctxt "#30092" msgid "Reset Vault" @@ -414,7 +414,7 @@ msgstr "IP local" msgctxt "#30098" msgid "Logging & Diagnostics" -msgstr "Registro y diagnósticos" +msgstr "Registro y diagnóstico" msgctxt "#30099" msgid "Use HLS instead of Dash for non-encrypted streams" @@ -648,7 +648,7 @@ msgstr "Finlandia" # empty strings from id 30312 to 30345 msgctxt "#30346" msgid "Now Playing" -msgstr "Reproducción en curso" +msgstr "Reproduciendo" msgctxt "#30347" msgid "Now" @@ -837,7 +837,7 @@ msgstr "Ocultar" msgctxt "#30506" msgid "Remove Favourite" -msgstr "Quitar favorito" +msgstr "Borrar favorito" msgctxt "#30507" msgid "Select Channels »" @@ -940,7 +940,7 @@ msgstr "Página" msgctxt "#30538" msgid "The selected live stream has not yet|begun." -msgstr "La transmisión en directo seleccionada aún no ha|comenzado." +msgstr "La transmisión en directo seleccionada aún no ha comenzado." msgctxt "#30539" msgid "Live stream" @@ -948,7 +948,7 @@ msgstr "Transmisión en directo" msgctxt "#30540" msgid "Geo-blocked" -msgstr "Geobloqueado" +msgstr "Geobloqueo" msgctxt "#30541" msgid "Queue item" @@ -1064,15 +1064,15 @@ msgstr "Algunos de los add-ons de Retrospect Channel no están activados, lo que msgctxt "#30569" msgid "Ignore SSL verification errors (Security Risk!)" -msgstr "Ignorar los errores de verificación SSL (¡Riesgo de seguridad!)" +msgstr "Ignorar errores de verificación SSL (¡Riesgo de seguridad!)" msgctxt "#30570" msgid "Hide channel initialisation messages" -msgstr "Ocultar los mensajes de inicialización del canal" +msgstr "Ocultar mensajes de inicialización del canal" msgctxt "#30571" msgid "Use Kodi InputStream Adaptive add-on when possible" -msgstr "Usar el add-on InputStream Adaptive de Kodi cuando sea posible" +msgstr "Usar add-on InputStream Adaptive de Kodi cuando sea posible" msgctxt "#30572" msgid "Retrospect Settings »" @@ -1201,7 +1201,7 @@ msgstr "Limpiar caché y cookies »" msgctxt "#30605" msgid "Are you sure you want to clean all of Retrospect's cache and cookies? This action cannot be undone." -msgstr "¿Está seguro de que desea limpiar todo el caché y las cookies de Retrospect? Esta acción no se puede deshacer." +msgstr "¿Está seguro de que desea limpiar todo la caché y las cookies de Retrospect? Esta acción no se puede deshacer." msgctxt "#30606" msgid "Minimum level for showing notifications" diff --git a/plugin.video.retrospect/resources/language/resource.language.nl_nl/strings.po b/plugin.video.retrospect/resources/language/resource.language.nl_nl/strings.po index 023071e475..40ebfc0cc5 100644 --- a/plugin.video.retrospect/resources/language/resource.language.nl_nl/strings.po +++ b/plugin.video.retrospect/resources/language/resource.language.nl_nl/strings.po @@ -754,7 +754,11 @@ msgctxt "#30371" msgid "Trending" msgstr "Trending" -# empty strings from id 30367 to 30400 +msgctxt "#30372" +msgid "New Search" +msgstr "Nieuwe zoekopdracht" + +# empty strings from id 30373 to 30400 msgctxt "#30401" msgid "Dutch" msgstr "Nederlands" diff --git a/plugin.video.retrospect/resources/language/resource.language.uk_ua/strings.po b/plugin.video.retrospect/resources/language/resource.language.uk_ua/strings.po index 2884c27d68..e27cb92110 100644 --- a/plugin.video.retrospect/resources/language/resource.language.uk_ua/strings.po +++ b/plugin.video.retrospect/resources/language/resource.language.uk_ua/strings.po @@ -4,7 +4,7 @@ msgstr "" "Project-Id-Version: Retrospect Add-on\n" "Report-Msgid-Bugs-To: translations@kodi.tv\n" "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2024-04-14 12:13+0000\n" +"PO-Revision-Date: 2024-04-22 04:13+0000\n" "Last-Translator: Christian Gade <gade@kodi.tv>\n" "Language-Team: Ukrainian <https://kodi.weblate.cloud/projects/kodi-add-ons-video/plugin-video-retrospect/uk_ua/>\n" "Language: uk_ua\n" @@ -124,7 +124,7 @@ msgstr "" msgctxt "#30025" msgid "None" -msgstr "" +msgstr "Жоден" msgctxt "#30026" msgid "Group list if it exceeds # folders" @@ -144,7 +144,7 @@ msgstr "Сервер" msgctxt "#30030" msgid "Port" -msgstr "" +msgstr "Порт" msgctxt "#30031" msgid "Cache HTTP(S) requests" @@ -298,7 +298,7 @@ msgstr "" msgctxt "#30069" msgid "Settings" -msgstr "" +msgstr "Налаштування" msgctxt "#30070" msgid "Let Kodi determine bitrate if possible" @@ -422,7 +422,7 @@ msgstr "" msgctxt "#30100" msgid "None" -msgstr "" +msgstr "Жоден" msgctxt "#30101" msgid "Regional" diff --git a/plugin.video.retrospect/resources/lib/actions/__init__.py b/plugin.video.retrospect/resources/lib/actions/__init__.py index e756575c17..f4e7c93527 100644 --- a/plugin.video.retrospect/resources/lib/actions/__init__.py +++ b/plugin.video.retrospect/resources/lib/actions/__init__.py @@ -3,4 +3,4 @@ __all__ = ["addonaction", "channellistaction", "categoryaction", "vaultaction", "keyword", "logaction", "configurechannelaction", "folderaction", "favouritesaction", "action", "videoaction", "contextaction", "actionparser", "cleanaction", "shortcutaction", - "iptvmanageraction", "executeaction"] + "iptvmanageraction", "executeaction", "searchaction"] diff --git a/plugin.video.retrospect/resources/lib/actions/action.py b/plugin.video.retrospect/resources/lib/actions/action.py index d24f696f3d..7eb84f7d56 100644 --- a/plugin.video.retrospect/resources/lib/actions/action.py +++ b/plugin.video.retrospect/resources/lib/actions/action.py @@ -14,6 +14,7 @@ SET_ENCRYPTION_PIN = "changepin" # : Used for setting an application pin SET_ENCRYPTED_VALUE = "encryptsetting" # : Used for setting an application pin RESET_VAULT = "resetvault" # : Used for resetting the vault +SEARCH = "search" # : Used for searching POST_LOG = "postlog" # : Used for sending log files to pastebin.com CLEANUP = "cleanup" # : Used for cleaning the cache and cookies IPTVMANAGER = "iptvmanager" # : Used by IPTV Manager to query channel data diff --git a/plugin.video.retrospect/resources/lib/actions/actionparser.py b/plugin.video.retrospect/resources/lib/actions/actionparser.py index 64dfac85b0..c0191484f0 100644 --- a/plugin.video.retrospect/resources/lib/actions/actionparser.py +++ b/plugin.video.retrospect/resources/lib/actions/actionparser.py @@ -1,7 +1,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later import random +from typing import Optional, Union +from resources.lib.channelinfo import ChannelInfo +from resources.lib.chn_class import Channel from resources.lib.retroconfig import Config from resources.lib.logger import Logger from resources.lib.pickler import Pickler @@ -107,6 +110,10 @@ def media_item(self): return self.__media_item + @property + def action(self): + return self.params.get(keyword.ACTION) + @property def pickle_hash(self): """ Returns the pickle hash of the current item. @@ -121,8 +128,10 @@ def pickle_hash(self): return self.params.get(keyword.STORE_ID) - def create_action_url(self, channel, action, item=None, store_id=None, category=None): - """ Creates an URL that includes an action. + def create_action_url(self, channel: Union[Channel, ChannelInfo], action: str, + item: MediaItem = None, store_id: Optional[str] = None, + category: Optional[str] = None, needle: Optional[str] = None) -> str: + """ Creates a URL that includes an action. Arguments: channel : Channel - @@ -131,14 +140,14 @@ def create_action_url(self, channel, action, item=None, store_id=None, category= Keyword Arguments: item : MediaItem - - :param ChannelInfo|Channel channel: The channel object to use for the URL - :param str action: Action to create an url for - :param MediaItem item: The media item to add - :param str store_id: The ID of the pickle store - :param str category: The category to use + :param channel: The channel object to use for the URL + :param action: Action to create an url for + :param item: The media item to add + :param store_id: The ID of the pickle store + :param str category: The category to use + :param needle: A search needle. :return: a complete action url with all keywords and values - :rtype: str|unicode """ @@ -161,6 +170,8 @@ def create_action_url(self, channel, action, item=None, store_id=None, category= # params[keyword.RANDOM_LIVE] = random.randint(10000, 99999) params[keyword.ACTION] = action + if needle: + params[keyword.NEEDLE] = needle # it might have an item or not if item is not None: @@ -214,7 +225,11 @@ def __get_parameters(self, query_string): try: for pair in query_string.split("&"): - (k, v) = pair.split("=") + if "=" in pair: + (k, v) = pair.split("=") + else: + k = pair + v = "" result[k] = v # if the channelcode was empty, it was stripped, add it again. diff --git a/plugin.video.retrospect/resources/lib/actions/folderaction.py b/plugin.video.retrospect/resources/lib/actions/folderaction.py index b9a53d12f9..a99c289173 100644 --- a/plugin.video.retrospect/resources/lib/actions/folderaction.py +++ b/plugin.video.retrospect/resources/lib/actions/folderaction.py @@ -20,7 +20,7 @@ class FolderAction(AddonAction): - def __init__(self, parameter_parser, channel, favorites=None): + def __init__(self, parameter_parser, channel, favorites=None, items=None): """Wraps the channel.process_folder_list :param ActionParser parameter_parser: A ActionParser object to is used to parse and @@ -38,6 +38,7 @@ def __init__(self, parameter_parser, channel, favorites=None): self.__channel = channel self.__media_item = parameter_parser.media_item self.__favorites = favorites + self.__items = items def execute(self): Logger.info("Plugin::process_folder_list Doing process_folder_list") @@ -50,7 +51,10 @@ def execute(self): # determine the parent guid parent_guid = self.parameter_parser.get_parent_guid(self.__channel, selected_item) - if self.__favorites is None: + if self.__items is not None: + watcher = StopWatch("Plugin process_folder_list of existing items", Logger.instance()) + media_items = self.__items + elif self.__favorites is None: watcher = StopWatch("Plugin process_folder_list", Logger.instance()) media_items = self.__channel.process_folder_list(selected_item) watcher.lap("Class process_folder_list finished") @@ -240,6 +244,11 @@ def __set_kodi_properties(self, kodi_item, media_item, is_folder, is_favourite): # Set the properties for the context menu add-on kodi_item.setProperty(self._propertyRetrospect, "true") + + if media_item.is_search_folder: + # Search folders don't need more. + return + kodi_item.setProperty(self._propertyRetrospectFolder if is_folder else self._propertyRetrospectVideo, "true") @@ -295,7 +304,7 @@ def __add_sort_method_to_handle(self, handle, items=None): # Some items have episodes, only add the sorting options. sort_methods.append(xbmcplugin.SORT_METHOD_EPISODE) # 24 - is_search = self.__media_item.is_search(self.__media_item.url) if self.__media_item else False + is_search = self.parameter_parser.action == action.SEARCH if is_search: sort_methods.remove(xbmcplugin.SORT_METHOD_UNSORTED) sort_methods.insert(0, xbmcplugin.SORT_METHOD_UNSORTED) diff --git a/plugin.video.retrospect/resources/lib/actions/keyword.py b/plugin.video.retrospect/resources/lib/actions/keyword.py index 4a7980076e..163d964b61 100644 --- a/plugin.video.retrospect/resources/lib/actions/keyword.py +++ b/plugin.video.retrospect/resources/lib/actions/keyword.py @@ -17,3 +17,4 @@ PORT = "port" # : Used for communicating the port that is used to open the socket to IPTV Manager REQUEST = "request" # : Used for communicating what data is requested by IPTV Manager COMMAND = "command" # : User for executing a command. +NEEDLE = "needle" # : Used for searching. diff --git a/plugin.video.retrospect/resources/lib/actions/searchaction.py b/plugin.video.retrospect/resources/lib/actions/searchaction.py new file mode 100644 index 0000000000..9fc5fdcbce --- /dev/null +++ b/plugin.video.retrospect/resources/lib/actions/searchaction.py @@ -0,0 +1,109 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +import re +from typing import Optional, List + +import xbmc +import xbmcplugin + +from resources.lib import contenttype +from resources.lib.actions import action, keyword +from resources.lib.actions.actionparser import ActionParser +from resources.lib.actions.addonaction import AddonAction +from resources.lib.actions.folderaction import FolderAction +from resources.lib.addonsettings import AddonSettings, LOCAL +from resources.lib.chn_class import Channel +from resources.lib.helpers.htmlentityhelper import HtmlEntityHelper +from resources.lib.helpers.languagehelper import LanguageHelper +from resources.lib.logger import Logger +from resources.lib.mediaitem import FolderItem, MediaItem +from resources.lib.xbmcwrapper import XbmcWrapper + + +class SearchAction(AddonAction): + __channel: Channel + __needle: Optional[str] + __media_item: MediaItem + + def __init__(self, parameter_parser: ActionParser, channel: Channel, needle: Optional[str]): + """Wraps the channel.process_folder_list + + :param parameter_parser: A ActionParser object to is used to parse and create urls + :param channel: The channel info for the channel + :needle: The needle + + """ + + super().__init__(parameter_parser) + + self.__needle = needle if needle is None else HtmlEntityHelper.url_decode(needle) + self.__settings = AddonSettings.store(store_location=LOCAL) + self.__media_item = parameter_parser.media_item + self.__channel = channel + Logger.debug(f"Searching for: {self.__needle}") + + def execute(self): + # read the item from the parameters + selected_item: MediaItem = self.__media_item + + # determine the parent guid + parent_guid = self.parameter_parser.get_parent_guid(self.__channel, selected_item) + + if self.__needle is None: + self.__generate_search_history(selected_item, parent_guid) + return + + elif not self.__needle: + # Search input + needle = XbmcWrapper.show_key_board() + if not needle: + xbmcplugin.endOfDirectory(self.handle, False, cacheToDisc=True) + return + + # noinspection PyTypeChecker + history: List[str] = self.__settings.get_setting("search", self.__channel, []) # type: ignore + history = list(set([needle] + history)) + self.__settings.set_setting("search", history[0:10], self.__channel) + + # Make sure we actually load a new URL so a refresh won't pop up a loading screen. + needle = HtmlEntityHelper.url_encode(needle) + xbmcplugin.endOfDirectory(self.handle, True, cacheToDisc=True) + url = self.parameter_parser.create_action_url(self.__channel, action.SEARCH, needle=needle) + xbmc.executebuiltin(f"Container.Update({url})") + + else: + media_items = self.__channel.search_site(needle=self.__needle) + re_needle = re.escape(self.__needle) + Logger.debug(f"Highlighting {self.__needle} `{re_needle}` in results.") + highlighter = re.compile(f"({re_needle})", re.IGNORECASE) + + for item in media_items: + item.name = highlighter.sub(r"[COLOR gold]\1[/COLOR]", item.name) + if item.description: + item.description = highlighter.sub(r"[COLOR gold]\1[/COLOR]", item.description) + folder_action = FolderAction(self.parameter_parser, self.__channel, items=media_items) + folder_action.execute() + + def __generate_search_history(self, selected_item: MediaItem, parent_guid: str): + # noinspection PyTypeChecker + history: List[str] = self.__settings.get_setting("search", self.__channel, []) + + media_items = [] + search_item = FolderItem( + f"\b{LanguageHelper.get_localized_string(LanguageHelper.NewSearch)}", + f"{self.__channel.search_url}&{keyword.NEEDLE}=", + content_type=contenttype.VIDEOS + ) + search_item.actionUrl = search_item.url + media_items.append(search_item) + + for needle in history: + encoded_needle = HtmlEntityHelper.url_encode(needle) + url = self.parameter_parser.create_action_url(self.__channel, action.SEARCH, + needle=encoded_needle) + item = FolderItem(needle, url, content_type=contenttype.VIDEOS) + item.actionUrl = url + item.metaData["retrospect:needle"] = needle + media_items.append(item) + + folder_action = FolderAction(self.parameter_parser, self.__channel, items=media_items) + folder_action.execute() diff --git a/plugin.video.retrospect/resources/lib/chn_class.py b/plugin.video.retrospect/resources/lib/chn_class.py index ab99c80626..8cc251d9c4 100644 --- a/plugin.video.retrospect/resources/lib/chn_class.py +++ b/plugin.video.retrospect/resources/lib/chn_class.py @@ -4,6 +4,7 @@ import urllib.parse as parse from typing import Optional, List, Union +from resources.lib.actions import keyword, action from resources.lib.mediaitem import MediaItem, FolderItem, MediaStream from resources.lib import contenttype from resources.lib import mediatype @@ -41,7 +42,7 @@ def __init__(self, channel_info): # noinspection PyTypeChecker self.parentItem = None # type: MediaItem - self.currentParser = None # type: Optional[ParserData] + self.currentParser = None # type: Optional[ParserData] # More and more API's need a specific set of headers. This set is used for the self.mainListUri, and is set to # all items generated by the chn_class.py. @@ -58,6 +59,7 @@ def __init__(self, channel_info): self.channelCode = channel_info.channelCode self.channelDescription = channel_info.channelDescription self.moduleName = channel_info.moduleName + self.uses_external_addon = channel_info.uses_external_addon self.ignore = channel_info.ignore self.sortOrder = channel_info.sortOrder self.sortOrderPerCountry = channel_info.sortOrderPerCountry @@ -138,6 +140,16 @@ def init_channel(self): self.poster = TextureHandler.instance().get_texture_uri(self, self.poster) return + @property + def search_url(self) -> str: + if self.channelCode: + return (f"plugin://{Config.addonId}/?{keyword.CHANNEL}={self.url_id}" + f"&{keyword.CHANNEL_CODE}={self.channelCode}" + f"&{keyword.ACTION}={action.SEARCH}") + else: + return (f"plugin://{Config.addonId}/?{keyword.CHANNEL}={self.url_id}" + f"&{keyword.ACTION}={action.SEARCH}") + @property def sort_key(self): return "{0}-{1}".format(self.sortOrderPerCountry, self.channelName) @@ -148,7 +160,7 @@ def filter_premium(self) -> Optional[bool]: return None - def process_folder_list(self, parent_item: Optional[MediaItem]=None) -> List[MediaItem]: # NOSONAR + def process_folder_list(self, parent_item: Optional[MediaItem] = None) -> List[MediaItem]: # NOSONAR """ Process the selected item and gets it's child items using the available dataparsers. Accepts an <item> and returns a list of MediaListems with at least name & url @@ -209,10 +221,6 @@ def process_folder_list(self, parent_item: Optional[MediaItem]=None) -> List[Med data = UriHandler.open(url, additional_headers=headers, json=parent_item.postJson, no_cache=no_cache) else: data = UriHandler.open(url, additional_headers=headers, no_cache=no_cache) - # Searching a site using search_site() - elif MediaItem.is_search(url): - Logger.debug("Starting to search") - return self.search_site() # Labels instead of url's elif url.startswith("#"): data = "" @@ -497,20 +505,20 @@ def process_video_item(self, item): Logger.debug("Processing Updater from %s", data_parser) return data_parser.Updater(item) - def search_site(self, url=None): - """ Creates an list of items by searching the site. + def search_site(self, url: Optional[str] = None, needle: Optional[str] = None) -> List[MediaItem]: + """ Creates a list of items by searching the site. - This method is called when the URL of an item is "searchSite". The channel + This method is called when and item with `self.search_url` is opened. The channel calling this should implement the search functionality. This could also include showing of an input keyboard and following actions. - The %s the url will be replaced with an URL encoded representation of the + The %s the url will be replaced with a URL encoded representation of the text to search for. - :param str|None url: Url to use to search with a %s for the search parameters. + :param url: Url to use to search with an %s for the search parameters. + :param needle: The needle to search for. :return: A list with search results as MediaItems. - :rtype: list[MediaItem] """ @@ -518,16 +526,18 @@ def search_site(self, url=None): if url is None: item = MediaItem("Search Not Implented", "", media_type=mediatype.VIDEO) items.append(item) + + elif not needle: + item = MediaItem("No Needle Present For Search", "", media_type=mediatype.VIDEO) + items.append(item) + else: - items = [] - needle = XbmcWrapper.show_key_board() - if needle: - Logger.debug("Searching for '%s'", needle) - # convert to HTML - needle = HtmlEntityHelper.url_encode(needle) - search_url = url % (needle, ) - temp = MediaItem("Search", search_url, mediatype.FOLDER) - return self.process_folder_list(temp) + Logger.debug("Searching for '%s'", needle) + # convert to HTML + needle = HtmlEntityHelper.url_encode(needle) + search_url = url % (needle, ) + temp = MediaItem("Search", search_url, mediatype.FOLDER) + items = self.process_folder_list(temp) return items @@ -594,7 +604,7 @@ def pre_process_folder_list(self, data): return data, items # noinspection PyUnusedLocal - def post_process_folder_list(self, data, items): + def post_process_folder_list(self, data: Union[str, JsonHelper], items: List[MediaItem]) -> List[MediaItem]: """ Performs post-process actions for data processing. Accepts an data from the process_folder_list method, BEFORE the items are @@ -604,12 +614,10 @@ def post_process_folder_list(self, data, items): 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 + :param data: The retrieve data that was loaded for the current item and URL. + :param items: The currently available items :return: A tuple of the data and a list of MediaItems that were generated. - :rtype: list[MediaItem] """ @@ -880,7 +888,6 @@ def create_iptv_streams(self, parameter_parser): :rtype: list """ return [] - def create_iptv_epg(self, parameter_parser): """ Fallback function if not implemented. @@ -933,7 +940,7 @@ def _add_data_parser(self, url, name=None, preprocessor=None, """ Adds a DataParser to the handlers dictionary :param preprocessor: The pre-processor called - :type preprocessor: (str) -> (str|JsonHelper,list[MediaItem]) + :type preprocessor: (str|JsonHelper) -> (str|JsonHelper,list[MediaItem]) :param str name: The name of the DataParser :param str url: The URLs that triggers these handlers @@ -1036,9 +1043,6 @@ def __get_data_parsers(self, url: str, parser_label: Optional[str] = None) -> Li """ - if MediaItem.is_search(url): - return [] - # For now we need to be backwards compatible: if not self.dataParsers: self.__generate_data_parsers_from_old_methods(url) diff --git a/plugin.video.retrospect/resources/lib/helpers/languagehelper.py b/plugin.video.retrospect/resources/lib/helpers/languagehelper.py index 651656526a..938b62df09 100644 --- a/plugin.video.retrospect/resources/lib/helpers/languagehelper.py +++ b/plugin.video.retrospect/resources/lib/helpers/languagehelper.py @@ -80,6 +80,7 @@ class LanguageHelper(object): Fragments = 30369 AllEpisodes = 30370 Trending = 30371 + NewSearch = 30372 ChannelSelection = 30507 ShortCutName = 30512 diff --git a/plugin.video.retrospect/resources/lib/mediaitem.py b/plugin.video.retrospect/resources/lib/mediaitem.py index d48d087690..2fb1215668 100644 --- a/plugin.video.retrospect/resources/lib/mediaitem.py +++ b/plugin.video.retrospect/resources/lib/mediaitem.py @@ -4,7 +4,7 @@ from datetime import datetime from functools import reduce from random import getrandbits -from typing import Optional +from typing import Optional, Dict, Any, List, Union import xbmcgui @@ -16,6 +16,7 @@ from resources.lib.helpers.languagehelper import LanguageHelper from resources.lib import mediatype from resources.lib import contenttype +from resources.lib.retroconfig import Config from resources.lib.streams.adaptive import Adaptive from resources.lib.proxyinfo import ProxyInfo @@ -32,8 +33,33 @@ class MediaItem: """ + actionUrl: Optional[str] + cacheToDisc: bool + complete: bool + content_type: str + description: str + dontGroup: bool + episode: int + fanart: str + HttpHeaders: Dict[str, str] + icon: str + isCloaked: bool + isDrmProtected: bool + isGeoLocked: bool + isLive: bool + isPaid: bool + items: List["MediaItem"] + media_type: Optional[str] + metaData: Dict[Any, Any] + name: str postData: Optional[str] + poster: str postJson: Optional[dict] + streams: List["MediaStream"] + subtitle: Optional[str] + thumb: str + tv_show_title: Optional[str] + url: str LabelEpisode = "Episode" LabelTrackNumber = "TrackNumber" @@ -105,7 +131,7 @@ def __init__(self, title, url, media_type=mediatype.FOLDER, depickle=False, tv_s # musicvideos, videos, images, games. Defaults to 'episodes' self.content_type = contenttype.EPISODES - self.streams = [] # type: list[MediaStream] + self.streams = [] self.subtitle = None if depickle: @@ -212,6 +238,10 @@ def is_audio(self): """ return self.media_type in mediatype.AUDIO_TYPES + @property + def is_search_folder(self) -> bool: + return self.is_folder and "retrospect:needle" in self.metaData + def has_track(self): """ Does this MediaItem have a TrackNumber InfoLabel @@ -584,16 +614,14 @@ def get_resolved_kodi_item(self, bitrate, proxy=None): @property def uses_external_addon(self): - return self.url is not None and self.url.startswith("plugin://") + return (self.url is not None + and self.url.startswith("plugin://") + and not self.url.startswith(f"plugin://{Config.addonId}")) @property def title(self): return self.name - @staticmethod - def is_search(url): - return url == "searchSite" or url == "#searchSite" - def __get_matching_stream(self, bitrate): """ Returns the MediaStream for the requested bitrate. @@ -689,7 +717,7 @@ def __set_guids(self): # For live items and search, append a random part to the textual guid, as these items # actually have different content for the same URL. - if self.isLive or self.is_search(self.url): + if self.isLive: self.__guid = "%s%s" % (self.__guid, ("%0x" % getrandbits(8 * 4)).upper()) except: Logger.error("Error setting GUID for title:'%s' and url:'%s'. Falling back to UUID", @@ -1042,3 +1070,6 @@ def __str__(self): text = "%s\n + Property: %s=%s" % (text, prop[0], prop[1]) return text + + +MediaItemResult = Optional[Union[List[MediaItem], MediaItem]] diff --git a/plugin.video.retrospect/resources/lib/plugin.py b/plugin.video.retrospect/resources/lib/plugin.py index 531482173d..c71b8cef2f 100644 --- a/plugin.video.retrospect/resources/lib/plugin.py +++ b/plugin.video.retrospect/resources/lib/plugin.py @@ -150,6 +150,11 @@ def run(self): # NOSONAR from resources.lib.actions.folderaction import FolderAction addon_action = FolderAction(self, channel_object) + elif self.params[keyword.ACTION] == action.SEARCH: + needle: str = self.params.get(keyword.NEEDLE, None) + from resources.lib.actions.searchaction import SearchAction + addon_action = SearchAction(self, channel_object, needle) + elif self.params[keyword.ACTION] == action.PLAY_VIDEO: from resources.lib.actions.videoaction import VideoAction addon_action = VideoAction(self, channel_object) diff --git a/plugin.video.retrospect/resources/settings.xml b/plugin.video.retrospect/resources/settings.xml index b5848bf30f..8f050b05fe 100644 --- a/plugin.video.retrospect/resources/settings.xml +++ b/plugin.video.retrospect/resources/settings.xml @@ -67,7 +67,7 @@ <category id="channelsettings" label="30032"> <setting type="lsep" label="30063" /> <!-- start of active channels --> - <setting id="config_channel" type="select" label="30040" values="NPO Start|Videoland|Kijk.nl|Vier|Vijf|Zes|VTM.be|Stievie|VRT NU|Ketnet|SVT Play|UR Play|TV4 Play" /> + <setting id="config_channel" type="select" label="30040" values="NPO Start|Videoland|Kijk.nl|GoPlay|Vier|Vijf|Zes|Zeven|VTM.be|Stievie|VRT NU|Ketnet|SVT Play|UR Play|TV4 Play" /> <!-- end of active channels --> <setting type="lsep" label="30032" /> @@ -96,10 +96,10 @@ <setting id="channel_C5182B93-6B71-44BE-948F-0B74E6C2BAA6_videolandnl_password_set" label="30093" type="action" action="RunScript(plugin.video.retrospect, 0, ?action=encryptsetting&settingid=channel_C5182B93-6B71-44BE-948F-0B74E6C2BAA6_videolandnl_password&settingname=Videoland&tabfocus=102)" option="close" visible="eq(-15,Videoland)" /> <setting id="channel_C5182B93-6B71-44BE-948F-0B74E6C2BAA6_filter_premium" type="select" lvalues="30126|30127|30128" default="0" label="30081" visible="eq(-16,Videoland)" /> <!-- chn_vier.py --> - <setting id="viervijfzes_username" type="text" label="30094" default="" visible="eq(-17,Zes)|eq(-17,Vijf)|eq(-17,Vier)" /> - <setting id="viervijfzes_password_set" label="30093" type="action" action="RunScript(plugin.video.retrospect, 0, ?action=encryptsetting&settingid=viervijfzes_password&settingname=Vier/Vijf/Zes.be password&tabfocus=102)" option="close" visible="eq(-18,Zes)|eq(-18,Vijf)|eq(-18,Vier)" /> - <setting id="viervijfzes_password" default="" visible="eq(-19,Zes)|eq(-19,Vijf)|eq(-19,Vier)" /> - <setting id="viervijfzes_refresh_token" default="" visible="eq(-20,Zes)|eq(-20,Vijf)|eq(-20,Vier)" /> + <setting id="viervijfzes_username" type="text" label="30094" default="" visible="eq(-17,Zeven)|eq(-17,Zes)|eq(-17,Vijf)|eq(-17,Vier)|eq(-17,GoPlay)" /> + <setting id="viervijfzes_password_set" label="30093" type="action" action="RunScript(plugin.video.retrospect, 0, ?action=encryptsetting&settingid=viervijfzes_password&settingname=Vier/Vijf/Zes.be password&tabfocus=102)" option="close" visible="eq(-18,Zeven)|eq(-18,Zes)|eq(-18,Vijf)|eq(-18,Vier)|eq(-18,GoPlay)" /> + <setting id="viervijfzes_password" default="" visible="eq(-19,Zeven)|eq(-19,Zes)|eq(-19,Vijf)|eq(-19,Vier)|eq(-19,GoPlay)" /> + <setting id="viervijfzes_refresh_token" default="" visible="eq(-20,Zeven)|eq(-20,Zes)|eq(-20,Vijf)|eq(-20,Vier)|eq(-20,GoPlay)" /> <!-- chn_vrtnu.py --> <setting id="channel_F530A9EC-3C0D-49B6-96C2-480273417460_username" label="30094" type="text" default="" visible="eq(-21,VRT NU)" /> <setting id="channel_F530A9EC-3C0D-49B6-96C2-480273417460_password_set" label="30093" type="action" action="RunScript(plugin.video.retrospect, 0, ?action=encryptsetting&settingid=channel_F530A9EC-3C0D-49B6-96C2-480273417460_password&settingname=VRT NU password&tabfocus=102)" option="close" visible="eq(-22,VRT NU)" />