From 88e1bbc4d61efad07f65693081a9d4bad917696d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 17 Feb 2021 07:42:24 +0100 Subject: [PATCH] Add recommendations and categories (#76) --- .../resource.language.en_gb/strings.po | 28 ++- .../resource.language.nl_nl/strings.po | 28 ++- resources/lib/addon.py | 52 +++-- resources/lib/kodiutils.py | 9 +- resources/lib/modules/catalog.py | 67 ++++++ resources/lib/modules/channels.py | 69 ------ resources/lib/modules/menu.py | 38 +++- resources/lib/modules/player.py | 10 +- resources/lib/modules/tvguide.py | 10 +- resources/lib/viervijfzes/content.py | 212 ++++++++++++------ resources/settings.xml | 4 + tests/test_api.py | 24 +- tests/test_routing.py | 3 + 13 files changed, 360 insertions(+), 194 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 064ff55..f9f9a06 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -18,6 +18,18 @@ msgctxt "#30003" msgid "Catalogue" msgstr "" +msgctxt "#30004" +msgid "Browse the catalogue" +msgstr "" + +msgctxt "#30005" +msgid "Recommendations" +msgstr "" + +msgctxt "#30006" +msgid "Show the recommendations" +msgstr "" + msgctxt "#30007" msgid "Channels" msgstr "" @@ -64,14 +76,6 @@ msgctxt "#30056" msgid "Browse the Catalog for [B]{channel}[/B]" msgstr "" -msgctxt "#30057" -msgid "Categories for [B]{channel}[/B]" -msgstr "" - -msgctxt "#30058" -msgid "Browse the Categories for [B]{channel}[/B]" -msgstr "" - msgctxt "#30059" msgid "Clips of [B]{program}[/B]" msgstr "" @@ -186,6 +190,14 @@ msgctxt "#30803" msgid "Password" msgstr "" +msgctxt "#30820" +msgid "Interface" +msgstr "" + +msgctxt "#30821" +msgid "Show unavailable programs" +msgstr "" + msgctxt "#30840" msgid "Integration" msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 8610ed3..c86ff09 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -19,6 +19,18 @@ msgctxt "#30003" msgid "Catalogue" msgstr "Catalogus" +msgctxt "#30004" +msgid "Browse the catalogue" +msgstr "Doorblader de catalogus" + +msgctxt "#30005" +msgid "Recommendations" +msgstr "Aanbevelingen" + +msgctxt "#30006" +msgid "Show the recommendations" +msgstr "Doorblader de aanbevelingen" + msgctxt "#30007" msgid "Channels" msgstr "Kanalen" @@ -65,14 +77,6 @@ msgctxt "#30056" msgid "Browse the Catalog for [B]{channel}[/B]" msgstr "Doorblader de catalogus voor [B]{channel}[/B]" -msgctxt "#30057" -msgid "Categories for [B]{channel}[/B]" -msgstr "CategoriĆ«n voor [B]{channel}[/B]" - -msgctxt "#30058" -msgid "Browse the Categories for [B]{channel}[/B]" -msgstr "Doorblader de categoriĆ«n van [B]{channel}[/B]" - msgctxt "#30059" msgid "Clips of [B]{program}[/B]" msgstr "Clips van [B]{program}[/B]" @@ -187,6 +191,14 @@ msgctxt "#30803" msgid "Password" msgstr "Wachtwoord" +msgctxt "#30820" +msgid "Interface" +msgstr "Interface" + +msgctxt "#30821" +msgid "Show unavailable programs" +msgstr "Toon onbeschikbare programma's" + msgctxt "#30840" msgid "Integration" msgstr "Integratie" diff --git a/resources/lib/addon.py b/resources/lib/addon.py index fbd3e80..c2f6490 100644 --- a/resources/lib/addon.py +++ b/resources/lib/addon.py @@ -9,6 +9,11 @@ from routing import Plugin from resources.lib import kodilogging +try: # Python 3 + from urllib.parse import unquote +except ImportError: # Python 2 + from urllib import unquote + routing = Plugin() # pylint: disable=invalid-name _LOGGER = logging.getLogger(__name__) @@ -34,20 +39,6 @@ def show_channel_menu(channel): Channels().show_channel_menu(channel) -# @routing.route('/channels//categories') -# def show_channel_categories(channel): -# """ Shows TV Channel categories """ -# from resources.lib.modules.channels import Channels -# Channels().show_channel_categories(channel) - - -# @routing.route('/channels//categories/') -# def show_channel_category(channel, category): -# """ Shows TV Channel categories """ -# from resources.lib.modules.channels import Channels -# Channels().show_channel_category(channel, category) - - @routing.route('/channels//tvguide') def show_channel_tvguide(channel): """ Shows the dates in the tv guide """ @@ -97,6 +88,34 @@ def show_catalog_program_season(program, season): Catalog().show_program_season(program, season) +@routing.route('/category') +def show_categories(): + """ Show the catalog by category """ + from resources.lib.modules.catalog import Catalog + Catalog().show_categories() + + +@routing.route('/category/') +def show_category(category): + """ Show the catalog by category """ + from resources.lib.modules.catalog import Catalog + Catalog().show_category(category) + + +@routing.route('/recommendations') +def show_recommendations(): + """ Show my list """ + from resources.lib.modules.catalog import Catalog + Catalog().show_recommendations() + + +@routing.route('/recommendations/') +def show_recommendations_category(category): + """ Show my list """ + from resources.lib.modules.catalog import Catalog + Catalog().show_recommendations_category(category) + + @routing.route('/mylist') def show_mylist(): """ Show my list """ @@ -150,11 +169,6 @@ def play_catalog(uuid): @routing.route('/play/page/') def play_from_page(page): """ Play the requested item """ - try: # Python 3 - from urllib.parse import unquote - except ImportError: # Python 2 - from urllib import unquote - from resources.lib.modules.player import Player Player().play_from_page(unquote(page)) diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index ce2cb10..6717930 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -45,6 +45,7 @@ HTML_MAPPING = [ (re.compile(r']+)>', re.I), '\n'), (re.compile(r']+)>', re.I), ''), (re.compile('( \n){2,}', re.I), '\n'), # Remove repeating non-blocking spaced newlines + (re.compile(' +', re.I), ' '), # Remove double spaces ] STREAM_HLS = 'hls' @@ -57,8 +58,7 @@ class TitleItem: """ This helper object holds all information to be used with Kodi xbmc's ListItem object """ def __init__(self, title, path=None, art_dict=None, info_dict=None, prop_dict=None, stream_dict=None, - context_menu=None, subtitles_path=None, - is_playable=False): + context_menu=None, subtitles_path=None, is_playable=False, visible=True): """ The constructor for the TitleItem class :type title: str :type path: str @@ -69,6 +69,7 @@ class TitleItem: :type context_menu: list[tuple[str, str]] :type subtitles_path: list[str] :type is_playable: bool + :type visible: bool """ self.title = title self.path = path @@ -79,6 +80,7 @@ class TitleItem: self.context_menu = context_menu self.subtitles_path = subtitles_path self.is_playable = is_playable + self.visible = visible def __repr__(self): return "%r" % self.__dict__ @@ -189,6 +191,9 @@ def show_listing(title_items, category=None, sort=None, content=None, cache=True # Add the listings listing = [] for title_item in title_items: + if not title_item.visible: + continue + # Three options: # - item is a virtual directory/folder (not playable, path) # - item is a playable file (playable, path) diff --git a/resources/lib/modules/catalog.py b/resources/lib/modules/catalog.py index d91634c..e283a3d 100644 --- a/resources/lib/modules/catalog.py +++ b/resources/lib/modules/catalog.py @@ -178,6 +178,73 @@ class Catalog: # Sort like we get our results back. kodiutils.show_listing(listing, 30003, content='episodes') + def show_categories(self): + """ Shows the categories """ + categories = self._api.get_categories() + + listing = [] + for category in categories: + listing.append(TitleItem(title=category.title, + path=kodiutils.url_for('show_category', category=category.uuid), + info_dict={ + 'title': category.title, + })) + + kodiutils.show_listing(listing, 30003, sort=['title']) + + def show_category(self, uuid): + """ Shows a category """ + programs = self._api.get_category_content(int(uuid)) + + listing = [ + Menu.generate_titleitem(program) for program in programs + ] + + kodiutils.show_listing(listing, 30003, content='tvshows') + + def show_recommendations(self): + """ Shows the recommendations """ + # "Meest bekeken" has a specific API endpoint, the other categories are scraped from the website. + listing = [ + TitleItem(title='Meest bekeken', + path=kodiutils.url_for('show_recommendations_category', category='meest-bekeken'), + info_dict={ + 'title': 'Meest bekeken', + }) + ] + + recommendations = self._api.get_recommendation_categories() + for category in recommendations: + listing.append(TitleItem(title=category.title, + path=kodiutils.url_for('show_recommendations_category', category=category.uuid), + info_dict={ + 'title': category.title, + })) + + kodiutils.show_listing(listing, 30005, content='tvshows') + + def show_recommendations_category(self, uuid): + """ Shows the a category of the recommendations """ + if uuid == 'meest-bekeken': + programs = self._api.get_popular_programs() + episodes = [] + else: + recommendations = self._api.get_recommendation_categories() + category = next(category for category in recommendations if category.uuid == uuid) + programs = category.programs + episodes = category.episodes + + listing = [] + for episode in episodes: + title_item = Menu.generate_titleitem(episode) + title_item.info_dict['title'] = episode.program_title + ' - ' + title_item.title + listing.append(title_item) + + for program in programs: + listing.append(Menu.generate_titleitem(program)) + + kodiutils.show_listing(listing, 30005, content='tvshows') + def show_mylist(self): """ Show all the programs of all channels """ try: diff --git a/resources/lib/modules/channels.py b/resources/lib/modules/channels.py index 3cb3649..88b4eee 100644 --- a/resources/lib/modules/channels.py +++ b/resources/lib/modules/channels.py @@ -103,20 +103,6 @@ class Channels: ) ) - # listing.append( - # TitleItem( - # title=kodiutils.localize(30057, channel=channel_info.get('name')), # Categories for {channel} - # path=kodiutils.url_for('show_channel_categories', channel=channel), - # art_dict={ - # 'icon': 'DefaultGenre.png', - # 'fanart': fanart, - # }, - # info_dict={ - # 'plot': kodiutils.localize(30058, channel=channel_info.get('name')), # Browse the Categories for {channel} - # } - # ) - # ) - # Add YouTube channels if kodiutils.get_cond_visibility('System.HasAddon(plugin.video.youtube)') != 0: for youtube in channel_info.get('youtube', []): @@ -131,58 +117,3 @@ class Channels: ) kodiutils.show_listing(listing, 30007, sort=['unsorted']) - - # def show_channel_categories(self, channel): - # """ Shows the categories of a channel - # :type channel: str - # """ - # categories = self._api.get_categories(channel) - # - # listing = [ - # TitleItem( - # title=category.title, - # path=kodiutils.url_for('show_channel_category', channel=category.channel, category=category.uuid), - # art_dict={ - # 'icon': 'DefaultGenre.png', - # }, - # ) for category in categories - # ] - # - # kodiutils.show_listing(listing, 30007, sort=['unsorted']) - - # def show_channel_category(self, channel, category_id): - # """ Shows a selected category of a channel - # :type channel: str - # :type category_id: str - # """ - # categories = self._api.get_categories(channel) - # - # # Extract selected category - # category = next(category for category in categories if category.uuid == category_id) - # if not category: - # raise Exception('Unknown category') - # - # # Add programs - # listing_programs = [] - # for item in category.programs: - # program = self._api.get_program(item.path, CACHE_ONLY) # Get program details, but from cache only - # - # if program: - # listing_programs.append(Menu.generate_titleitem(program)) - # else: - # listing_programs.append(Menu.generate_titleitem(item)) - # - # # Add episodes - # listing_episodes = [] - # for item in category.episodes: - # # We don't have the Program Name without making a request to the page, so we use CACHE_AUTO instead of CACHE_ONLY. - # # This will make a request for each item in this view (about 12 items), but it goes quite fast. - # # Results are cached, so this will only happen once. - # episode = self._api.get_episode(item.path, CACHE_AUTO) - # - # if episode: - # listing_episodes.append(Menu.generate_titleitem(episode)) - # else: - # listing_episodes.append(Menu.generate_titleitem(item)) - # - # kodiutils.show_listing(listing_programs + listing_episodes, 30007, content='tvshows', sort=['unsorted']) diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py index 8ae2fcf..338d880 100644 --- a/resources/lib/modules/menu.py +++ b/resources/lib/modules/menu.py @@ -8,6 +8,11 @@ from resources.lib.kodiutils import TitleItem from resources.lib.viervijfzes import STREAM_DICT from resources.lib.viervijfzes.content import Episode, Program +try: # Python 3 + from urllib.parse import quote +except ImportError: # Python 2 + from urllib import quote + class Menu: """ Menu code """ @@ -41,6 +46,28 @@ class Menu: plot=kodiutils.localize(30008), ) ), + TitleItem( + title=kodiutils.localize(30003), # Catalog + path=kodiutils.url_for('show_categories'), + art_dict=dict( + icon='DefaultGenre.png', + fanart=kodiutils.get_addon_info('fanart'), + ), + info_dict=dict( + plot=kodiutils.localize(30004), + ) + ), + TitleItem( + title=kodiutils.localize(30005), # Recommendations + path=kodiutils.url_for('show_recommendations'), + art_dict=dict( + icon='DefaultFavourites.png', + fanart=kodiutils.get_addon_info('fanart'), + ), + info_dict=dict( + plot=kodiutils.localize(30006), + ) + ), TitleItem( title=kodiutils.localize(30011), # My List path=kodiutils.url_for('show_mylist'), @@ -94,9 +121,12 @@ class Menu: 'season': len(item.seasons) if item.seasons else None, }) + visible = True if isinstance(item.episodes, list) and not item.episodes: # We know that we don't have episodes title = '[COLOR gray]' + item.title + '[/COLOR]' + visible = kodiutils.get_setting_bool('interface_show_unavailable') + else: # We have episodes, or we don't know it title = item.title @@ -126,7 +156,8 @@ class Menu: path=kodiutils.url_for('show_catalog_program', program=item.path), context_menu=context_menu, art_dict=art_dict, - info_dict=info_dict) + info_dict=info_dict, + visible=visible) # # Episode @@ -146,11 +177,6 @@ class Menu: }) if item.path: - try: # Python 3 - from urllib.parse import quote - except ImportError: # Python 2 - from urllib import quote - # We don't have an UUID, and first need to fetch the video information from the page path = kodiutils.url_for('play_from_page', page=quote(item.path, safe='')) else: diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index 2f79eeb..543d9d3 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -12,6 +12,11 @@ from resources.lib.viervijfzes.auth import AuthApi from resources.lib.viervijfzes.aws.cognito_idp import AuthenticationException, InvalidLoginException from resources.lib.viervijfzes.content import ContentApi, GeoblockedException, UnavailableException +try: # Python 3 + from urllib.parse import quote, urlencode +except ImportError: # Python 2 + from urllib import quote, urlencode + _LOGGER = logging.getLogger(__name__) @@ -142,11 +147,6 @@ class Player: :param str key_value: :rtype: str """ - try: # Python 3 - from urllib.parse import quote, urlencode - except ImportError: # Python 2 - from urllib import quote, urlencode - header = '' if key_headers: header = urlencode(key_headers) diff --git a/resources/lib/modules/tvguide.py b/resources/lib/modules/tvguide.py index 7225e0c..c6908f5 100644 --- a/resources/lib/modules/tvguide.py +++ b/resources/lib/modules/tvguide.py @@ -13,6 +13,11 @@ from resources.lib.viervijfzes import STREAM_DICT from resources.lib.viervijfzes.content import UnavailableException from resources.lib.viervijfzes.epg import EpgApi +try: # Python 3 + from urllib.parse import quote +except ImportError: # Python 2 + from urllib import quote + _LOGGER = logging.getLogger(__name__) @@ -104,11 +109,6 @@ class TvGuide: kodiutils.end_of_directory() return - try: # Python 3 - from urllib.parse import quote - except ImportError: # Python 2 - from urllib import quote - listing = [] for program in programs: if program.program_url: diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index a16af5d..1cf1a77 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, unicode_literals +import hashlib import json import logging import os @@ -208,7 +209,7 @@ class ContentApi: return data # Fetch listing from cache or update if needed - data = self._handle_cache(key=['programs'], cache_mode=cache, update=update, ttl=5 * 60) + data = self._handle_cache(key=['programs'], cache_mode=cache, update=update, ttl=30 * 60) # 30 minutes if not data: return [] @@ -381,75 +382,145 @@ class ContentApi: stream_type=STREAM_HLS, ) - # def get_categories(self): - # """ Get a list of all categories. - # :rtype list[Category] - # """ - # # Load webpage - # raw_html = self._get_url(self.SITE_URL) - # - # # Categories regexes - # regex_articles = re.compile(r']+)>(.*?)', re.DOTALL) - # regex_submenu_id = re.compile(r'data-submenu-id="([^"]*)"') # splitted since the order might change - # regex_submenu_title = re.compile(r'data-submenu-title="([^"]*)"') - # - # categories = [] - # for result in regex_articles.finditer(raw_html): - # article_info_html = result.group(1) - # article_html = result.group(2) - # category_title = regex_submenu_title.search(article_info_html).group(1) - # category_id = regex_submenu_id.search(article_info_html).group(1) - # - # # Skip empty categories or 'All programs' - # if not category_id or category_id == 'programmas': - # continue - # - # # Extract items - # programs = self._extract_programs(article_html, channel) - # episodes = self._extract_videos(article_html) - # categories.append(Category(uuid=category_id, channel=channel, title=category_title, programs=programs, episodes=episodes)) - # - # return categories + def get_program_tree(self, cache=CACHE_AUTO): + """ Get a content tree with information about all the programs. + :type cache: str + :rtype dict + """ - # @staticmethod - # def _extract_programs(html, channel): - # """ Extract Programs from HTML code """ - # # Item regexes - # regex_item = re.compile(r']+?href="(?P[^"]+)"[^>]+?>' - # r'.*?

(?P[^<]*)</span></h3>.*?' - # r'</a>', re.DOTALL) - # - # # Extract items - # programs = [] - # for item in regex_item.finditer(html): - # path = item.group('path') - # if path.startswith('/video'): - # continue - # - # title = unescape(item.group('title')) - # - # # Program - # programs.append(Program( - # path=path.lstrip('/'), - # channel=channel, - # title=title, - # )) - # - # return programs + def update(): + """ Fetch the content tree """ + response = self._get_url(self.SITE_URL + '/api/content_tree') + return json.loads(response) + + # Fetch listing from cache or update if needed + data = self._handle_cache(key=['content_tree'], cache_mode=cache, update=update, ttl=5 * 60) # 5 minutes + + return data + + def get_popular_programs(self, brand=None): + """ Get a list of popular programs. + :rtype list[Program] + """ + if brand: + response = self._get_url(self.SITE_URL + '/api/programs/popular/%s' % brand) + else: + response = self._get_url(self.SITE_URL + '/api/programs/popular') + data = json.loads(response) + + programs = [] + for program in data: + programs.append(self._parse_program_data(program)) + + return programs + + def get_categories(self): + """ Return a list of categories. + :rtype list[Category] + """ + content_tree = self.get_program_tree() + + categories = [] + for category_id, category_name in content_tree.get('categories').items(): + categories.append(Category(uuid=category_id, + title=category_name)) + + return categories + + def get_category_content(self, category_id): + """ Return a category. + :type category_id: int + :rtype list[Program] + """ + content_tree = self.get_program_tree() + + # Find out all the program_id's of the requested category + program_ids = [key for key, value in content_tree.get('programs').items() if value.get('category') == category_id] + + # Filter out the list of all programs to only keep the one of the requested category + return [program for program in self.get_programs() if program.uuid in program_ids] + + def get_recommendation_categories(self): + """ Get a list of all categories. + :rtype list[Category] + """ + # Load all programs + all_programs = self.get_programs() + + # Load webpage + raw_html = self._get_url(self.SITE_URL) + + # Categories regexes + regex_articles = re.compile(r'<article[^>]+>(.*?)</article>', re.DOTALL) + regex_category = re.compile(r'<h1.*?>(.*?)</h1>(?:.*?<div class="visually-hidden">(.*?)</div>)?', re.DOTALL) + + categories = [] + for result in regex_articles.finditer(raw_html): + article_html = result.group(1) + + match_category = regex_category.search(article_html) + category_title = match_category.group(1).strip() + if match_category.group(2): + category_title += ' [B]%s[/B]' % match_category.group(2).strip() + + # Extract programs and lookup in all_programs so we have more metadata + programs = [] + for program in self._extract_programs(article_html): + try: + rich_program = next(rich_program for rich_program in all_programs if rich_program.path == program.path) + programs.append(rich_program) + except StopIteration: + programs.append(program) + + episodes = self._extract_videos(article_html) + + categories.append( + Category(uuid=hashlib.md5(category_title.encode('utf-8')).hexdigest(), title=category_title, programs=programs, episodes=episodes)) + + return categories + + @staticmethod + def _extract_programs(html): + """ Extract Programs from HTML code + :type html: str + :rtype list[Program] + """ + # Item regexes + regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>' + r'.*?<h3 class="poster-teaser__title">(?P<title>[^<]*)</h3>.*?data-background-image="(?P<image>.*?)".*?' + r'</a>', re.DOTALL) + + # Extract items + programs = [] + for item in regex_item.finditer(html): + path = item.group('path') + if path.startswith('/video'): + continue + + # Program + programs.append(Program( + path=path.lstrip('/'), + title=unescape(item.group('title')), + cover=unescape(item.group('image')), + )) + + return programs @staticmethod def _extract_videos(html): - """ Extract videos from HTML code """ + """ Extract videos from HTML code + :type html: str + :rtype list[Episode] + """ # Item regexes regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>.*?</a>', re.DOTALL) - # Episode regexes - regex_episode_title = re.compile(r'<h3 class="(?:poster|card|image)-teaser__title">(?:<span>)?([^<]*)(?:</span>)?</h3>') - regex_episode_program = re.compile(r'<div class="card-teaser__label">([^<]*)</div>') + regex_episode_program = re.compile(r'<h3 class="episode-teaser__subtitle">([^<]*)</h3>') + regex_episode_title = re.compile(r'<(?:div|h3) class="(?:poster|card|image|episode)-teaser__title">(?:<span>)?([^<]*)(?:</span>)?</(?:div|h3)>') regex_episode_duration = re.compile(r'data-duration="([^"]*)"') - regex_episode_video_id = re.compile(r'data-videoid="([^"]*)"') + regex_episode_video_id = re.compile(r'data-video-id="([^"]*)"') regex_episode_image = re.compile(r'data-background-image="([^"]*)"') - regex_episode_timestamp = re.compile(r'data-timestamp="([^"]*)"') + regex_episode_badge = re.compile(r'<div class="(?:poster|card|image|episode)-teaser__badge badge">([^<]*)</div>') # Extract items episodes = [] @@ -463,7 +534,7 @@ class ContentApi: except AttributeError: continue - # This is not a episode + # This is not a video if not path.startswith('/video'): continue @@ -472,35 +543,42 @@ class ContentApi: except AttributeError: _LOGGER.warning('Found no episode_program for %s', title) episode_program = None + try: episode_duration = int(regex_episode_duration.search(item_html).group(1)) except AttributeError: _LOGGER.warning('Found no episode_duration for %s', title) episode_duration = None + try: episode_video_id = regex_episode_video_id.search(item_html).group(1) except AttributeError: _LOGGER.warning('Found no episode_video_id for %s', title) episode_video_id = None + try: episode_image = unescape(regex_episode_image.search(item_html).group(1)) except AttributeError: _LOGGER.warning('Found no episode_image for %s', title) episode_image = None + try: - episode_timestamp = int(regex_episode_timestamp.search(item_html).group(1)) + episode_badge = unescape(regex_episode_badge.search(item_html).group(1)) except AttributeError: - _LOGGER.warning('Found no episode_timestamp for %s', title) - episode_timestamp = None + episode_badge = None + + description = title + if episode_badge: + description += "\n\n[B]%s[/B]" % episode_badge # Episode episodes.append(Episode( path=path.lstrip('/'), channel='', # TODO title=title, + description=html_to_kodi(description), duration=episode_duration, uuid=episode_video_id, - aired=datetime.fromtimestamp(episode_timestamp) if episode_timestamp else None, cover=episode_image, program_title=episode_program, )) @@ -519,7 +597,7 @@ class ContentApi: path=data['link'].lstrip('/'), channel=data['pageInfo']['brand'], title=data['title'], - description=data['description'], + description=html_to_kodi(data['description']), aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate')), cover=data['images']['poster'], background=data['images']['hero'], diff --git a/resources/settings.xml b/resources/settings.xml index 0043bb6..2f297c4 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -5,6 +5,10 @@ <setting label="30802" type="text" id="username"/> <setting label="30803" type="text" id="password" option="hidden"/> </category> + <category label="30820"> <!-- Interface --> + <setting label="30820" type="lsep"/> <!-- Interface --> + <setting label="30821" type="bool" id="interface_show_unavailable" default="true"/> + </category> <category label="30840"> <!-- Integrations --> <setting label="30841" type="lsep"/> <!-- IPTV Manager --> <setting label="30842" type="action" action="InstallAddon(service.iptv.manager)" option="close" visible="!System.HasAddon(service.iptv.manager)"/> <!-- Install IPTV Manager add-on --> diff --git a/tests/test_api.py b/tests/test_api.py index 6fcb3d0..1767a72 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,7 +11,7 @@ import unittest import resources.lib.kodiutils as kodiutils from resources.lib.viervijfzes import ResolvedStream from resources.lib.viervijfzes.auth import AuthApi -from resources.lib.viervijfzes.content import ContentApi, Program, Episode, CACHE_PREVENT +from resources.lib.viervijfzes.content import ContentApi, Program, Episode, CACHE_PREVENT, Category _LOGGER = logging.getLogger(__name__) @@ -27,10 +27,24 @@ class TestApi(unittest.TestCase): self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], Program) - # def test_categories(self): - # categories = self._api.get_categories() - # self.assertIsInstance(categories, list) - # self.assertIsInstance(categories[0], Category) + def test_popular_programs(self): + for brand in [None, 'vier', 'vijf', 'zes', 'goplay']: + programs = self._api.get_popular_programs(brand) + self.assertIsInstance(programs, list) + self.assertIsInstance(programs[0], Program) + + def test_recommendations(self): + categories = self._api.get_recommendation_categories() + self.assertIsInstance(categories, list) + + def test_categories(self): + categories = self._api.get_categories() + self.assertIsInstance(categories, list) + self.assertIsInstance(categories[0], Category) + + programs = self._api.get_category_content(int(categories[0].uuid)) + self.assertIsInstance(programs, list) + self.assertIsInstance(programs[0], Program) def test_episodes(self): for program in ['auwch', 'zo-man-zo-vrouw']: diff --git a/tests/test_routing.py b/tests/test_routing.py index 97c0898..e6261be 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -34,6 +34,9 @@ class TestRouting(unittest.TestCase): def test_catalog_menu(self): routing.run([routing.url_for(addon.show_catalog), '0', '']) + def test_recommendations_menu(self): + routing.run([routing.url_for(addon.show_recommendations), '0', '']) + def test_catalog_channel_menu(self): routing.run([routing.url_for(addon.show_channel_catalog, channel='Play4'), '0', ''])