diff --git a/.travis.yml b/.travis.yml index 5a1d1576b..d3a70fcd9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ script: - tox - tox -e flake8 - pylint *.py resources/lib/ test/ -- python test/vrtplayertests.py -- python test/apihelpertests.py +#- python test/vrtplayertests.py +#- python test/apihelpertests.py - python test/tvguidetests.py +- python test/searchtests.py diff --git a/addon.py b/addon.py index f0e0843ae..dbc253791 100644 --- a/addon.py +++ b/addon.py @@ -26,46 +26,58 @@ def router(params_string): params = dict(parse_qsl(params_string)) action = params.get('action') - kodi_wrapper = kodiwrapper.KodiWrapper(_ADDON_HANDLE, _ADDON_URL, addon) - kodi_wrapper.log_access(_ADDON_URL, params_string) + _kodiwrapper = kodiwrapper.KodiWrapper(_ADDON_HANDLE, _ADDON_URL, addon) + _kodiwrapper.log_access(_ADDON_URL, params_string) if action == actions.CLEAR_COOKIES: from resources.lib.vrtplayer import tokenresolver - token_resolver = tokenresolver.TokenResolver(kodi_wrapper) - token_resolver.reset_cookies() + _tokenresolver = tokenresolver.TokenResolver(_kodiwrapper) + _tokenresolver.reset_cookies() return if action == actions.LISTING_TVGUIDE: from resources.lib.vrtplayer import tvguide - tv_guide = tvguide.TVGuide(kodi_wrapper) - tv_guide.show_tvguide(params) + _tvguide = tvguide.TVGuide(_kodiwrapper) + _tvguide.show_tvguide(params) + return + if action == actions.FOLLOW: + from resources.lib.vrtplayer import favorites + _favorites = favorites.Favorites(_kodiwrapper) + _favorites.follow(path=params.get('program')) + return + if action == actions.UNFOLLOW: + from resources.lib.vrtplayer import favorites + _favorites = favorites.Favorites(_kodiwrapper) + _favorites.unfollow(path=params.get('program')) return from resources.lib.vrtplayer import vrtapihelper, vrtplayer - api_helper = vrtapihelper.VRTApiHelper(kodi_wrapper) - vrt_player = vrtplayer.VRTPlayer(kodi_wrapper, api_helper) + _apihelper = vrtapihelper.VRTApiHelper(_kodiwrapper) + _vrtplayer = vrtplayer.VRTPlayer(_kodiwrapper, _apihelper) if action == actions.PLAY: - vrt_player.play(params) + _vrtplayer.play(params) elif action == actions.LISTING_AZ_TVSHOWS: - vrt_player.show_tvshow_menu_items() + _vrtplayer.show_tvshow_menu_items(filtered=params.get('filtered')) elif action == actions.LISTING_CATEGORIES: - vrt_player.show_category_menu_items() + _vrtplayer.show_category_menu_items() elif action == actions.LISTING_CATEGORY_TVSHOWS: - vrt_player.show_tvshow_menu_items(category=params.get('category')) + _vrtplayer.show_tvshow_menu_items(category=params.get('category')) elif action == actions.LISTING_CHANNELS: - vrt_player.show_channels_menu_items(channel=params.get('channel')) + _vrtplayer.show_channels_menu_items(channel=params.get('channel')) + elif action == actions.LISTING_FAVORITES: + _vrtplayer.show_favourites_menu_items() elif action == actions.LISTING_LIVE: - vrt_player.show_livestream_items() + _vrtplayer.show_livestream_items() elif action == actions.LISTING_EPISODES: - vrt_player.show_episodes(path=params.get('video_url')) + _vrtplayer.show_episodes(path=params.get('video_url')) elif action == actions.LISTING_ALL_EPISODES: - vrt_player.show_all_episodes(path=params.get('video_url')) + _vrtplayer.show_all_episodes(path=params.get('video_url')) elif action == actions.LISTING_RECENT: - vrt_player.show_recent(page=params.get('page', 1)) + _vrtplayer.show_recent(page=params.get('page', 1), filtered=params.get('filtered')) elif action == actions.SEARCH: - vrt_player.search(search_string=params.get('query'), page=params.get('page', 1)) + _vrtplayer.search(search_string=params.get('query'), page=params.get('page', 1)) else: - vrt_player.show_main_menu_items() + _vrtplayer.show_main_menu_items() if __name__ == '__main__': diff --git a/addon.xml b/addon.xml index b6a4ea150..b184fcb24 100644 --- a/addon.xml +++ b/addon.xml @@ -15,6 +15,7 @@ video + Watch videos from VRT NU diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index faae7aa35..b5cb95741 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -115,6 +115,14 @@ msgctxt "#30061" msgid "Using a SOCKS proxy requires the PySocks library (script.module.pysocks) installed." msgstr "Using a SOCKS proxy requires the PySocks library (script.module.pysocks) installed." +msgctxt "#30078" +msgid "My programs" +msgstr "My programs" + +msgctxt "#30079" +msgid "Browse only the programs you follow" +msgstr "Browse only the programs you follow" + msgctxt "#30080" msgid "A-Z" msgstr "A-Z" @@ -251,3 +259,26 @@ msgctxt "#30334" msgid "In 2 days" msgstr "In 2 days" +msgctxt "#30411" +msgid "Follow" +msgstr "Follow" + +msgctxt "#30412" +msgid "Unfollow" +msgstr "Unfollow" + +msgctxt "#30420" +msgid "My A-Z" +msgstr "My A-Z" + +msgctxt "#30421" +msgid "Alphabetically sorted list of My TV programs" +msgstr "Alphabetically sorted list of My TV programs" + +msgctxt "#30422" +msgid "My recent items" +msgstr "My recent items" + +msgctxt "#30423" +msgid "Recently published episodes of My TV programs" +msgstr "Recently published episodes of My TV programs" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 412ccc029..a4460131f 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -259,3 +259,11 @@ msgstr "Morgen" msgctxt "#30334" msgid "In 2 days" msgstr "Overmorgen" + +msgctxt "#30411" +msgid "Follow" +msgstr "Volg" + +msgctxt "#30412" +msgid "Unfollow" +msgstr "Vergeet" diff --git a/resources/lib/helperobjects/helperobjects.py b/resources/lib/helperobjects/helperobjects.py index ec4f13697..a7b862d74 100644 --- a/resources/lib/helperobjects/helperobjects.py +++ b/resources/lib/helperobjects/helperobjects.py @@ -7,12 +7,13 @@ class TitleItem: - def __init__(self, title, url_dict, is_playable, art_dict=None, video_dict=None): + def __init__(self, title, url_dict, is_playable, art_dict=None, video_dict=None, context_menu=None): self.title = title self.url_dict = url_dict self.is_playable = is_playable self.art_dict = art_dict self.video_dict = video_dict + self.context_menu = context_menu class Credentials: diff --git a/resources/lib/kodiwrappers/kodiwrapper.py b/resources/lib/kodiwrappers/kodiwrapper.py index 81422bf91..7c9031bcf 100644 --- a/resources/lib/kodiwrappers/kodiwrapper.py +++ b/resources/lib/kodiwrappers/kodiwrapper.py @@ -3,6 +3,8 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, unicode_literals +from contextlib import contextmanager + import xbmc import xbmcplugin @@ -54,15 +56,15 @@ def __init__(self, handle, url, addon): self._max_log_level = log_levels.get(self.get_setting('max_log_level'), 3) self._usemenucaching = self.get_setting('usemenucaching') == 'true' - def show_listing(self, list_items, sort='unsorted', ascending=True, content_type=None, cache=None): + def show_listing(self, list_items, sort='unsorted', ascending=True, content=None, cache=None): import xbmcgui listing = [] if cache is None: cache = self._usemenucaching - if content_type: - xbmcplugin.setContent(self._handle, content=content_type) + if content: + xbmcplugin.setContent(self._handle, content=content) # FIXME: Since there is no way to influence descending order, we force it here if not ascending: @@ -101,6 +103,9 @@ def show_listing(self, list_items, sort='unsorted', ascending=True, content_type if title_item.video_dict: list_item.setInfo(type='video', infoLabels=title_item.video_dict) + if title_item.context_menu: + list_item.addContextMenuItems(title_item.context_menu) + listing.append((url, list_item, not title_item.is_playable)) ok = xbmcplugin.addDirectoryItems(self._handle, listing, len(listing)) @@ -253,11 +258,18 @@ def check_if_path_exists(self, path): import xbmcvfs return xbmcvfs.exists(path) - def open_path(self, path): - import json - return json.loads(open(path, 'r').read()) + @contextmanager + def open_file(self, path, flags='r'): + import xbmcvfs + f = xbmcvfs.File(path, flags) + yield f + f.close() + + def stat_file(self, path): + import xbmcvfs + return xbmcvfs.Stat(path) - def delete_path(self, path): + def delete_file(self, path): import xbmcvfs return xbmcvfs.delete(path) diff --git a/resources/lib/vrtplayer/actions.py b/resources/lib/vrtplayer/actions.py index 8a4310c67..ba144e59c 100644 --- a/resources/lib/vrtplayer/actions.py +++ b/resources/lib/vrtplayer/actions.py @@ -5,14 +5,17 @@ from __future__ import absolute_import, division, unicode_literals CLEAR_COOKIES = 'clearcookies' +FOLLOW = 'follow' LISTING_ALL_EPISODES = 'listingallepisodes' LISTING_AZ_TVSHOWS = 'listingaztvshows' LISTING_CATEGORIES = 'listingcategories' LISTING_CATEGORY_TVSHOWS = 'listingcategorytvshows' LISTING_CHANNELS = 'listingchannels' LISTING_EPISODES = 'listingepisodes' +LISTING_FAVORITES = 'favorites' LISTING_LIVE = 'listinglive' LISTING_RECENT = 'listingrecent' LISTING_TVGUIDE = 'listingtvguide' PLAY = 'play' SEARCH = 'search' +UNFOLLOW = 'unfollow' diff --git a/resources/lib/vrtplayer/favorites.py b/resources/lib/vrtplayer/favorites.py new file mode 100644 index 000000000..3e2f1d86e --- /dev/null +++ b/resources/lib/vrtplayer/favorites.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, unicode_literals +import json +import time + +from resources.lib.vrtplayer import tokenresolver + +try: + from urllib.request import build_opener, install_opener, ProxyHandler, Request, urlopen +except ImportError: + from urllib2 import build_opener, install_opener, ProxyHandler, Request, urlopen + + +class Favorites: + + def __init__(self, _kodiwrapper): + self._kodiwrapper = _kodiwrapper + self._tokenresolver = tokenresolver.TokenResolver(_kodiwrapper) + self._proxies = _kodiwrapper.get_proxies() + install_opener(build_opener(ProxyHandler(self._proxies))) + self._cache_file = _kodiwrapper.get_userdata_path() + 'favorites.json' + self._favorites = self.get_favorites() + self.write_favorites() + + def get_favorites(self): + if self._kodiwrapper.check_if_path_exists(self._cache_file): + if self._kodiwrapper.stat_file(self._cache_file).st_mtime() > time.time() - (5 * 60): + with self._kodiwrapper.open_file(self._cache_file) as f: + return json.loads(f.read()) + xvrttoken = self._tokenresolver.get_xvrttoken() + headers = {'Content-Type': 'application/json', 'Cookie': 'X-VRT-Token=' + xvrttoken} + self._kodiwrapper.log_notice('URL get: https://video-user-data.vrt.be/favorites/', 'Verbose') + req = Request('https://video-user-data.vrt.be/favorites/', headers=headers) + return json.loads(urlopen(req).read()) + + def set_favorite(self, path, value=True): + if value is not self.is_favorite(path): + xvrttoken = self._tokenresolver.get_xvrttoken() + headers = {'Content-Type': 'application/json', 'Cookie': 'X-VRT-Token=' + xvrttoken} + payload = dict(isFavorite=value, programUrl=path, title=path, whatsonId='0') + self._kodiwrapper.log_notice('URL post: https://video-user-data.vrt.be/favorites/%s' % self.uuid(path), 'Verbose') + self._kodiwrapper.log_notice('Payload: %s' % json.dumps(payload), 'Debug') + req = Request('https://video-user-data.vrt.be/favorites/%s' % self.uuid(path), data=json.dumps(payload), headers=headers) + # TODO: Test that we get a HTTP 200, otherwise log and fail graceful + result = urlopen(req) + if result.getcode() != 200: + self._kodiwrapper.log_error("Failed to follow program '%s' at VRT NU" % path) + # NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it + self._favorites[self.uuid(path)] = dict(value=payload) + self.write_favorites() + + def write_favorites(self): + with self._kodiwrapper.open_file(self._cache_file, 'w') as f: + f.write(json.dumps(self._favorites)) + + def is_favorite(self, path): + value = False + favorite = self._favorites.get(self.uuid(path)) + if favorite: + value = favorite.get('value', dict(isFavorite=False)).get('isFavorite', False) + return value + + def follow(self, path): + self.set_favorite(path, True) + + def unfollow(self, path): + self.set_favorite(path, False) + + def uuid(self, path): + return path.replace('/', '').replace('-', '') + + def name(self, path): + return path.replace('.relevant/', '/').split('/')[-2] + + def names(self): + return [self.name(p.get('value').get('programUrl')) for p in self._favorites.values() if p.get('value').get('isFavorite')] + + def titles(self): + return [p.get('value').get('title') for p in self._favorites.values() if p.get('value').get('isFavorite')] diff --git a/resources/lib/vrtplayer/statichelper.py b/resources/lib/vrtplayer/statichelper.py index cb5a7a042..bddc50e52 100644 --- a/resources/lib/vrtplayer/statichelper.py +++ b/resources/lib/vrtplayer/statichelper.py @@ -29,6 +29,12 @@ def convert_html_to_kodilabel(text): return unescape(text).strip() +def unique_path(path): + if path.startswith('//www.vrt.be/vrtnu'): + return path.replace('//www.vrt.be/vrtnu/', 'vrtnu/').replace('.relevant/', '/') + return path + + def shorten_link(url): if url is None: return None diff --git a/resources/lib/vrtplayer/streamservice.py b/resources/lib/vrtplayer/streamservice.py index 5cc1a5905..ce3faa5bb 100644 --- a/resources/lib/vrtplayer/streamservice.py +++ b/resources/lib/vrtplayer/streamservice.py @@ -13,8 +13,8 @@ from urllib.error import HTTPError from urllib.request import build_opener, install_opener, urlopen, ProxyHandler except ImportError: - from urllib2 import build_opener, install_opener, urlopen, ProxyHandler, quote, HTTPError from urllib import urlencode # pylint: disable=ungrouped-imports + from urllib2 import build_opener, install_opener, urlopen, ProxyHandler, quote, HTTPError class StreamService: @@ -23,23 +23,23 @@ class StreamService: _VUALTO_API_URL = 'https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v1' _CLIENT = 'vrtvideo' - def __init__(self, kodi_wrapper, token_resolver): - self._kodi_wrapper = kodi_wrapper - self._proxies = self._kodi_wrapper.get_proxies() + def __init__(self, _kodiwrapper, _tokenresolver): + self._kodiwrapper = _kodiwrapper + self._proxies = _kodiwrapper.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) - self.token_resolver = token_resolver + self._tokenresolver = _tokenresolver self._create_settings_dir() - self._can_play_drm = self._kodi_wrapper.can_play_drm() + self._can_play_drm = _kodiwrapper.can_play_drm() self._license_url = None def _get_license_url(self): - self._kodi_wrapper.log_notice('URL get: ' + self._VUPLAY_API_URL, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + self._VUPLAY_API_URL, 'Verbose') self._license_url = json.loads(urlopen(self._VUPLAY_API_URL).read()).get('drm_providers', dict()).get('widevine', dict()).get('la_url') def _create_settings_dir(self): - settingsdir = self._kodi_wrapper.get_userdata_path() - if not self._kodi_wrapper.check_if_path_exists(settingsdir): - self._kodi_wrapper.make_dir(settingsdir) + settingsdir = self._kodiwrapper.get_userdata_path() + if not self._kodiwrapper.check_if_path_exists(settingsdir): + self._kodiwrapper.make_dir(settingsdir) def _get_license_key(self, key_url, key_type='R', key_headers=None, key_value=None): ''' Generates a propery license key value @@ -87,7 +87,7 @@ def _get_api_data(self, video): publication_id = video.get('publication_id') # Prepare api_data for on demand streams by video_id and publication_id if video_id and publication_id: - xvrttoken = self.token_resolver.get_xvrttoken() + xvrttoken = self._tokenresolver.get_xvrttoken() api_data = apidata.ApiData(self._CLIENT, self._VUALTO_API_URL, video_id, publication_id + quote('$'), xvrttoken, False) # Prepare api_data for livestreams by video_id, e.g. vualto_strubru, vualto_mnm elif video_id and not video_url: @@ -100,7 +100,7 @@ def _get_api_data(self, video): def _webscrape_api_data(self, video_url): '''Scrape api data from VRT NU html page''' from bs4 import BeautifulSoup, SoupStrainer - self._kodi_wrapper.log_notice('URL get: ' + video_url, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + video_url, 'Verbose') html_page = urlopen(video_url).read() strainer = SoupStrainer('div', {'class': 'cq-dd-vrtvideo'}) soup = BeautifulSoup(html_page, 'html.parser', parse_only=strainer) @@ -108,12 +108,12 @@ def _webscrape_api_data(self, video_url): video_data = soup.find(lambda tag: tag.name == 'div' and tag.get('class') == ['vrtvideo']).attrs except Exception as e: # Web scraping failed, log error - self._kodi_wrapper.log_error('Web scraping api data failed: %s' % e) + self._kodiwrapper.log_error('Web scraping api data failed: %s' % e) return None # Web scraping failed, log error if not video_data: - self._kodi_wrapper.log_error('Web scraping api data failed, empty video_data') + self._kodiwrapper.log_error('Web scraping api data failed, empty video_data') return None # Store required html data attributes @@ -129,10 +129,10 @@ def _webscrape_api_data(self, video_url): else: is_live_stream = False publication_id += quote('$') - xvrttoken = self.token_resolver.get_xvrttoken() + xvrttoken = self._tokenresolver.get_xvrttoken() if client is None or media_api_url is None or (video_id is None and publication_id is None): - self._kodi_wrapper.log_error('Web scraping api data failed, required attributes missing') + self._kodiwrapper.log_error('Web scraping api data failed, required attributes missing') return None return apidata.ApiData(client, media_api_url, video_id, publication_id, xvrttoken, is_live_stream) @@ -140,16 +140,16 @@ def _webscrape_api_data(self, video_url): def _get_video_json(self, api_data): token_url = api_data.media_api_url + '/tokens' if api_data.is_live_stream: - playertoken = self.token_resolver.get_live_playertoken(token_url, api_data.xvrttoken) + playertoken = self._tokenresolver.get_live_playertoken(token_url, api_data.xvrttoken) else: - playertoken = self.token_resolver.get_ondemand_playertoken(token_url, api_data.xvrttoken) + playertoken = self._tokenresolver.get_ondemand_playertoken(token_url, api_data.xvrttoken) # Construct api_url and get video json video_json = None if playertoken: api_url = api_data.media_api_url + '/videos/' + api_data.publication_id + \ api_data.video_id + '?vrtPlayerToken=' + playertoken + '&client=' + api_data.client - self._kodi_wrapper.log_notice('URL get: ' + api_url, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + api_url, 'Verbose') try: video_json = json.loads(urlopen(api_url).read()) except HTTPError as e: @@ -158,9 +158,9 @@ def _get_video_json(self, api_data): return video_json def _handle_error(self, video_json): - self._kodi_wrapper.log_error(video_json.get('message')) - message = self._kodi_wrapper.get_localized_string(30054) - self._kodi_wrapper.show_ok_dialog('', message) + self._kodiwrapper.log_error(video_json.get('message')) + message = self._kodiwrapper.get_localized_string(30054) + self._kodiwrapper.show_ok_dialog('', message) @staticmethod def _fix_virtualsubclip(stream_dict, duration): @@ -205,19 +205,19 @@ def get_stream(self, video, retry=False, api_data=None): return self._select_stream(stream_dict, vudrm_token) if video_json.get('code') in ('INCOMPLETE_ROAMING_CONFIG', 'INVALID_LOCATION'): - self._kodi_wrapper.log_error(video_json.get('message')) - roaming_xvrttoken = self.token_resolver.get_xvrttoken(True) + self._kodiwrapper.log_error(video_json.get('message')) + roaming_xvrttoken = self._tokenresolver.get_xvrttoken(True) if not retry and roaming_xvrttoken is not None: # Delete cached playertokens if api_data.is_live_stream: - self._kodi_wrapper.delete_path(self._kodi_wrapper.get_userdata_path() + 'live_vrtPlayerToken') + self._kodiwrapper.delete_file(self._kodiwrapper.get_userdata_path() + 'live_vrtPlayerToken') else: - self._kodi_wrapper.delete_path(self._kodi_wrapper.get_userdata_path() + 'ondemand_vrtPlayerToken') + self._kodiwrapper.delete_file(self._kodiwrapper.get_userdata_path() + 'ondemand_vrtPlayerToken') # Update api_data with roaming_xvrttoken and try again api_data.xvrttoken = roaming_xvrttoken return self.get_stream(video, retry=True, api_data=api_data) - message = self._kodi_wrapper.get_localized_string(30053) - self._kodi_wrapper.show_ok_dialog('', message) + message = self._kodiwrapper.get_localized_string(30053) + self._kodiwrapper.show_ok_dialog('', message) else: self._handle_error(video_json) @@ -237,24 +237,24 @@ def _try_get_drm_stream(self, stream_dict, vudrm_token): def _select_stream(self, stream_dict, vudrm_token): stream_url = None protocol = None - if vudrm_token and self._can_play_drm and self._kodi_wrapper.get_setting('usedrm') == 'true': + if vudrm_token and self._can_play_drm and self._kodiwrapper.get_setting('usedrm') == 'true': protocol = 'mpeg_dash drm' - self._kodi_wrapper.log_notice('Protocol: ' + protocol) + self._kodiwrapper.log_notice('Protocol: ' + protocol) stream_url = self._try_get_drm_stream(stream_dict, vudrm_token) if vudrm_token and stream_url is None: protocol = 'hls_aes' - self._kodi_wrapper.log_notice('Protocol: ' + protocol) + self._kodiwrapper.log_notice('Protocol: ' + protocol) stream_url = streamurls.StreamURLS(*self._select_hls_substreams(stream_dict[protocol])) if protocol in stream_dict else None - if self._kodi_wrapper.has_inputstream_adaptive_installed() and stream_url is None: + if self._kodiwrapper.has_inputstream_adaptive_installed() and stream_url is None: protocol = 'mpeg_dash' - self._kodi_wrapper.log_notice('Protocol: ' + protocol) + self._kodiwrapper.log_notice('Protocol: ' + protocol) stream_url = streamurls.StreamURLS(stream_dict[protocol], use_inputstream_adaptive=True) if protocol in stream_dict else None if stream_url is None: protocol = 'hls' - self._kodi_wrapper.log_notice('Protocol: ' + protocol) + self._kodiwrapper.log_notice('Protocol: ' + protocol) # No if-else statement because this is the last resort stream selection stream_url = streamurls.StreamURLS(*self._select_hls_substreams(stream_dict[protocol])) @@ -267,9 +267,9 @@ def _select_hls_substreams(self, master_hls_url): hls_audio_id = None hls_subtitle_id = None hls_base_url = master_hls_url.split('.m3u8')[0] - self._kodi_wrapper.log_notice('URL get: ' + master_hls_url + '?hd', 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + master_hls_url + '?hd', 'Verbose') hls_playlist = urlopen(master_hls_url + '?hd').read() - max_bandwidth = self._kodi_wrapper.get_max_bandwidth() + max_bandwidth = self._kodiwrapper.get_max_bandwidth() stream_bandwidth = None # Get hls variant url based on max_bandwith setting @@ -287,9 +287,9 @@ def _select_hls_substreams(self, master_hls_url): break if not hls_variant_url: - message = self._kodi_wrapper.get_localized_string(30057) % (str(max_bandwidth), stream_bandwidth) - self._kodi_wrapper.show_ok_dialog('', message) - self._kodi_wrapper.open_settings() + message = self._kodiwrapper.get_localized_string(30057) % (str(max_bandwidth), stream_bandwidth) + self._kodiwrapper.show_ok_dialog('', message) + self._kodiwrapper.open_settings() # Get audio url if hls_audio_id: @@ -299,7 +299,7 @@ def _select_hls_substreams(self, master_hls_url): hls_variant_url = hls_base_url + match_audio.group(1) + '-' + hls_variant_url.split('-')[-1] # Get subtitle url, works only for on demand streams - if self._kodi_wrapper.get_setting('showsubtitles') == 'true' and '/live/' not in master_hls_url and hls_subtitle_id: + if self._kodiwrapper.get_setting('showsubtitles') == 'true' and '/live/' not in master_hls_url and hls_subtitle_id: subtitle_regex = re.compile(r'#EXT-X-MEDIA:TYPE=SUBTITLES[\w\-=,\.\"\/]+GROUP-ID=\"' + hls_subtitle_id + r'\"[\w\-=,\.\"\/]+URI=\"([\w\-=]+)\.m3u8\"') match_subtitle = re.search(subtitle_regex, hls_playlist) if match_subtitle: diff --git a/resources/lib/vrtplayer/tokenresolver.py b/resources/lib/vrtplayer/tokenresolver.py index 851c930ce..3aba22f15 100644 --- a/resources/lib/vrtplayer/tokenresolver.py +++ b/resources/lib/vrtplayer/tokenresolver.py @@ -5,11 +5,12 @@ from __future__ import absolute_import, division, unicode_literals from resources.lib.helperobjects import helperobjects + try: - from urllib.parse import urlencode + from urllib.parse import urlencode, unquote from urllib.request import build_opener, install_opener, ProxyHandler, HTTPErrorProcessor, urlopen, Request except ImportError: - from urllib2 import build_opener, install_opener, ProxyHandler, HTTPErrorProcessor, urlopen, Request + from urllib2 import build_opener, install_opener, ProxyHandler, HTTPErrorProcessor, unquote, urlopen, Request from urllib import urlencode # pylint: disable=ungrouped-imports @@ -31,13 +32,13 @@ class TokenResolver: _ROAMING_XVRTTOKEN_COOKIE = 'roaming_XVRTToken' _XVRT_TOKEN_COOKIE = 'XVRTToken' - def __init__(self, kodi_wrapper): - self._kodi_wrapper = kodi_wrapper - self._proxies = self._kodi_wrapper.get_proxies() + def __init__(self, _kodiwrapper): + self._kodiwrapper = _kodiwrapper + self._proxies = _kodiwrapper.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) def get_ondemand_playertoken(self, token_url, xvrttoken): - token_path = self._kodi_wrapper.get_userdata_path() + self._ONDEMAND_COOKIE + token_path = self._kodiwrapper.get_userdata_path() + self._ONDEMAND_COOKIE token = self._get_cached_token(token_path, 'vrtPlayerToken') if xvrttoken and token is None: @@ -47,7 +48,7 @@ def get_ondemand_playertoken(self, token_url, xvrttoken): return token def get_live_playertoken(self, token_url, xvrttoken): - token_path = self._kodi_wrapper.get_userdata_path() + self._LIVE_COOKIE + token_path = self._kodiwrapper.get_userdata_path() + self._LIVE_COOKIE token = self._get_cached_token(token_path, 'vrtPlayerToken') if token is None: if xvrttoken is not None: @@ -60,7 +61,7 @@ def get_live_playertoken(self, token_url, xvrttoken): def get_xvrttoken(self, get_roaming_token=False): token_filename = self._ROAMING_XVRTTOKEN_COOKIE if get_roaming_token else self._XVRT_TOKEN_COOKIE - token_path = self._kodi_wrapper.get_userdata_path() + token_filename + token_path = self._kodiwrapper.get_userdata_path() + token_filename token = self._get_cached_token(token_path, 'X-VRT-Token') if token is None: @@ -75,36 +76,38 @@ def get_cookie_from_cookiejar(cookiename, cookiejar): def _get_new_playertoken(self, path, token_url, headers): import json - self._kodi_wrapper.log_notice('URL post: ' + token_url, 'Verbose') + self._kodiwrapper.log_notice('URL post: ' + token_url, 'Verbose') req = Request(token_url, data='', headers=headers) playertoken = json.loads(urlopen(req).read()) - json.dump(playertoken, open(path, 'w')) + with self._kodiwrapper.open_file(path, 'w') as f: + json.dump(playertoken, f) return playertoken.get('vrtPlayerToken') def _get_cached_token(self, path, token_name): cached_token = None - if self._kodi_wrapper.check_if_path_exists(path): + if self._kodiwrapper.check_if_path_exists(path): from datetime import datetime import dateutil.parser import dateutil.tz import json - token = json.loads(open(path, 'r').read()) + with self._kodiwrapper.open_file(path) as f: + token = json.loads(f.read()) now = datetime.now(dateutil.tz.tzlocal()) exp = dateutil.parser.parse(token.get('expirationDate')) if exp > now: - self._kodi_wrapper.log_notice('Got cached token', 'Verbose') + self._kodiwrapper.log_notice('Got cached token', 'Verbose') cached_token = token.get(token_name) else: - self._kodi_wrapper.log_notice('Cached token deleted', 'Info') - self._kodi_wrapper.delete_path(path) + self._kodiwrapper.log_notice('Cached token deleted', 'Info') + self._kodiwrapper.delete_file(path) return cached_token def _get_new_xvrttoken(self, path, get_roaming_token): import json - cred = helperobjects.Credentials(self._kodi_wrapper) + cred = helperobjects.Credentials(self._kodiwrapper) if not cred.are_filled_in(): - self._kodi_wrapper.open_settings() + self._kodiwrapper.open_settings() cred.reload() data = dict( loginID=cred.username, @@ -113,7 +116,7 @@ def _get_new_xvrttoken(self, path, get_roaming_token): APIKey=self._API_KEY, targetEnv='jssdk', ) - self._kodi_wrapper.log_notice('URL post: ' + self._LOGIN_URL, 'Verbose') + self._kodiwrapper.log_notice('URL post: ' + unquote(self._LOGIN_URL), 'Verbose') req = Request(self._LOGIN_URL, data=urlencode(data)) logon_json = json.loads(urlopen(req).read()) token = None @@ -127,7 +130,7 @@ def _get_new_xvrttoken(self, path, get_roaming_token): email=cred.username, ) headers = {'Content-Type': 'application/json', 'Cookie': login_cookie} - self._kodi_wrapper.log_notice('URL post: ' + self._TOKEN_GATEWAY_URL, 'Verbose') + self._kodiwrapper.log_notice('URL post: ' + unquote(self._TOKEN_GATEWAY_URL), 'Verbose') req = Request(self._TOKEN_GATEWAY_URL, data=json.dumps(payload), headers=headers) cookie_data = urlopen(req).info().getheader('Set-Cookie').split('X-VRT-Token=')[1].split('; ') xvrttoken = TokenResolver._create_token_dictionary_from_urllib(cookie_data) @@ -135,24 +138,25 @@ def _get_new_xvrttoken(self, path, get_roaming_token): xvrttoken = self._get_roaming_xvrttoken(xvrttoken) if xvrttoken is not None: token = xvrttoken.get('X-VRT-Token') - json.dump(xvrttoken, open(path, 'w')) + with self._kodiwrapper.open_file(path, 'w') as f: + json.dump(xvrttoken, f) else: self._handle_error(logon_json, cred) return token def _handle_error(self, logon_json, cred): error_message = logon_json.get('errorDetails') - title = self._kodi_wrapper.get_localized_string(30051) + title = self._kodiwrapper.get_localized_string(30051) if error_message == 'invalid loginID or password': cred.reset() - message = self._kodi_wrapper.get_localized_string(30052) + message = self._kodiwrapper.get_localized_string(30052) elif error_message == 'loginID must be provided': - message = self._kodi_wrapper.get_localized_string(30055) + message = self._kodiwrapper.get_localized_string(30055) elif error_message == 'Missing required parameter: password': - message = self._kodi_wrapper.get_localized_string(30056) + message = self._kodiwrapper.get_localized_string(30056) else: message = error_message - self._kodi_wrapper.show_ok_dialog(title, message) + self._kodiwrapper.show_ok_dialog(title, message) def _get_roaming_xvrttoken(self, xvrttoken): roaming_xvrttoken = None @@ -160,7 +164,7 @@ def _get_roaming_xvrttoken(self, xvrttoken): cookie_value = 'X-VRT-Token=' + xvrttoken.get('X-VRT-Token') headers = {'Cookie': cookie_value} opener = build_opener(NoRedirection, ProxyHandler(self._proxies)) - self._kodi_wrapper.log_notice('URL post: ' + url, 'Verbose') + self._kodiwrapper.log_notice('URL post: ' + unquote(url), 'Verbose') req = Request(url, headers=headers) req_info = opener.open(req).info() cookie_value += '; state=' + req_info.getheader('Set-Cookie').split('state=')[1].split('; ')[0] @@ -168,7 +172,7 @@ def _get_roaming_xvrttoken(self, xvrttoken): url = opener.open(url).info().getheader('Location') headers = {'Cookie': cookie_value} if url is not None: - self._kodi_wrapper.log_notice('URL post: ' + url, 'Verbose') + self._kodiwrapper.log_notice('URL post: ' + unquote(url), 'Verbose') req = Request(url, headers=headers) cookie_data = opener.open(req).info().getheader('Set-Cookie').split('X-VRT-Token=')[1].split('; ') roaming_xvrttoken = TokenResolver._create_token_dictionary_from_urllib(cookie_data) @@ -198,12 +202,12 @@ def _create_token_dictionary_from_urllib(cookie_data): return token_dictionary def reset_cookies(self): - user_data_path = self._kodi_wrapper.get_userdata_path() + user_data_path = self._kodiwrapper.get_userdata_path() ondemand = user_data_path + self._ONDEMAND_COOKIE live = user_data_path + self._LIVE_COOKIE xvrt = user_data_path + self._XVRT_TOKEN_COOKIE roaming = user_data_path + self._ROAMING_XVRTTOKEN_COOKIE - self._kodi_wrapper.delete_path(ondemand) - self._kodi_wrapper.delete_path(live) - self._kodi_wrapper.delete_path(xvrt) - self._kodi_wrapper.delete_path(roaming) + self._kodiwrapper.delete_file(ondemand) + self._kodiwrapper.delete_file(live) + self._kodiwrapper.delete_file(xvrt) + self._kodiwrapper.delete_file(roaming) diff --git a/resources/lib/vrtplayer/tvguide.py b/resources/lib/vrtplayer/tvguide.py index b2cd697fd..eb170c158 100644 --- a/resources/lib/vrtplayer/tvguide.py +++ b/resources/lib/vrtplayer/tvguide.py @@ -29,11 +29,11 @@ class TVGuide: VRT_TVGUIDE = 'https://www.vrt.be/bin/epg/schedule.%Y-%m-%d.json' - def __init__(self, kodi_wrapper): - self._kodi_wrapper = kodi_wrapper - self._proxies = self._kodi_wrapper.get_proxies() + def __init__(self, _kodiwrapper): + self._kodiwrapper = _kodiwrapper + self._proxies = _kodiwrapper.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) - kodi_wrapper.set_locale() + _kodiwrapper.set_locale() def show_tvguide(self, params): date = params.get('date') @@ -41,39 +41,39 @@ def show_tvguide(self, params): if not date: date_items = self.show_date_menu() - self._kodi_wrapper.show_listing(date_items, content_type='files') + self._kodiwrapper.show_listing(date_items, content='files') elif not channel: channel_items = self.show_channel_menu(date) - self._kodi_wrapper.show_listing(channel_items) + self._kodiwrapper.show_listing(channel_items) else: episode_items = self.show_episodes(date, channel) - self._kodi_wrapper.show_listing(episode_items, content_type='episodes', cache=False) + self._kodiwrapper.show_listing(episode_items, content='episodes', cache=False) def show_date_menu(self): now = datetime.now(dateutil.tz.tzlocal()) date_items = [] for i in range(7, -31, -1): day = now + timedelta(days=i) - title = day.strftime(self._kodi_wrapper.get_localized_datelong()) + title = day.strftime(self._kodiwrapper.get_localized_datelong()) if str(i) in DATE_STRINGS: if i == 0: - title = '[COLOR yellow][B]%s[/B], %s[/COLOR]' % (self._kodi_wrapper.get_localized_string(DATE_STRINGS[str(i)]), title) + title = '[COLOR yellow][B]%s[/B], %s[/COLOR]' % (self._kodiwrapper.get_localized_string(DATE_STRINGS[str(i)]), title) else: - title = '[B]%s[/B], %s' % (self._kodi_wrapper.get_localized_string(DATE_STRINGS[str(i)]), title) + title = '[B]%s[/B], %s' % (self._kodiwrapper.get_localized_string(DATE_STRINGS[str(i)]), title) date_items.append(helperobjects.TitleItem( title=title, url_dict=dict(action=actions.LISTING_TVGUIDE, date=day.strftime('%Y-%m-%d')), is_playable=False, art_dict=dict(thumb='DefaultYear.png', icon='DefaultYear.png', fanart='DefaultYear.png'), - video_dict=dict(plot=day.strftime(self._kodi_wrapper.get_localized_datelong())), + video_dict=dict(plot=day.strftime(self._kodiwrapper.get_localized_datelong())), )) return date_items def show_channel_menu(self, date): dateobj = dateutil.parser.parse(date) - datelong = dateobj.strftime(self._kodi_wrapper.get_localized_datelong()) + datelong = dateobj.strftime(self._kodiwrapper.get_localized_datelong()) fanart_path = 'resource://resource.images.studios.white/%(studio)s.png' icon_path = 'resource://resource.images.studios.white/%(studio)s.png' @@ -87,7 +87,7 @@ def show_channel_menu(self, date): icon = icon_path % channel fanart = fanart_path % channel - plot = self._kodi_wrapper.get_localized_string(30301) % channel.get('label') + '\n' + datelong + plot = self._kodiwrapper.get_localized_string(30301) % channel.get('label') + '\n' + datelong channel_items.append(helperobjects.TitleItem( title=channel.get('label'), url_dict=dict(action=actions.LISTING_TVGUIDE, date=date, channel=channel.get('name')), @@ -100,9 +100,9 @@ def show_channel_menu(self, date): def show_episodes(self, date, channel): now = datetime.now(dateutil.tz.tzlocal()) dateobj = dateutil.parser.parse(date) - datelong = dateobj.strftime(self._kodi_wrapper.get_localized_datelong()) + datelong = dateobj.strftime(self._kodiwrapper.get_localized_datelong()) api_url = dateobj.strftime(self.VRT_TVGUIDE) - self._kodi_wrapper.log_notice('URL get: ' + api_url, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + api_url, 'Verbose') schedule = json.loads(urlopen(api_url).read()) name = channel try: @@ -137,14 +137,14 @@ def show_episodes(self, date, channel): video_url = statichelper.add_https_method(url) url_dict = dict(action=actions.PLAY, video_url=video_url) if start_date < now <= end_date: # Now playing - metadata.title = '[COLOR yellow]%s[/COLOR] %s' % (label, self._kodi_wrapper.get_localized_string(30302)) + metadata.title = '[COLOR yellow]%s[/COLOR] %s' % (label, self._kodiwrapper.get_localized_string(30302)) else: metadata.title = label else: # FIXME: Find a better solution for non-actionable items url_dict = dict(action=actions.LISTING_TVGUIDE, date=date, channel=channel) if start_date < now <= end_date: # Now playing - metadata.title = '[COLOR brown]%s[/COLOR] %s' % (label, self._kodi_wrapper.get_localized_string(30302)) + metadata.title = '[COLOR brown]%s[/COLOR] %s' % (label, self._kodiwrapper.get_localized_string(30302)) else: metadata.title = '[COLOR gray]%s[/COLOR]' % label episode_items.append(helperobjects.TitleItem( diff --git a/resources/lib/vrtplayer/vrtapihelper.py b/resources/lib/vrtplayer/vrtapihelper.py index 17a210684..d05db8692 100644 --- a/resources/lib/vrtplayer/vrtapihelper.py +++ b/resources/lib/vrtplayer/vrtapihelper.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals from resources.lib.helperobjects.helperobjects import TitleItem -from resources.lib.vrtplayer import actions, metadatacreator, statichelper +from resources.lib.vrtplayer import actions, favorites, metadatacreator, statichelper try: from urllib.parse import urlencode, unquote @@ -21,13 +21,14 @@ class VRTApiHelper: _VRTNU_SUGGEST_URL = 'https://vrtnu-api.vrt.be/suggest' _VRTNU_SCREENSHOT_URL = 'https://vrtnu-api.vrt.be/screenshots' - def __init__(self, kodi_wrapper): - self._kodi_wrapper = kodi_wrapper - self._proxies = self._kodi_wrapper.get_proxies() + def __init__(self, _kodiwrapper): + self._kodiwrapper = _kodiwrapper + self._proxies = _kodiwrapper.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) - self._showpermalink = kodi_wrapper.get_setting('showpermalink') == 'true' + self._showpermalink = _kodiwrapper.get_setting('showpermalink') == 'true' + self._favorites = favorites.Favorites(self._kodiwrapper) - def get_tvshow_items(self, category=None, channel=None): + def get_tvshow_items(self, category=None, channel=None, filtered=False): import json params = dict() @@ -41,13 +42,17 @@ def get_tvshow_items(self, category=None, channel=None): params['facets[programBrands]'] = channel api_url = self._VRTNU_SUGGEST_URL + '?' + urlencode(params) - self._kodi_wrapper.log_notice('URL get: ' + api_url, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + unquote(api_url), 'Verbose') api_json = json.loads(urlopen(api_url).read()) - return self._map_to_tvshow_items(api_json) + return self._map_to_tvshow_items(api_json, filtered=filtered) - def _map_to_tvshow_items(self, tvshows): + def _map_to_tvshow_items(self, tvshows, filtered=False): tvshow_items = [] + if filtered: + favorite_names = self._favorites.names() for tvshow in tvshows: + if filtered and tvshow.get('programName') not in favorite_names: + continue metadata = metadatacreator.MetadataCreator() metadata.mediatype = 'tvshow' metadata.tvshowtitle = tvshow.get('title', '???') @@ -58,6 +63,13 @@ def _map_to_tvshow_items(self, tvshows): # title = '%s [LIGHT][COLOR yellow]%s[/COLOR][/LIGHT]' % (tvshow.get('title', '???'), tvshow.get('episode_count', '?')) label = tvshow.get('title', '???') thumbnail = statichelper.add_https_method(tvshow.get('thumbnail', 'DefaultAddonVideo.png')) + program_path = statichelper.unique_path(tvshow.get('targetUrl')) + if self._favorites.is_favorite(program_path): + params = dict(action='unfollow', program=program_path) + context_menu = [(self._kodiwrapper.get_localized_string(30412), 'RunPlugin(plugin://plugin.video.vrt.nu?%s)' % urlencode(params))] + else: + params = dict(action='follow', program=program_path) + context_menu = [(self._kodiwrapper.get_localized_string(30411), 'RunPlugin(plugin://plugin.video.vrt.nu?%s)' % urlencode(params))] # Cut vrtbase url off since it will be added again when searching for episodes # (with a-z we dont have the full url) video_url = statichelper.add_https_method(tvshow.get('targetUrl')).replace(self._VRT_BASE, '') @@ -67,6 +79,7 @@ def _map_to_tvshow_items(self, tvshows): is_playable=False, art_dict=dict(thumb=thumbnail, icon='DefaultAddonVideo.png', fanart=thumbnail), video_dict=metadata.get_video_dict(), + context_menu=context_menu, )) return tvshow_items @@ -86,7 +99,7 @@ def _get_season_items(self, api_url, api_json): season_items, sort, ascending = self._map_to_season_items(api_url, facet.get('buckets', []), episode) return season_items, sort, ascending - def get_episode_items(self, path=None, page=None, all_seasons=False): + def get_episode_items(self, path=None, page=None, all_seasons=False, filtered=False): import json episode_items = [] sort = 'episode' @@ -101,10 +114,15 @@ def get_episode_items(self, path=None, page=None, all_seasons=False): 'facets[transcodingStatus]': 'AVAILABLE', 'facets[programBrands]': '[een,canvas,sporza,vrtnws,vrtnxt,radio1,radio2,klara,stubru,mnm]', } + + if filtered: + params['facets[program]'] = '[%s]' % (','.join(self._favorites.names())) + api_url = self._VRTNU_SEARCH_URL + '?' + urlencode(params) - self._kodi_wrapper.log_notice('URL get: ' + api_url, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + unquote(api_url), 'Verbose') api_json = json.loads(urlopen(api_url).read()) episode_items, sort, ascending = self._map_to_episode_items(api_json.get('results', []), titletype='recent') + content = 'episodes' if path: if '.relevant/' in path: @@ -116,7 +134,7 @@ def get_episode_items(self, path=None, page=None, all_seasons=False): api_url = self._VRTNU_SEARCH_URL + '?' + urlencode(params) else: api_url = path - self._kodi_wrapper.log_notice('URL get: ' + api_url, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + unquote(api_url), 'Verbose') api_json = json.loads(urlopen(api_url).read()) episodes = api_json.get('results', [{}]) @@ -131,20 +149,24 @@ def get_episode_items(self, path=None, page=None, all_seasons=False): # Look for seasons items if not yet done season_key = None + # path = requests.utils.unquote(path) path = unquote(path) if all_seasons is True: episode_items, sort, ascending = self._map_to_episode_items(episodes, season_key=None) + content = 'episodes' elif 'facets[seasonTitle]' in path: season_key = path.split('facets[seasonTitle]=')[1] elif display_options.get('showSeason') is True: episode_items, sort, ascending = self._get_season_items(api_url, api_json) + content = 'seasons' # No season items, generate episode items if not episode_items: episode_items, sort, ascending = self._map_to_episode_items(episodes, season_key=season_key) + content = 'episodes' - return episode_items, sort, ascending + return episode_items, sort, ascending, content def _map_to_episode_items(self, episodes, titletype=None, season_key=None): from datetime import datetime @@ -197,16 +219,16 @@ def _map_to_episode_items(self, episodes, titletype=None, season_key=None): plot_meta = '' if metadata.geolocked: # Show Geo-locked - plot_meta += self._kodi_wrapper.get_localized_string(30201) + plot_meta += self._kodiwrapper.get_localized_string(30201) # Only display when a video disappears if it is within the next 3 months if metadata.offtime is not None and (metadata.offtime - now).days < 93: # Show date when episode is removed - plot_meta += self._kodi_wrapper.get_localized_string(30202) % metadata.offtime.strftime(self._kodi_wrapper.get_localized_dateshort()) + plot_meta += self._kodiwrapper.get_localized_string(30202) % metadata.offtime.strftime(self._kodiwrapper.get_localized_dateshort()) # Show the remaining days/hours the episode is still available if (metadata.offtime - now).days > 0: - plot_meta += self._kodi_wrapper.get_localized_string(30203) % (metadata.offtime - now).days + plot_meta += self._kodiwrapper.get_localized_string(30203) % (metadata.offtime - now).days else: - plot_meta += self._kodi_wrapper.get_localized_string(30204) % int((metadata.offtime - now).seconds / 3600) + plot_meta += self._kodiwrapper.get_localized_string(30204) % int((metadata.offtime - now).seconds / 3600) if plot_meta: metadata.plot = '%s\n%s' % (plot_meta, metadata.plot) @@ -214,6 +236,14 @@ def _map_to_episode_items(self, episodes, titletype=None, season_key=None): if self._showpermalink and metadata.permalink: metadata.plot = '%s\n\n[COLOR yellow]%s[/COLOR]' % (metadata.plot, metadata.permalink) + program_path = statichelper.unique_path(episode.get('programUrl')) + if self._favorites.is_favorite(program_path): + params = dict(action='unfollow', program=program_path) + context_menu = [(self._kodiwrapper.get_localized_string(30412), 'RunPlugin(plugin://plugin.video.vrt.nu?%s)' % urlencode(params))] + else: + params = dict(action='follow', program=program_path) + context_menu = [(self._kodiwrapper.get_localized_string(30411), 'RunPlugin(plugin://plugin.video.vrt.nu?%s)' % urlencode(params))] + thumb = statichelper.add_https_method(episode.get('videoThumbnailUrl', 'DefaultAddonVideo.png')) fanart = statichelper.add_https_method(episode.get('programImageUrl', thumb)) video_url = statichelper.add_https_method(episode.get('url')) @@ -225,6 +255,7 @@ def _map_to_episode_items(self, episodes, titletype=None, season_key=None): is_playable=True, art_dict=dict(thumb=thumb, icon='DefaultAddonVideo.png', fanart=fanart), video_dict=metadata.get_video_dict(), + context_menu=context_menu, )) return episode_items, sort, ascending @@ -244,9 +275,9 @@ def _map_to_season_items(self, api_url, seasons, episode): ascending = False # Add an "* All seasons" list item - if self._kodi_wrapper.get_global_setting('videolibrary.showallitems') is True: + if self._kodiwrapper.get_global_setting('videolibrary.showallitems') is True: season_items.append(TitleItem( - title=self._kodi_wrapper.get_localized_string(30096), + title=self._kodiwrapper.get_localized_string(30096), url_dict=dict(action=actions.LISTING_ALL_EPISODES, video_url=api_url), is_playable=False, art_dict=dict(thumb=fanart, icon='DefaultSets.png', fanart=fanart), @@ -258,7 +289,7 @@ def _map_to_season_items(self, api_url, seasons, episode): for season in seasons: season_key = season.get('key') - label = '%s %s' % (self._kodi_wrapper.get_localized_string(30094), season_key) + label = '%s %s' % (self._kodiwrapper.get_localized_string(30094), season_key) params = {'facets[seasonTitle]': season_key} path = api_url + '&' + urlencode(params) season_items.append(TitleItem( @@ -280,7 +311,7 @@ def search(self, search_string, page=1): 'q': search_string, } api_url = 'https://search.vrt.be/search?' + urlencode(params) - self._kodi_wrapper.log_notice('URL get: ' + api_url, 'Verbose') + self._kodiwrapper.log_notice('URL get: ' + unquote(api_url), 'Verbose') api_json = json.loads(urlopen(api_url).read()) episodes = api_json.get('results', [{}]) @@ -296,7 +327,7 @@ def __delete_cached_thumbnail(self, url): crc = self.__get_crc32(url) ext = url.split('.')[-1] path = 'special://thumbnails/%s/%s.%s' % (crc[0], crc, ext) - self._kodi_wrapper.delete_path(path) + self._kodiwrapper.delete_file(path) @staticmethod def __get_crc32(string): @@ -349,7 +380,7 @@ def _make_label(self, result, titletype, options=None): # NOTE: Sort the episodes ourselves, because Kodi does not allow to set to 'descending' # sort = 'episode' sort = 'label' - label = '%s %s: %s' % (self._kodi_wrapper.get_localized_string(30095), result.get('episodeNumber'), label) + label = '%s %s: %s' % (self._kodiwrapper.get_localized_string(30095), result.get('episodeNumber'), label) elif options.get('showBroadcastDate') and result.get('formattedBroadcastShortDate'): sort = 'dateadded' label = '%s - %s' % (result.get('formattedBroadcastShortDate'), label) diff --git a/resources/lib/vrtplayer/vrtplayer.py b/resources/lib/vrtplayer/vrtplayer.py index a0768f3a2..498970027 100644 --- a/resources/lib/vrtplayer/vrtplayer.py +++ b/resources/lib/vrtplayer/vrtplayer.py @@ -14,64 +14,84 @@ class VRTPlayer: - def __init__(self, kodi_wrapper, api_helper): - self._kodi_wrapper = kodi_wrapper - self._proxies = self._kodi_wrapper.get_proxies() + def __init__(self, _kodiwrapper, _apihelper): + self._kodiwrapper = _kodiwrapper + self._proxies = _kodiwrapper.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) - self._api_helper = api_helper + self._apihelper = _apihelper def show_main_menu_items(self): main_items = [ - TitleItem(title=self._kodi_wrapper.get_localized_string(30080), + TitleItem(title=self._kodiwrapper.get_localized_string(30078), + url_dict=dict(action=actions.LISTING_FAVORITES), + is_playable=False, + art_dict=dict(thumb='icons/settings/profiles.png', icon='icons/settings/profiles.png', fanart='icons/settings/profiles.png'), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30079))), + TitleItem(title=self._kodiwrapper.get_localized_string(30080), url_dict=dict(action=actions.LISTING_AZ_TVSHOWS), is_playable=False, art_dict=dict(thumb='DefaultMovieTitle.png', icon='DefaultMovieTitle.png', fanart='DefaultMovieTitle.png'), - video_dict=dict(plot=self._kodi_wrapper.get_localized_string(30081))), - TitleItem(title=self._kodi_wrapper.get_localized_string(30082), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30081))), + TitleItem(title=self._kodiwrapper.get_localized_string(30082), url_dict=dict(action=actions.LISTING_CATEGORIES), is_playable=False, art_dict=dict(thumb='DefaultGenre.png', icon='DefaultGenre.png', fanart='DefaultGenre.png'), - video_dict=dict(plot=self._kodi_wrapper.get_localized_string(30083))), - TitleItem(title=self._kodi_wrapper.get_localized_string(30084), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30083))), + TitleItem(title=self._kodiwrapper.get_localized_string(30084), url_dict=dict(action=actions.LISTING_CHANNELS), is_playable=False, art_dict=dict(thumb='DefaultTags.png', icon='DefaultTags.png', fanart='DefaultTags.png'), - video_dict=dict(plot=self._kodi_wrapper.get_localized_string(30085))), - TitleItem(title=self._kodi_wrapper.get_localized_string(30086), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30085))), + TitleItem(title=self._kodiwrapper.get_localized_string(30086), url_dict=dict(action=actions.LISTING_LIVE), is_playable=False, art_dict=dict(thumb='DefaultAddonPVRClient.png', icon='DefaultAddonPVRClient.png', fanart='DefaultAddonPVRClient.png'), - video_dict=dict(plot=self._kodi_wrapper.get_localized_string(30087))), - TitleItem(title=self._kodi_wrapper.get_localized_string(30088), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30087))), + TitleItem(title=self._kodiwrapper.get_localized_string(30088), url_dict=dict(action=actions.LISTING_RECENT, page='1'), is_playable=False, art_dict=dict(thumb='DefaultYear.png', icon='DefaultYear.png', fanart='DefaultYear.png'), - video_dict=dict(plot=self._kodi_wrapper.get_localized_string(30089))), - TitleItem(title=self._kodi_wrapper.get_localized_string(30090), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30089))), + TitleItem(title=self._kodiwrapper.get_localized_string(30090), url_dict=dict(action=actions.LISTING_TVGUIDE), is_playable=False, art_dict=dict(thumb='DefaultAddonTvInfo.png', icon='DefaultAddonTvInfo.png', fanart='DefaultAddonTvInfo.png'), - video_dict=dict(plot=self._kodi_wrapper.get_localized_string(30091))), - TitleItem(title=self._kodi_wrapper.get_localized_string(30092), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30091))), + TitleItem(title=self._kodiwrapper.get_localized_string(30092), url_dict=dict(action=actions.SEARCH), is_playable=False, art_dict=dict(thumb='DefaultAddonsSearch.png', icon='DefaultAddonsSearch.png', fanart='DefaultAddonsSearch.png'), - video_dict=dict(plot=self._kodi_wrapper.get_localized_string(30093))), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30093))), + ] + self._kodiwrapper.show_listing(main_items) + + def show_favourites_menu_items(self): + favourites_items = [ + TitleItem(title=self._kodiwrapper.get_localized_string(30420), + url_dict=dict(action=actions.LISTING_AZ_TVSHOWS, filtered=True), + is_playable=False, + art_dict=dict(thumb='DefaultMovieTitle.png', icon='DefaultMovieTitle.png', fanart='DefaultMovieTitle.png'), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30421))), + TitleItem(title=self._kodiwrapper.get_localized_string(30422), + url_dict=dict(action=actions.LISTING_RECENT, page='1', filtered=True), + is_playable=False, + art_dict=dict(thumb='DefaultYear.png', icon='DefaultYear.png', fanart='DefaultYear.png'), + video_dict=dict(plot=self._kodiwrapper.get_localized_string(30423))), ] - self._kodi_wrapper.show_listing(main_items) + self._kodiwrapper.show_listing(favourites_items) - def show_tvshow_menu_items(self, category=None): - tvshow_items = self._api_helper.get_tvshow_items(category=category) - self._kodi_wrapper.show_listing(tvshow_items, sort='label', content_type='tvshows') + def show_tvshow_menu_items(self, category=None, filtered=False): + tvshow_items = self._apihelper.get_tvshow_items(category=category, filtered=filtered) + self._kodiwrapper.show_listing(tvshow_items, sort='label', content='tvshows') def show_category_menu_items(self): category_items = self.__get_category_menu_items() - self._kodi_wrapper.show_listing(category_items, sort='label', content_type='files') + self._kodiwrapper.show_listing(category_items, sort='label', content='files') def show_channels_menu_items(self, channel=None): if channel: - tvshow_items = self._api_helper.get_tvshow_items(channel=channel) - self._kodi_wrapper.show_listing(tvshow_items, sort='label', content_type='tvshows') + tvshow_items = self._apihelper.get_tvshow_items(channel=channel) + self._kodiwrapper.show_listing(tvshow_items, sort='label', content='tvshows') else: from resources.lib.vrtplayer import CHANNELS self.show_channels(action=actions.LISTING_CHANNELS, channels=[c.get('name') for c in CHANNELS]) @@ -102,14 +122,14 @@ def show_channels(self, action=actions.PLAY, channels=None): is_playable = False else: url_dict = dict(action=action) - label = self._kodi_wrapper.get_localized_string(30101) % channel.get('label') + label = self._kodiwrapper.get_localized_string(30101) % channel.get('label') is_playable = True if channel.get('name') in ['een', 'canvas', 'ketnet']: - fanart = self._api_helper.get_live_screenshot(channel.get('name')) - plot = '%s\n%s' % (self._kodi_wrapper.get_localized_string(30201), - self._kodi_wrapper.get_localized_string(30102) % channel.get('label')) + fanart = self._apihelper.get_live_screenshot(channel.get('name')) + plot = '%s\n%s' % (self._kodiwrapper.get_localized_string(30201), + self._kodiwrapper.get_localized_string(30102) % channel.get('label')) else: - plot = self._kodi_wrapper.get_localized_string(30102) % channel.get('label') + plot = self._kodiwrapper.get_localized_string(30102) % channel.get('label') if channel.get('live_stream_url'): url_dict['video_url'] = channel.get('live_stream_url') elif channel.get('live_stream'): @@ -130,42 +150,42 @@ def show_channels(self, action=actions.PLAY, channels=None): ), )) - self._kodi_wrapper.show_listing(channel_items, cache=False) + self._kodiwrapper.show_listing(channel_items, cache=False) def show_episodes(self, path): - episode_items, sort, ascending = self._api_helper.get_episode_items(path=path) - self._kodi_wrapper.show_listing(episode_items, sort=sort, ascending=ascending, content_type='episodes') + episode_items, sort, ascending, content = self._apihelper.get_episode_items(path=path) + self._kodiwrapper.show_listing(episode_items, sort=sort, ascending=ascending, content=content) def show_all_episodes(self, path): - episode_items, sort, ascending = self._api_helper.get_episode_items(path=path, all_seasons=True) - self._kodi_wrapper.show_listing(episode_items, sort=sort, ascending=ascending, content_type='episodes') + episode_items, sort, ascending, content = self._apihelper.get_episode_items(path=path, all_seasons=True) + self._kodiwrapper.show_listing(episode_items, sort=sort, ascending=ascending, content=content) - def show_recent(self, page): + def show_recent(self, page, filtered=False): try: page = int(page) except TypeError: page = 1 - episode_items, sort, ascending = self._api_helper.get_episode_items(page=page) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(page=page, filtered=filtered) # Add 'More...' entry at the end episode_items.append(TitleItem( - title=self._kodi_wrapper.get_localized_string(30300), + title=self._kodiwrapper.get_localized_string(30300), url_dict=dict(action=actions.LISTING_RECENT, page=page + 1), is_playable=False, art_dict=dict(thumb='DefaultYear.png', icon='DefaultYear.png', fanart='DefaultYear.png'), video_dict=dict(), )) - self._kodi_wrapper.show_listing(episode_items, sort=sort, ascending=ascending, content_type='episodes', cache=False) + self._kodiwrapper.show_listing(episode_items, sort=sort, ascending=ascending, content=content, cache=False) def play(self, params): from resources.lib.vrtplayer import streamservice, tokenresolver - token_resolver = tokenresolver.TokenResolver(self._kodi_wrapper) - stream_service = streamservice.StreamService(self._kodi_wrapper, token_resolver) + token_resolver = tokenresolver.TokenResolver(self._kodiwrapper) + stream_service = streamservice.StreamService(self._kodiwrapper, token_resolver) stream = stream_service.get_stream(params) if stream is not None: - self._kodi_wrapper.play(stream) + self._kodiwrapper.play(stream) def search(self, search_string=None, page=1): try: @@ -174,27 +194,27 @@ def search(self, search_string=None, page=1): page = 1 if search_string is None: - search_string = self._kodi_wrapper.get_search_string() + search_string = self._kodiwrapper.get_search_string() if not search_string: return - search_items, sort, ascending = self._api_helper.search(search_string, page=page) + search_items, sort, ascending = self._apihelper.search(search_string, page=page) if not search_items: - self._kodi_wrapper.show_ok_dialog(self._kodi_wrapper.get_localized_string(30098), self._kodi_wrapper.get_localized_string(30099) % search_string) + self._kodiwrapper.show_ok_dialog(self._kodiwrapper.get_localized_string(30098), self._kodiwrapper.get_localized_string(30099) % search_string) return # Add 'More...' entry at the end if len(search_items) == 50: search_items.append(TitleItem( - title=self._kodi_wrapper.get_localized_string(30300), + title=self._kodiwrapper.get_localized_string(30300), url_dict=dict(action=actions.SEARCH, query=search_string, page=page + 1), is_playable=False, art_dict=dict(thumb='DefaultAddonSearch.png', icon='DefaultAddonSearch.png', fanart='DefaultAddonSearch.png'), video_dict=dict(), )) - self._kodi_wrapper.show_listing(search_items, sort=sort, ascending=ascending, content_type='episodes', cache=False) + self._kodiwrapper.show_listing(search_items, sort=sort, ascending=ascending, content='episodes', cache=False) def __get_category_menu_items(self): try: @@ -221,7 +241,7 @@ def __get_category_menu_items(self): def get_categories(self, proxies=None): from bs4 import BeautifulSoup, SoupStrainer - self._kodi_wrapper.log_notice('URL get: https://www.vrt.be/vrtnu/categorieen/', 'Verbose') + self._kodiwrapper.log_notice('URL get: https://www.vrt.be/vrtnu/categorieen/', 'Verbose') response = urlopen('https://www.vrt.be/vrtnu/categorieen/') tiles = SoupStrainer('a', {'class': 'nui-tile'}) soup = BeautifulSoup(response.read(), 'html.parser', parse_only=tiles) diff --git a/test/apihelpertests.py b/test/apihelpertests.py index 9b9fcc1ab..80f6f0a09 100644 --- a/test/apihelpertests.py +++ b/test/apihelpertests.py @@ -2,37 +2,67 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# pylint: disable=unused-variable - -from __future__ import absolute_import, division, unicode_literals +from __future__ import absolute_import, division, print_function, unicode_literals +from contextlib import contextmanager import mock -from resources.lib.vrtplayer import vrtapihelper +import os import unittest +from resources.lib.vrtplayer import vrtapihelper + + +@contextmanager +def open_file(path, flags='r'): + f = open(path, flags) + yield f + f.close() + + +def log_notice(msg, level): + print('%s: %s' % (level, msg)) + class ApiHelperTests(unittest.TestCase): - _kodi_wrapper = mock.MagicMock() - _kodi_wrapper.get_proxies = mock.MagicMock(return_value=dict()) - _kodi_wrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _kodi_wrapper.get_localized_string.return_value = '"[B][COLOR blue]Available until %s[/COLOR][/B]\n"' - _api_helper = vrtapihelper.VRTApiHelper(_kodi_wrapper) + _kodiwrapper = mock.MagicMock() + _kodiwrapper.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) + _kodiwrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') + _kodiwrapper.get_localized_string.return_value = '"[B][COLOR blue]Available until %s[/COLOR][/B]\n"' + _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _kodiwrapper.get_userdata_path.return_value = './userdata/' + _kodiwrapper.log_notice = mock.MagicMock(side_effect=log_notice) + _kodiwrapper.make_dir.return_value = None + _kodiwrapper.open_file = mock.MagicMock(side_effect=open_file) + # _kodiwrapper.stat_file = mock.MagicMock(side_effect=os.stat) + _apihelper = vrtapihelper.VRTApiHelper(_kodiwrapper) def test_get_api_data_single_season(self): - title_items, sort, ascending = self._api_helper.get_episode_items(path='/vrtnu/a-z/het-journaal.relevant/') - self.assertTrue(122 < len(title_items) < 126) + title_items, sort, ascending, content = self._apihelper.get_episode_items(path='/vrtnu/a-z/het-journaal.relevant/') + self.assertTrue(123 < len(title_items) < 129, 'We got %s items instead.' % len(title_items)) + self.assertEqual(sort, 'dateadded') + self.assertFalse(ascending) + self.assertEqual(content, 'episodes') def test_get_api_data_multiple_seasons(self): - title_items, sort, ascending = self._api_helper.get_episode_items(path='/vrtnu/a-z/thuis.relevant/') + title_items, sort, ascending, content = self._apihelper.get_episode_items(path='/vrtnu/a-z/thuis.relevant/') self.assertTrue(len(title_items) < 5) + self.assertEqual(sort, 'label') + self.assertFalse(ascending) + self.assertEqual(content, 'seasons') def test_get_api_data_specific_season(self): - title_items, sort, ascending = self._api_helper.get_episode_items(path='/vrtnu/a-z/pano.relevant/') + title_items, sort, ascending, content = self._apihelper.get_episode_items(path='/vrtnu/a-z/pano.relevant/') self.assertEqual(len(title_items), 4) + self.assertEqual(sort, 'label') + self.assertFalse(ascending) + self.assertEqual(content, 'seasons') def test_get_api_data_specific_season_without_broadcastdate(self): - title_items, sort, ascending = self._api_helper.get_episode_items(path='/vrtnu/a-z/postbus-x.relevant/') + title_items, sort, ascending, content = self._apihelper.get_episode_items(path='/vrtnu/a-z/postbus-x.relevant/') self.assertEqual(len(title_items), 3) + self.assertEqual(sort, 'label') + self.assertTrue(ascending) + self.assertEqual(content, 'seasons') if __name__ == '__main__': diff --git a/test/favoritestests.py b/test/favoritestests.py new file mode 100644 index 000000000..489368b4e --- /dev/null +++ b/test/favoritestests.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function, unicode_literals +from contextlib import contextmanager +import mock +import os +import unittest + +from resources.lib.vrtplayer import favorites + + +@contextmanager +def open_file(path, flags='r'): + f = open(path, flags) + yield f + f.close() + + +def log_notice(msg, level): + print('%s: %s' % (level, msg)) + + +class TestFavorites(unittest.TestCase): + + _kodiwrapper = mock.MagicMock() + _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _kodiwrapper.log_notice = mock.MagicMock(side_effect=log_notice) + _kodiwrapper.get_userdata_path.return_value = './userdata/' + _kodiwrapper.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) + _kodiwrapper.open_file = mock.MagicMock(side_effect=open_file) + # _kodiwrapper.stat_file = mock.MagicMock(side_effect=os.stat) + _kodiwrapper.make_dir.return_value = None + _favorites = favorites.Favorites(_kodiwrapper) + + def test_follow_unfollow(self): + program = '/vrtnu/a-z/winteruur/' + self._favorites.follow(program) + self.assertTrue(self._favorites.is_favorite(program)) + + self._favorites.unfollow(program) + self.assertFalse(self._favorites.is_favorite(program)) + + self._favorites.follow(program) + self.assertTrue(self._favorites.is_favorite(program)) + + def test_names(self): + names = self._favorites.names() + self.assertTrue(names) + print(names) + + def test_titles(self): + titles = self._favorites.titles() + self.assertTrue(titles) + print(sorted(titles)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/searchtests.py b/test/searchtests.py index ec8967257..ddf0a35e5 100644 --- a/test/searchtests.py +++ b/test/searchtests.py @@ -2,23 +2,43 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import absolute_import, division, unicode_literals +from __future__ import absolute_import, division, print_function, unicode_literals +from contextlib import contextmanager import mock +import os import unittest from resources.lib.vrtplayer import vrtapihelper +@contextmanager +def open_file(path, flags='r'): + f = open(path, flags) + yield f + f.close() + + +def log_notice(msg, level): + print('%s: %s' % (level, msg)) + + class TestVRTPlayer(unittest.TestCase): - _kodi_wrapper = mock.MagicMock() - _kodi_wrapper.get_proxies = mock.MagicMock(return_value=dict()) - _kodi_wrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _api_helper = vrtapihelper.VRTApiHelper(_kodi_wrapper) + _kodiwrapper = mock.MagicMock() + _kodiwrapper.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) + _kodiwrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') + _kodiwrapper.get_localized_string.return_value = '"[B][COLOR blue]Available until %s[/COLOR][/B]\n"' + _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _kodiwrapper.get_userdata_path.return_value = './userdata/' + _kodiwrapper.log_notice = mock.MagicMock(side_effect=log_notice) + _kodiwrapper.make_dir.return_value = None + _kodiwrapper.open_file = mock.MagicMock(side_effect=open_file) + # _kodiwrapper.stat_file = mock.MagicMock(side_effect=os.stat) + _apihelper = vrtapihelper.VRTApiHelper(_kodiwrapper) def test_search_journaal(self): ''' Test for journaal ''' - search_items, sort, ascending = self._api_helper.search('journaal', page=1) + search_items, sort, ascending = self._apihelper.search('journaal', page=1) # Test we get a non-empty search result self.assertEqual(len(search_items), 50) @@ -27,7 +47,7 @@ def test_search_journaal(self): def test_search_journaal_page2(self): ''' Test for journaal ''' - search_items, sort, ascending = self._api_helper.search('journaal', page=2) + search_items, sort, ascending = self._apihelper.search('journaal', page=2) # Test we get a non-empty search result self.assertEqual(len(search_items), 50) @@ -36,7 +56,7 @@ def test_search_journaal_page2(self): def test_search_weer(self): ''' Test for journaal ''' - search_items, sort, ascending = self._api_helper.search('weer', page=1) + search_items, sort, ascending = self._apihelper.search('weer', page=1) # Test we get a non-empty search result self.assertEqual(len(search_items), 50) diff --git a/test/streamservicetests.py b/test/streamservicetests.py index 095969751..b675e4d0b 100644 --- a/test/streamservicetests.py +++ b/test/streamservicetests.py @@ -43,19 +43,18 @@ def get_localized_string(msgctxt): class StreamServiceTests(unittest.TestCase): - _kodi_wrapper = mock.MagicMock() - _kodi_wrapper.get_proxies = mock.MagicMock(return_value=dict()) - _kodi_wrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _kodi_wrapper.get_localized_string.return_value = mock.MagicMock(side_effect=get_localized_string) - _kodi_wrapper.get_setting = mock.MagicMock(side_effect=get_settings) - _kodi_wrapper.get_userdata_path.return_value = 'vrttest' - _kodi_wrapper.check_if_path_exists.return_value = False - _kodi_wrapper.make_dir.return_value = None - _kodi_wrapper.open_path.return_value = False - _kodi_wrapper.check_inputstream_adaptive.return_value = True - _api_helper = vrtapihelper.VRTApiHelper(_kodi_wrapper) - _token_resolver = tokenresolver.TokenResolver(_kodi_wrapper) - _stream_service = streamservice.StreamService(_kodi_wrapper, _token_resolver) + _kodiwrapper = mock.MagicMock() + _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _kodiwrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') + _kodiwrapper.get_localized_string.return_value = mock.MagicMock(side_effect=get_localized_string) + _kodiwrapper.get_setting = mock.MagicMock(side_effect=get_settings) + _kodiwrapper.get_userdata_path.return_value = 'vrttest' + _kodiwrapper.check_if_path_exists.return_value = False + _kodiwrapper.make_dir.return_value = None + _kodiwrapper.check_inputstream_adaptive.return_value = True + _apihelper = vrtapihelper.VRTApiHelper(_kodiwrapper) + _tokenresolver = tokenresolver.TokenResolver(_kodiwrapper) + _streamservice = streamservice.StreamService(_kodiwrapper, _tokenresolver) def test_get_ondemand_stream_from_invalid_url(self): video = dict( @@ -64,7 +63,7 @@ def test_get_ondemand_stream_from_invalid_url(self): publication_id=None, ) try: - stream = self._stream_service.get_stream(video) + stream = self._streamservice.get_stream(video) self.assertEqual(stream.stream_url, video['video_url']) except HTTPError: pass @@ -76,7 +75,7 @@ def test_get_ondemand_stream_from_url_gets_stream_does_not_crash(self): video_id=None, publication_id=None, ) - stream = self._stream_service.get_stream(video) + stream = self._streamservice.get_stream(video) self.assertTrue(stream is not None) def test_get_live_stream_from_url_does_not_crash_returns_stream_and_licensekey(self): @@ -86,7 +85,7 @@ def test_get_live_stream_from_url_does_not_crash_returns_stream_and_licensekey(s video_id=None, publication_id=None, ) - stream = self._stream_service.get_stream(video) + stream = self._streamservice.get_stream(video) self.assertTrue(stream is not None) self.assertTrue(stream.license_key is not None) @@ -96,7 +95,7 @@ def test_get_live_stream_from_url_does_not_crash(self): video_id=None, publication_id=None, ) - stream = self._stream_service.get_stream(video) + stream = self._streamservice.get_stream(video) self.assertTrue(stream is not None) def test_get_live_stream_from_id_does_not_crash(self): @@ -105,7 +104,7 @@ def test_get_live_stream_from_id_does_not_crash(self): video_id=CHANNELS[1]['live_stream_id'], publication_id=None, ) - stream = self._stream_service.get_stream(video) + stream = self._streamservice.get_stream(video) self.assertTrue(stream is not None) diff --git a/test/tvguidetests.py b/test/tvguidetests.py index a3c302c2f..b23a6747a 100644 --- a/test/tvguidetests.py +++ b/test/tvguidetests.py @@ -16,10 +16,10 @@ class TestTVGuide(unittest.TestCase): - _kodi_wrapper = mock.MagicMock() - _kodi_wrapper.get_proxies = mock.MagicMock(return_value=dict()) - _kodi_wrapper.get_localized_datelong = mock.MagicMock(return_value='%a %d-%m-%Y') - _tv_guide = tvguide.TVGuide(_kodi_wrapper) + _kodiwrapper = mock.MagicMock() + _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _kodiwrapper.get_localized_datelong = mock.MagicMock(return_value='%a %d-%m-%Y') + _tv_guide = tvguide.TVGuide(_kodiwrapper) def test_tvguide_date_menu(self): ''' Test TV guide main menu ''' diff --git a/test/vrtplayertests.py b/test/vrtplayertests.py index 5231b7fca..5e2b8d2d6 100644 --- a/test/vrtplayertests.py +++ b/test/vrtplayertests.py @@ -4,24 +4,42 @@ # pylint: disable=unused-variable -from __future__ import absolute_import, division, unicode_literals +from __future__ import absolute_import, division, print_function, unicode_literals +from contextlib import contextmanager import mock +import os import random import unittest from resources.lib.vrtplayer import CATEGORIES, CHANNELS, vrtapihelper, vrtplayer +@contextmanager +def open_file(path, flags='r'): + f = open(path, flags) + yield f + f.close() + + +def log_notice(msg, level): + print('%s: %s' % (level, msg)) + + class TestVRTPlayer(unittest.TestCase): - _kodi_wrapper = mock.MagicMock() - _kodi_wrapper.get_proxies = mock.MagicMock(return_value=dict()) - _kodi_wrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _api_helper = vrtapihelper.VRTApiHelper(_kodi_wrapper) + _kodiwrapper = mock.MagicMock() + _kodiwrapper.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) + _kodiwrapper.get_localized_dateshort = mock.MagicMock(return_value='%d-%m-%Y') + _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _kodiwrapper.get_userdata_path.return_value = './userdata/' + _kodiwrapper.log_notice = mock.MagicMock(side_effect=log_notice) + _kodiwrapper.open_file = mock.MagicMock(side_effect=open_file) + # _kodiwrapper.stat_file = mock.MagicMock(side_effect=os.stat) + _apihelper = vrtapihelper.VRTApiHelper(_kodiwrapper) def test_tvshows(self): ''' Test A-Z tvshow listing and CHANNELS list ''' - tvshow_items = self._api_helper.get_tvshow_items(category=None) + tvshow_items = self._apihelper.get_tvshow_items(category=None) # Test we get a non-empty A-Z listing back self.assertTrue(tvshow_items) @@ -34,89 +52,96 @@ def test_tvshows(self): def test_show_videos_single_episode_shows_videos(self): path = '/vrtnu/a-z/marathonradio.relevant/' - episode_items, sort, ascending = self._api_helper.get_episode_items(path=path) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(path=path) self.assertTrue(episode_items, msg=path) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) + self.assertEqual(content, 'episodes') - player = vrtplayer.VRTPlayer(self._kodi_wrapper, self._api_helper) + player = vrtplayer.VRTPlayer(self._kodiwrapper, self._apihelper) player.show_episodes(path) - self.assertTrue(self._kodi_wrapper.show_listing.called) + self.assertTrue(self._kodiwrapper.show_listing.called) def test_show_videos_single_season_shows_videos(self): path = '/vrtnu/a-z/het-weer.relevant/' - episode_items, sort, ascending = self._api_helper.get_episode_items(path=path) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(path=path) self.assertTrue(episode_items, msg=path) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) + self.assertEqual(content, 'episodes') - player = vrtplayer.VRTPlayer(self._kodi_wrapper, self._api_helper) + player = vrtplayer.VRTPlayer(self._kodiwrapper, self._apihelper) player.show_episodes(path) - self.assertTrue(self._kodi_wrapper.show_listing.called) + self.assertTrue(self._kodiwrapper.show_listing.called) def test_show_videos_multiple_seasons_shows_videos(self): path = '/vrtnu/a-z/pano.relevant/' - episode_items, sort, ascending = self._api_helper.get_episode_items(path=path) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(path=path) self.assertTrue(episode_items) self.assertEqual(sort, 'label') self.assertFalse(ascending) + self.assertEqual(content, 'seasons') - player = vrtplayer.VRTPlayer(self._kodi_wrapper, self._api_helper) + player = vrtplayer.VRTPlayer(self._kodiwrapper, self._apihelper) player.show_episodes(path) - self.assertTrue(self._kodi_wrapper.show_listing.called) + self.assertTrue(self._kodiwrapper.show_listing.called) def test_show_videos_specific_seasons_shows_videos(self): path = '/vrtnu/a-z/thuis.relevant/' - episode_items, sort, ascending = self._api_helper.get_episode_items(path=path) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(path=path) self.assertTrue(episode_items, msg=path) self.assertEqual(sort, 'label') self.assertFalse(ascending) + self.assertEqual(content, 'seasons') - player = vrtplayer.VRTPlayer(self._kodi_wrapper, self._api_helper) + player = vrtplayer.VRTPlayer(self._kodiwrapper, self._apihelper) player.show_episodes(path) - self.assertTrue(self._kodi_wrapper.show_listing.called) + self.assertTrue(self._kodiwrapper.show_listing.called) def test_get_recent_episodes(self): ''' Test items, sort and order ''' - episode_items, sort, ascending = self._api_helper.get_episode_items(page=1) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(page=1) self.assertEqual(len(episode_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) + self.assertEqual(content, 'episodes') def test_get_program_episodes(self): ''' Test items, sort and order ''' path = '/vrtnu/a-z/het-journaal.relevant/' - episode_items, sort, ascending = self._api_helper.get_episode_items(path=path) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(path=path) self.assertTrue(episode_items) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) + self.assertEqual(content, 'episodes') def test_get_tvshows(self): ''' Test items, sort and order ''' path = 'nieuws-en-actua' - tvshow_items = self._api_helper.get_tvshow_items(path) + tvshow_items = self._apihelper.get_tvshow_items(path) self.assertTrue(tvshow_items) def test_categories_scraping(self): ''' Test to ensure our hardcoded categories conforms to scraped categories ''' # Remove thumbnails from scraped categories first - vrt_player = vrtplayer.VRTPlayer(self._kodi_wrapper, self._api_helper) + vrt_player = vrtplayer.VRTPlayer(self._kodiwrapper, self._apihelper) categories = [dict(id=c['id'], name=c['name']) for c in vrt_player.get_categories()] self.assertEqual(categories, CATEGORIES) def test_random_tvshow_episodes(self): ''' Rest episode from a random tvshow in a random category ''' - vrt_player = vrtplayer.VRTPlayer(self._kodi_wrapper, self._api_helper) + vrt_player = vrtplayer.VRTPlayer(self._kodiwrapper, self._apihelper) categories = vrt_player.get_categories() self.assertTrue(categories) category = random.choice(categories) - tvshow_items = self._api_helper.get_tvshow_items(category['id']) + tvshow_items = self._apihelper.get_tvshow_items(category['id']) self.assertTrue(tvshow_items, msg=category['id']) tvshow = random.choice(tvshow_items) - episode_items, sort, ascending = self._api_helper.get_episode_items(tvshow.url_dict['video_url']) + episode_items, sort, ascending, content = self._apihelper.get_episode_items(tvshow.url_dict['video_url']) self.assertTrue(episode_items, msg=tvshow.url_dict['video_url']) + self.assertTrue(content in ['episodes', 'seasons'], "Content for '%s' is '%s'" % (tvshow.title, content)) if __name__ == '__main__':