From eb5b4265239aeac7bb35ae1f3357122a51f58699 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 15 May 2019 14:52:30 +0200 Subject: [PATCH] Add "My programs" menu (#213) This PR adds a way of tracking the programs you follow. - Add a context menu to programs or episodes with either "Follow" or "Unfollow" - Download and cache favorites from VRT NU - Add an A-Z listing of followed programs - Add a Recent listing of followed programs - Add invisible setting 'usefavorites' to disable "My programs" (for unit tests) - Disable brand filter in "My programs" recent listing (should be configurable really) --- Makefile | 1 + addon.py | 21 +++- addon.xml | 1 + .../resource.language.en_gb/strings.po | 49 ++++++++- .../resource.language.nl_nl/strings.po | 50 ++++++++- resources/lib/helperobjects/helperobjects.py | 3 +- resources/lib/kodiwrappers/kodiwrapper.py | 18 +++- resources/lib/vrtplayer/actions.py | 4 + resources/lib/vrtplayer/favorites.py | 100 ++++++++++++++++++ resources/lib/vrtplayer/statichelper.py | 6 ++ resources/lib/vrtplayer/tokenresolver.py | 65 ++++++++++-- resources/lib/vrtplayer/vrtapihelper.py | 62 +++++++++-- resources/lib/vrtplayer/vrtplayer.py | 59 ++++++++--- resources/settings.xml | 4 +- test/favoritestests.py | 53 ++++++++++ 15 files changed, 458 insertions(+), 38 deletions(-) create mode 100644 resources/lib/vrtplayer/favorites.py create mode 100644 test/favoritestests.py diff --git a/Makefile b/Makefile index 9edf3f36..8bb816f1 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ unit: PYTHONPATH=$(pwd) python test/apihelpertests.py PYTHONPATH=$(pwd) python test/tvguidetests.py PYTHONPATH=$(pwd) python test/searchtests.py + PYTHONPATH=$(pwd) python test/favoritestests.py @echo -e "$(white)=$(blue) Unit tests finished successfully.$(reset)" zip: test diff --git a/addon.py b/addon.py index 0e1aa8fd..e2ed9e56 100644 --- a/addon.py +++ b/addon.py @@ -39,6 +39,21 @@ def router(params_string): _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(program=params.get('program'), path=params.get('path')) + return + if action == actions.UNFOLLOW: + from resources.lib.vrtplayer import favorites + _favorites = favorites.Favorites(_kodiwrapper) + _favorites.unfollow(program=params.get('program'), path=params.get('path')) + return + if action == actions.REFRESH_FAVORITES: + from resources.lib.vrtplayer import favorites + _favorites = favorites.Favorites(_kodiwrapper) + _favorites.update_favorites() + return from resources.lib.vrtplayer import vrtapihelper, vrtplayer _apihelper = vrtapihelper.VRTApiHelper(_kodiwrapper) @@ -47,13 +62,15 @@ def router(params_string): if action == actions.PLAY: _vrtplayer.play(params) elif action == actions.LISTING_AZ_TVSHOWS: - _vrtplayer.show_tvshow_menu_items() + _vrtplayer.show_tvshow_menu_items(filtered=params.get('filtered')) 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')) + elif action == actions.LISTING_FAVORITES: + _vrtplayer.show_favorites_menu_items() elif action == actions.LISTING_LIVE: _vrtplayer.show_livestream_items() elif action == actions.LISTING_EPISODES: @@ -61,7 +78,7 @@ def router(params_string): elif action == actions.LISTING_ALL_EPISODES: _vrtplayer.show_all_episodes(path=params.get('video_url')) elif action == actions.LISTING_RECENT: - _vrtplayer.show_recent(page=params.get('page', 1)) + _vrtplayer.show_recent(page=params.get('page', 1), filtered=params.get('filtered')) 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 59b4294a..d652b4ce 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 d2901e7a..05672766 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -20,10 +20,14 @@ msgid "Interface" msgstr "" msgctxt "#30001" -msgid "Show episode permalink in plot" +msgid "Enable My programs" msgstr "" msgctxt "#30002" +msgid "Show episode permalink in plot" +msgstr "" + +msgctxt "#30003" msgid "Enable menu caching" msgstr "" @@ -75,6 +79,10 @@ msgctxt "#30042" msgid "Install Widevine (for DRM content)" msgstr "" +msgctxt "#30047" +msgid "Refresh favorites" +msgstr "" + msgctxt "#30048" msgid "Clear VRT cookies" msgstr "" @@ -115,6 +123,14 @@ msgctxt "#30061" msgid "Using a SOCKS proxy requires the PySocks library (script.module.pysocks) installed." msgstr "" +msgctxt "#30078" +msgid "My programs" +msgstr "" + +msgctxt "#30079" +msgid "Browse only the programs you follow" +msgstr "" + msgctxt "#30080" msgid "A-Z" msgstr "" @@ -251,3 +267,34 @@ msgctxt "#30334" msgid "In 2 days" msgstr "" +msgctxt "#30411" +msgid "Follow" +msgstr "" + +msgctxt "#30412" +msgid "Unfollow" +msgstr "" + +msgctxt "#30415" +msgid "No followed programs found" +msgstr "" + +msgctxt "#30416" +msgid "We could not find any programs that were followed.\n\nEither right-click on a program or an episode to follow a program, or follow a program on the VRT NU website." +msgstr "" + +msgctxt "#30420" +msgid "My A-Z" +msgstr "" + +msgctxt "#30421" +msgid "Alphabetically sorted list of My TV programs" +msgstr "" + +msgctxt "#30422" +msgid "My recent items" +msgstr "" + +msgctxt "#30423" +msgid "Recently published episodes of My TV programs" +msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 412ccc02..26bb958b 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -21,10 +21,14 @@ msgid "Interface" msgstr "Interface" msgctxt "#30001" +msgid "Enable My programs" +msgstr "Toon Mijn TV programma's" + +msgctxt "#30002" msgid "Show episode permalink in plot" msgstr "Toon aflevering permalink in beschrijving" -msgctxt "#30002" +msgctxt "#30003" msgid "Enable menu caching" msgstr "Gebruik menu caching" @@ -84,6 +88,10 @@ msgctxt "#30042" msgid "Install Widevine (for DRM content)" msgstr "Installeer Widevine (voor DRM content)" +msgctxt "#30047" +msgid "Refresh favorites" +msgstr "Ververs gevolgde programma's" + msgctxt "#30048" msgid "Clear VRT cookies" msgstr "Verwijder VRT cookies" @@ -124,6 +132,14 @@ msgctxt "#30061" msgid "Using a SOCKS proxy requires the PySocks library (script.module.pysocks) installed." msgstr "Het gebruik van SOCKS proxies vereist dat de PySocks library (script.module.pysocks) geïnstalleerd is." +msgctxt "#30078" +msgid "My programs" +msgstr "Mijn TV programma's" + +msgctxt "#30079" +msgid "Browse only the programs you follow" +msgstr "Bekijk enkel de programma's die je volgt" + msgctxt "#30080" msgid "A-Z" msgstr "A-Z" @@ -259,3 +275,35 @@ msgstr "Morgen" msgctxt "#30334" msgid "In 2 days" msgstr "Overmorgen" + +msgctxt "#30411" +msgid "Follow" +msgstr "Volg" + +msgctxt "#30412" +msgid "Unfollow" +msgstr "Vergeet" + +msgctxt "#30415" +msgid "No followed programs found" +msgstr "Geen programma's worden gevolgd" + +msgctxt "#30416" +msgid "We could not find any programs that were followed.\n\nEither right-click on a program or an episode to follow a program, or follow a program on the VRT NU website." +msgstr "We konden geen programma's vonden die je volgt.\n\nJe kan een programma volgen door rechts te klikken op een programma of aflevering, of om ze op de VRT NU website to volgen." + +msgctxt "#30420" +msgid "My A-Z" +msgstr "Mijn TV programma's" + +msgctxt "#30421" +msgid "Alphabetically sorted list of My TV programs" +msgstr "Alle TV-programma's die je volgt in alfabetische volgorde" + +msgctxt "#30422" +msgid "My recent items" +msgstr "Mijn recente afleveringen" + +msgctxt "#30423" +msgid "Recently published episodes of My TV programs" +msgstr "Recent gepubliceerde afleveringen van TV-programma's die je volgt" diff --git a/resources/lib/helperobjects/helperobjects.py b/resources/lib/helperobjects/helperobjects.py index 0b00ac57..10ce7abd 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 31261ad2..c15f56d0 100644 --- a/resources/lib/kodiwrappers/kodiwrapper.py +++ b/resources/lib/kodiwrappers/kodiwrapper.py @@ -155,6 +155,9 @@ def show_listing(self, list_items, sort='unsorted', ascending=True, content=None 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)) @@ -199,7 +202,13 @@ def get_search_string(self): def show_ok_dialog(self, title, message): import xbmcgui - xbmcgui.Dialog().ok(self._addon.getAddonInfo('name'), title, message) + if not title: + title = self._addon.getAddonInfo('name') + xbmcgui.Dialog().ok(title, message) + + def show_notification(self, message, time=4000): + import xbmcgui + xbmcgui.Dialog().notification(self._addon.getAddonInfo('name'), message, xbmcgui.NOTIFICATION_INFO, time) def set_locale(self): import locale @@ -334,10 +343,17 @@ def open_file(self, path, flags='r'): yield f f.close() + def stat_file(self, path): + import xbmcvfs + return xbmcvfs.Stat(path) + def delete_file(self, path): import xbmcvfs return xbmcvfs.delete(path) + def container_refresh(self): + xbmc.executebuiltin('Container.Refresh') + def log_access(self, url, query_string, log_level='Verbose'): ''' Log addon access ''' if log_levels.get(log_level, 0) <= self._max_log_level: diff --git a/resources/lib/vrtplayer/actions.py b/resources/lib/vrtplayer/actions.py index 8a4310c6..c42c8536 100644 --- a/resources/lib/vrtplayer/actions.py +++ b/resources/lib/vrtplayer/actions.py @@ -5,14 +5,18 @@ 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' +REFRESH_FAVORITES = 'refreshfavorites' SEARCH = 'search' +UNFOLLOW = 'unfollow' diff --git a/resources/lib/vrtplayer/favorites.py b/resources/lib/vrtplayer/favorites.py new file mode 100644 index 00000000..30eedf5e --- /dev/null +++ b/resources/lib/vrtplayer/favorites.py @@ -0,0 +1,100 @@ +# -*- 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() + + 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.mktime(time.localtime()) - (2 * 60): + self._kodiwrapper.log_notice('CACHE: %s vs %s' % (self._kodiwrapper.stat_file(self._cache_file).st_mtime(), time.mktime(time.localtime()) - (5 * 60)), 'Debug') + with self._kodiwrapper.open_file(self._cache_file) as f: + self._favorites = json.loads(f.read()) + return + self.update_favorites() + + def update_favorites(self): + xvrttoken = self._tokenresolver.get_fav_xvrttoken() + headers = { + 'authorization': 'Bearer ' + xvrttoken, + 'content-type': 'application/json', + # 'Cookie': 'X-VRT-Token=' + xvrttoken, + 'Referer': 'https://www.vrt.be/vrtnu', + } + req = Request('https://video-user-data.vrt.be/favorites', headers=headers) + self._favorites = json.loads(urlopen(req).read()) + self.write_favorites() + + def set_favorite(self, program, path, value=True): + if value is not self.is_favorite(path): + xvrttoken = self._tokenresolver.get_fav_xvrttoken() + headers = { + 'authorization': 'Bearer ' + xvrttoken, + 'content-type': 'application/json', + # 'Cookie': 'X-VRT-Token=' + xvrttoken, + 'Referer': 'https://www.vrt.be/vrtnu', + } + payload = dict(isFavorite=value, programUrl=path, title=program) + self._kodiwrapper.log_notice('URL post: https://video-user-data.vrt.be/favorites/%s' % self.uuid(path), 'Verbose') + 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, program, path): + self._kodiwrapper.show_notification('Follow ' + program) + self.set_favorite(program, path, True) + self._kodiwrapper.container_refresh() + + def unfollow(self, program, path): + self._kodiwrapper.show_notification('Unfollow ' + program) + self.set_favorite(program, path, False) + self._kodiwrapper.container_refresh() + + 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 f3cc721c..7c7be081 100644 --- a/resources/lib/vrtplayer/statichelper.py +++ b/resources/lib/vrtplayer/statichelper.py @@ -30,6 +30,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/tokenresolver.py b/resources/lib/vrtplayer/tokenresolver.py index aa8e865b..599fbd5f 100644 --- a/resources/lib/vrtplayer/tokenresolver.py +++ b/resources/lib/vrtplayer/tokenresolver.py @@ -7,10 +7,10 @@ try: from urllib.parse import urlencode, unquote - from urllib.request import build_opener, install_opener, ProxyHandler, HTTPErrorProcessor, urlopen, Request + from urllib.request import build_opener, install_opener, ProxyHandler, HTTPCookieProcessor, HTTPErrorProcessor, urlopen, Request except ImportError: from urllib import urlencode # pylint: disable=ungrouped-imports - from urllib2 import build_opener, install_opener, ProxyHandler, HTTPErrorProcessor, unquote, urlopen, Request + from urllib2 import build_opener, install_opener, ProxyHandler, HTTPCookieProcessor, HTTPErrorProcessor, unquote, urlopen, Request class NoRedirection(HTTPErrorProcessor): @@ -26,9 +26,12 @@ class TokenResolver: _API_KEY = '3_qhEcPa5JGFROVwu5SWKqJ4mVOIkwlFNMSKwzPDAh8QZOtHqu6L4nD5Q7lk0eXOOG' _LOGIN_URL = 'https://accounts.vrt.be/accounts.login' _TOKEN_GATEWAY_URL = 'https://token.vrt.be' + _VRT_LOGIN_URL = 'https://login.vrt.be/perform_login' _ONDEMAND_COOKIE = 'ondemand_vrtPlayerToken' _LIVE_COOKIE = 'live_vrtPlayerToken' _ROAMING_XVRTTOKEN_COOKIE = 'roaming_XVRTToken' + _FAV_TOKEN_GATEWAY_URL = 'https://token.vrt.be/vrtnuinitlogin?provider=site&destination=https://www.vrt.be/vrtnu/' + _FAV_XVRTTOKEN_COOKIE = 'user_XVRTToken' _XVRT_TOKEN_COOKIE = 'XVRTToken' def __init__(self, _kodiwrapper): @@ -67,11 +70,14 @@ def get_xvrttoken(self, get_roaming_token=False): token = self._get_new_xvrttoken(token_path, get_roaming_token) return token - @staticmethod - def get_cookie_from_cookiejar(cookiename, cookiejar): - for cookie in cookiejar: - if cookie.name == cookiename: - yield cookie + def get_fav_xvrttoken(self): + token_filename = self._FAV_XVRTTOKEN_COOKIE + token_path = self._kodiwrapper.get_userdata_path() + token_filename + token = self._get_cached_token(token_path, 'X-VRT-Token') + + if token is None: + token = self._get_fav_xvrttoken(token_path) + return token def _get_new_playertoken(self, path, token_url, headers): import json @@ -143,6 +149,47 @@ def _get_new_xvrttoken(self, path, get_roaming_token): self._handle_error(logon_json, cred) return token + def _get_fav_xvrttoken(self, path): + import cookielib + import json + cred = helperobjects.Credentials(self._kodiwrapper) + if not cred.are_filled_in(): + self._kodiwrapper.open_settings() + cred.reload() + data = dict( + loginID=cred.username, + password=cred.password, + sessionExpiration='-1', + APIKey=self._API_KEY, + targetEnv='jssdk', + ) + 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 + if logon_json.get('errorCode') == 0: + payload = dict( + UID=logon_json.get('UID'), + UIDSignature=logon_json.get('UIDSignature'), + signatureTimestamp=logon_json.get('signatureTimestamp'), + client_id='vrtnu-site', + submit='submit', + ) + cookiejar = cookielib.CookieJar() + opener = build_opener(HTTPCookieProcessor(cookiejar)) + self._kodiwrapper.log_notice('URL get: ' + unquote(self._FAV_TOKEN_GATEWAY_URL), 'Verbose') + opener.open(self._FAV_TOKEN_GATEWAY_URL) + self._kodiwrapper.log_notice('URL post: ' + unquote(self._VRT_LOGIN_URL), 'Verbose') + opener.open(self._VRT_LOGIN_URL, data=urlencode(payload)) + xvrttoken = TokenResolver._create_token_dictionary(cookiejar) + if xvrttoken is not None: + token = xvrttoken.get('X-VRT-Token') + 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._kodiwrapper.get_localized_string(30051) @@ -168,6 +215,7 @@ def _get_roaming_xvrttoken(self, xvrttoken): req_info = opener.open(req).info() cookie_value += '; state=' + req_info.getheader('Set-Cookie').split('state=')[1].split('; ')[0] url = req_info.getheader('Location') + self._kodiwrapper.log_notice('URL get: ' + unquote(url), 'Verbose') url = opener.open(url).info().getheader('Location') headers = {'Cookie': cookie_value} if url is not None: @@ -180,7 +228,8 @@ def _get_roaming_xvrttoken(self, xvrttoken): @staticmethod def _create_token_dictionary(cookie_jar): token_dictionary = None - xvrttoken_cookie = next(TokenResolver.get_cookie_from_cookiejar('X-VRT-Token', cookie_jar)) + # Get cookie from cookiejar + xvrttoken_cookie = next(cookie for cookie in cookie_jar if cookie.name == 'X-VRT-Token') if xvrttoken_cookie is not None: from datetime import datetime token_dictionary = { diff --git a/resources/lib/vrtplayer/vrtapihelper.py b/resources/lib/vrtplayer/vrtapihelper.py index 4da886e5..04c96aee 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,12 @@ def __init__(self, _kodiwrapper): self._proxies = _kodiwrapper.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) self._showpermalink = _kodiwrapper.get_setting('showpermalink') == 'true' + if _kodiwrapper.get_setting('usefavorites') == 'true': + self._favorites = favorites.Favorites(self._kodiwrapper) + else: + self._favorites = None - def get_tvshow_items(self, category=None, channel=None): + def get_tvshow_items(self, category=None, channel=None, filtered=False): import json params = dict() @@ -44,11 +48,15 @@ def get_tvshow_items(self, category=None, channel=None): api_url = self._VRTNU_SUGGEST_URL + '?' + urlencode(params) 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', '???') @@ -59,6 +67,16 @@ 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: + if self._favorites.is_favorite(program_path): + params = dict(action='unfollow', program=tvshow.get('title'), path=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=tvshow.get('title'), path=program_path) + context_menu = [(self._kodiwrapper.get_localized_string(30411), 'RunPlugin(plugin://plugin.video.vrt.nu?%s)' % urlencode(params))] + else: + context_menu = [] # 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, '') @@ -68,6 +86,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 @@ -88,7 +107,7 @@ def _get_season_items(self, api_url, api_json): pass return season_items, sort, ascending, content - 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' @@ -100,13 +119,18 @@ def get_episode_items(self, path=None, page=None, all_seasons=False): 'from': ((page - 1) * 50) + 1, 'i': 'video', 'size': 50, - 'facets[transcodingStatus]': 'AVAILABLE', - 'facets[programBrands]': '[een,canvas,sporza,vrtnws,vrtnxt,radio1,radio2,klara,stubru,mnm]', + # 'facets[transcodingStatus]': 'AVAILABLE', } + + if filtered: + params['facets[programName]'] = '[%s]' % (','.join(self._favorites.names())) + else: + params['facets[programBrands]'] = '[een,canvas,sporza,vrtnws,vrtnxt,radio1,radio2,klara,stubru,mnm]' + api_url = self._VRTNU_SEARCH_URL + '?' + urlencode(params) self._kodiwrapper.log_notice('URL get: ' + unquote(api_url), 'Verbose') api_json = json.loads(urlopen(api_url).read()) - episode_items, sort, ascending, content = self._map_to_episode_items(api_json.get('results', []), titletype='recent') + episode_items, sort, ascending, content = self._map_to_episode_items(api_json.get('results', []), titletype='recent', filtered=filtered) if path: if '.relevant/' in path: @@ -133,6 +157,7 @@ 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: @@ -148,13 +173,15 @@ def get_episode_items(self, path=None, page=None, all_seasons=False): return episode_items, sort, ascending, content - def _map_to_episode_items(self, episodes, titletype=None, season_key=None): + def _map_to_episode_items(self, episodes, titletype=None, season_key=None, filtered=False): from datetime import datetime import dateutil.parser import dateutil.tz now = datetime.now(dateutil.tz.tzlocal()) sort = 'episode' ascending = True + if filtered: + favorite_names = self._favorites.names() episode_items = [] for episode in episodes: # VRT API workaround: seasonTitle facet behaves as a partial match regex, @@ -162,6 +189,9 @@ def _map_to_episode_items(self, episodes, titletype=None, season_key=None): if season_key and episode.get('seasonTitle') != season_key: continue + if filtered and episode.get('programName') not in favorite_names: + continue + # Support search highlights highlight = episode.get('highlight') if highlight: @@ -222,6 +252,17 @@ 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: + if self._favorites.is_favorite(program_path): + params = dict(action='unfollow', program=episode.get('program'), path=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=episode.get('program'), path=program_path) + context_menu = [(self._kodiwrapper.get_localized_string(30411), 'RunPlugin(plugin://plugin.video.vrt.nu?%s)' % urlencode(params))] + else: + context_menu = [] + 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')) @@ -233,6 +274,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, 'episodes' @@ -249,8 +291,8 @@ def _map_to_season_items(self, api_url, seasons, episodes): metadata = metadatacreator.MetadataCreator() metadata.tvshowtitle = episode.get('program') - metadata.subtitle = statichelper.convert_html_to_kodilabel(episode.get('programDescription')) metadata.plot = statichelper.convert_html_to_kodilabel(episode.get('programDescription')) + metadata.plotoutline = statichelper.convert_html_to_kodilabel(episode.get('programDescription')) metadata.mediatype = 'season' metadata.brands = episode.get('programBrands') or episode.get('brands') metadata.geolocked = episode.get('allowedRegion') == 'BE' diff --git a/resources/lib/vrtplayer/vrtplayer.py b/resources/lib/vrtplayer/vrtplayer.py index 110dd1c3..b9f4fcf3 100644 --- a/resources/lib/vrtplayer/vrtplayer.py +++ b/resources/lib/vrtplayer/vrtplayer.py @@ -21,7 +21,19 @@ def __init__(self, _kodiwrapper, _apihelper): self._apihelper = _apihelper def show_main_menu_items(self): - main_items = [ + main_items = [] + + # Only add 'My programs' when this is enabled in config + if self._kodiwrapper.get_setting('usefavorites') == 'true': + main_items.append(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)) + )) + + main_items.extend([ TitleItem(title=self._kodiwrapper.get_localized_string(30080), url_dict=dict(action=actions.LISTING_AZ_TVSHOWS), is_playable=False, @@ -57,11 +69,31 @@ def show_main_menu_items(self): is_playable=False, art_dict=dict(thumb='DefaultAddonsSearch.png', icon='DefaultAddonsSearch.png', fanart='DefaultAddonsSearch.png'), video_dict=dict(plot=self._kodiwrapper.get_localized_string(30093))), - ] + ]) self._kodiwrapper.show_listing(main_items) - def show_tvshow_menu_items(self, category=None): - tvshow_items = self._apihelper.get_tvshow_items(category=category) + def show_favorites_menu_items(self): + favorites_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(favorites_items) + + # Show dialog when no favorites were found + from resources.lib.vrtplayer import favorites + if not favorites.Favorites(self._kodiwrapper).names(): + self._kodiwrapper.show_ok_dialog(self._kodiwrapper.get_localized_string(30415), self._kodiwrapper.get_localized_string(30416)) + + 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): @@ -140,22 +172,23 @@ def show_all_episodes(self, path): 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, content = 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( - 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(), - )) + if len(episode_items) == 50: + episode_items.append(TitleItem( + title=self._kodiwrapper.get_localized_string(30300), + url_dict=dict(action=actions.LISTING_RECENT, page=page + 1, filtered=filtered), + is_playable=False, + art_dict=dict(thumb='DefaultYear.png', icon='DefaultYear.png', fanart='DefaultYear.png'), + video_dict=dict(), + )) self._kodiwrapper.show_listing(episode_items, sort=sort, ascending=ascending, content=content, cache=False) diff --git a/resources/settings.xml b/resources/settings.xml index 87df01f7..6645c8fe 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,8 +1,9 @@ - + + @@ -18,6 +19,7 @@ + diff --git a/test/favoritestests.py b/test/favoritestests.py new file mode 100644 index 00000000..44ac93c4 --- /dev/null +++ b/test/favoritestests.py @@ -0,0 +1,53 @@ +# -*- 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 +import mock +import os +import unittest + +from resources.lib.vrtplayer import favorites +from test import SETTINGS, get_setting, log_notice, open_file, stat_file + +SETTINGS['usefavorites'] = 'true' + + +class TestFavorites(unittest.TestCase): + + _kodiwrapper = mock.MagicMock() + _kodiwrapper.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) + _kodiwrapper.get_proxies = mock.MagicMock(return_value=dict()) + _kodiwrapper.get_setting = mock.MagicMock(side_effect=get_setting) + _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=stat_file) + _favorites = favorites.Favorites(_kodiwrapper) + + def test_follow_unfollow(self): + program = 'Winteruur' + program_path = '/vrtnu/a-z/winteruur/' + self._favorites.follow(program, program_path) + self.assertTrue(self._favorites.is_favorite(program_path)) + + self._favorites.unfollow(program, program_path) + self.assertFalse(self._favorites.is_favorite(program_path)) + + self._favorites.follow(program, program_path) + self.assertTrue(self._favorites.is_favorite(program_path)) + + 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()