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 0e1aa8fd2..bfa020a74 100644 --- a/addon.py +++ b/addon.py @@ -38,6 +38,19 @@ def router(params_string): from resources.lib.vrtplayer import tvguide _tvguide = tvguide.TVGuide(_kodiwrapper) _tvguide.show_tvguide(params) +<<<<<<< HEAD +======= + 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')) +>>>>>>> Add "My programs" menu return from resources.lib.vrtplayer import vrtapihelper, vrtplayer @@ -47,13 +60,22 @@ def router(params_string): if action == actions.PLAY: _vrtplayer.play(params) elif action == actions.LISTING_AZ_TVSHOWS: +<<<<<<< HEAD _vrtplayer.show_tvshow_menu_items() +======= + _vrtplayer.show_tvshow_menu_items(filtered=params.get('filtered')) +>>>>>>> Add "My programs" menu elif action == actions.LISTING_CATEGORIES: _vrtplayer.show_category_menu_items() elif action == actions.LISTING_CATEGORY_TVSHOWS: _vrtplayer.show_tvshow_menu_items(category=params.get('category')) elif action == actions.LISTING_CHANNELS: _vrtplayer.show_channels_menu_items(channel=params.get('channel')) +<<<<<<< HEAD +======= + elif action == actions.LISTING_FAVORITES: + _vrtplayer.show_favourites_menu_items() +>>>>>>> Add "My programs" menu elif action == actions.LISTING_LIVE: _vrtplayer.show_livestream_items() elif action == actions.LISTING_EPISODES: @@ -61,7 +83,11 @@ def router(params_string): elif action == actions.LISTING_ALL_EPISODES: _vrtplayer.show_all_episodes(path=params.get('video_url')) elif action == actions.LISTING_RECENT: +<<<<<<< HEAD _vrtplayer.show_recent(page=params.get('page', 1)) +======= + _vrtplayer.show_recent(page=params.get('page', 1), filtered=params.get('filtered')) +>>>>>>> Add "My programs" menu elif action == actions.SEARCH: _vrtplayer.search(search_string=params.get('query'), page=params.get('page', 1)) else: 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 0b00ac577..10ce7abdc 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 a1159317c..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: @@ -210,9 +210,9 @@ def get_stream(self, video, retry=False, api_data=None): if not retry and roaming_xvrttoken is not None: # Delete cached playertokens if api_data.is_live_stream: - self._kodiwrapper.delete_path(self._kodiwrapper.get_userdata_path() + 'live_vrtPlayerToken') + self._kodiwrapper.delete_file(self._kodiwrapper.get_userdata_path() + 'live_vrtPlayerToken') else: - self._kodiwrapper.delete_path(self._kodiwrapper.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) diff --git a/resources/lib/vrtplayer/tokenresolver.py b/resources/lib/vrtplayer/tokenresolver.py index e86fa07d7..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 @@ -78,7 +79,8 @@ def _get_new_playertoken(self, path, token_url, headers): 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): @@ -89,7 +91,8 @@ def _get_cached_token(self, path, token_name): 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: @@ -97,7 +100,7 @@ def _get_cached_token(self, path, token_name): cached_token = token.get(token_name) else: self._kodiwrapper.log_notice('Cached token deleted', 'Info') - self._kodiwrapper.delete_path(path) + self._kodiwrapper.delete_file(path) return cached_token def _get_new_xvrttoken(self, path, get_roaming_token): @@ -113,7 +116,7 @@ def _get_new_xvrttoken(self, path, get_roaming_token): APIKey=self._API_KEY, targetEnv='jssdk', ) - self._kodiwrapper.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._kodiwrapper.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,7 +138,8 @@ 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 @@ -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._kodiwrapper.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._kodiwrapper.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) @@ -203,7 +207,7 @@ def reset_cookies(self): live = user_data_path + self._LIVE_COOKIE xvrt = user_data_path + self._XVRT_TOKEN_COOKIE roaming = user_data_path + self._ROAMING_XVRTTOKEN_COOKIE - self._kodiwrapper.delete_path(ondemand) - self._kodiwrapper.delete_path(live) - self._kodiwrapper.delete_path(xvrt) - self._kodiwrapper.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 89f6aa636..eb170c158 100644 --- a/resources/lib/vrtplayer/tvguide.py +++ b/resources/lib/vrtplayer/tvguide.py @@ -41,7 +41,7 @@ def show_tvguide(self, params): if not date: date_items = self.show_date_menu() - self._kodiwrapper.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) @@ -49,7 +49,7 @@ def show_tvguide(self, params): else: episode_items = self.show_episodes(date, channel) - self._kodiwrapper.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()) diff --git a/resources/lib/vrtplayer/vrtapihelper.py b/resources/lib/vrtplayer/vrtapihelper.py index b454ee6cf..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 @@ -26,8 +26,9 @@ def __init__(self, _kodiwrapper): self._proxies = _kodiwrapper.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) 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._kodiwrapper.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._kodiwrapper.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._kodiwrapper.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 @@ -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 @@ -280,7 +311,7 @@ def search(self, search_string, page=1): 'q': search_string, } api_url = 'https://search.vrt.be/search?' + urlencode(params) - self._kodiwrapper.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._kodiwrapper.delete_path(path) + self._kodiwrapper.delete_file(path) @staticmethod def __get_crc32(string): diff --git a/resources/lib/vrtplayer/vrtplayer.py b/resources/lib/vrtplayer/vrtplayer.py index a123b775b..4071af75e 100644 --- a/resources/lib/vrtplayer/vrtplayer.py +++ b/resources/lib/vrtplayer/vrtplayer.py @@ -22,6 +22,11 @@ def __init__(self, _kodiwrapper, _apihelper): def show_main_menu_items(self): main_items = [ + 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, @@ -60,18 +65,33 @@ def show_main_menu_items(self): ] self._kodiwrapper.show_listing(main_items) - def show_tvshow_menu_items(self, category=None): - tvshow_items = self._apihelper.get_tvshow_items(category=category) - self._kodiwrapper.show_listing(tvshow_items, sort='label', content_type='tvshows') + 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._kodiwrapper.show_listing(favourites_items) + + 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._kodiwrapper.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._apihelper.get_tvshow_items(channel=channel) - self._kodiwrapper.show_listing(tvshow_items, sort='label', content_type='tvshows') + 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]) @@ -133,20 +153,20 @@ def show_channels(self, action=actions.PLAY, channels=None): self._kodiwrapper.show_listing(channel_items, cache=False) def show_episodes(self, path): - episode_items, sort, ascending = self._apihelper.get_episode_items(path=path) - self._kodiwrapper.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._apihelper.get_episode_items(path=path, all_seasons=True) - self._kodiwrapper.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._apihelper.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( @@ -157,7 +177,7 @@ def show_recent(self, page): video_dict=dict(), )) - self._kodiwrapper.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 @@ -194,7 +214,7 @@ def search(self, search_string=None, page=1): video_dict=dict(), )) - self._kodiwrapper.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: diff --git a/test/apihelpertests.py b/test/apihelpertests.py index bfe90d488..6e7138486 100644 --- a/test/apihelpertests.py +++ b/test/apihelpertests.py @@ -2,38 +2,68 @@ # 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 +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): _kodiwrapper = mock.MagicMock() - _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + + _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._apihelper.get_episode_items(path='/vrtnu/a-z/het-journaal.relevant/') - self.assertTrue(123 < len(title_items) < 129) + 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._apihelper.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._apihelper.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._apihelper.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 6ae8594cf..ddf0a35e5 100644 --- a/test/searchtests.py +++ b/test/searchtests.py @@ -2,18 +2,38 @@ # 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): _kodiwrapper = mock.MagicMock() - _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _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): diff --git a/test/streamservicetests.py b/test/streamservicetests.py index 44ee9d7ff..b675e4d0b 100644 --- a/test/streamservicetests.py +++ b/test/streamservicetests.py @@ -51,7 +51,6 @@ class StreamServiceTests(unittest.TestCase): _kodiwrapper.get_userdata_path.return_value = 'vrttest' _kodiwrapper.check_if_path_exists.return_value = False _kodiwrapper.make_dir.return_value = None - _kodiwrapper.open_path.return_value = False _kodiwrapper.check_inputstream_adaptive.return_value = True _apihelper = vrtapihelper.VRTApiHelper(_kodiwrapper) _tokenresolver = tokenresolver.TokenResolver(_kodiwrapper) diff --git a/test/vrtplayertests.py b/test/vrtplayertests.py index 3c12a5d87..cc16a37f4 100644 --- a/test/vrtplayertests.py +++ b/test/vrtplayertests.py @@ -4,19 +4,37 @@ # 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): _kodiwrapper = mock.MagicMock() - _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _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) _vrtplayer = vrtplayer.VRTPlayer(_kodiwrapper, _apihelper) @@ -35,58 +53,64 @@ def test_tvshows(self): def test_show_videos_single_episode_shows_videos(self): path = '/vrtnu/a-z/marathonradio.relevant/' - episode_items, sort, ascending = self._apihelper.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') self._vrtplayer.show_episodes(path) 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._apihelper.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') self._vrtplayer.show_episodes(path) 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._apihelper.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') self._vrtplayer.show_episodes(path) 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._apihelper.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') self._vrtplayer.show_episodes(path) self.assertTrue(self._kodiwrapper.show_listing.called) def test_get_recent_episodes(self): ''' Test items, sort and order ''' - episode_items, sort, ascending = self._apihelper.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._apihelper.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 ''' @@ -110,8 +134,9 @@ def test_random_tvshow_episodes(self): self.assertTrue(tvshow_items, msg=category['id']) tvshow = random.choice(tvshow_items) - episode_items, sort, ascending = self._apihelper.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__':