From 9af16e9da7cbe5fa3c78064f46e5432073e63416 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Sat, 11 May 2019 19:08:23 +0200 Subject: [PATCH] Add "My programs" menu 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) --- .travis.yml | 1 + 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 | 104 ++++++++++++++++++ resources/lib/vrtplayer/statichelper.py | 6 + resources/lib/vrtplayer/tokenresolver.py | 1 + resources/lib/vrtplayer/vrtapihelper.py | 66 +++++++++-- resources/lib/vrtplayer/vrtplayer.py | 71 ++++++++---- resources/settings.xml | 4 +- test/apihelpertests.py | 12 +- test/favoritestests.py | 53 +++++++++ test/vrtplayertests.py | 21 ++-- 18 files changed, 439 insertions(+), 47 deletions(-) create mode 100644 resources/lib/vrtplayer/favorites.py create mode 100644 test/favoritestests.py diff --git a/.travis.yml b/.travis.yml index 5a1d1576b..b3f931a6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,3 +20,4 @@ script: - python test/vrtplayertests.py - python test/apihelpertests.py - python test/tvguidetests.py +- python test/searchtests.py diff --git a/Makefile b/Makefile index 9edf3f36c..8bb816f14 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 0e1aa8fd2..e2ed9e56f 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 59b4294a5..d652b4ceb 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..b4acc91fd 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 "Interface" msgctxt "#30001" +msgid "Enable My programs" +msgstr "Enable My programs" + +msgctxt "#30002" msgid "Show episode permalink in plot" msgstr "Show episode permalink in plot" -msgctxt "#30002" +msgctxt "#30003" msgid "Enable menu caching" msgstr "Enable menu caching" @@ -75,6 +79,10 @@ msgctxt "#30042" msgid "Install Widevine (for DRM content)" msgstr "Install Widevine (for DRM content)" +msgctxt "#30047" +msgid "Refresh favorites" +msgstr "Refresh favorites" + msgctxt "#30048" msgid "Clear VRT cookies" msgstr "Clear VRT cookies" @@ -115,6 +123,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 +267,34 @@ msgctxt "#30334" msgid "In 2 days" msgstr "In 2 days" +msgctxt "#30411" +msgid "Follow" +msgstr "Follow" + +msgctxt "#30412" +msgid "Unfollow" +msgstr "Unfollow" + +msgctxt "#30415" +msgid "No followed programs found" +msgstr "No followed programs found" + +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 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." + +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..26bb958b7 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 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 2cb276b1a..cdb7b2357 100644 --- a/resources/lib/kodiwrappers/kodiwrapper.py +++ b/resources/lib/kodiwrappers/kodiwrapper.py @@ -104,6 +104,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)) @@ -148,7 +151,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 @@ -263,10 +272,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 8a4310c67..c42c85362 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 000000000..69716baa1 --- /dev/null +++ b/resources/lib/vrtplayer/favorites.py @@ -0,0 +1,104 @@ +# -*- 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_xvrttoken() + headers = { + 'accept': 'application/json', + 'authorization': 'Bearer ' + xvrttoken, + 'content-type': 'application/json', + 'Cookie': 'X-VRT-Token=' + xvrttoken, + 'DNT': '1', + '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_xvrttoken() + headers = { + 'accept': 'application/json', + 'authorization': 'Bearer ' + xvrttoken, + 'content-type': 'application/json', + 'Cookie': 'X-VRT-Token=' + xvrttoken, + 'DNT': '1', + '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 cb5a7a042..311ceaa1c 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/tokenresolver.py b/resources/lib/vrtplayer/tokenresolver.py index aa8e865b7..4cc9b2eb1 100644 --- a/resources/lib/vrtplayer/tokenresolver.py +++ b/resources/lib/vrtplayer/tokenresolver.py @@ -168,6 +168,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: diff --git a/resources/lib/vrtplayer/vrtapihelper.py b/resources/lib/vrtplayer/vrtapihelper.py index 16edec35a..80926ba5a 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 @@ -87,7 +106,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' @@ -99,13 +118,19 @@ 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 = self._map_to_episode_items(api_json.get('results', []), titletype='recent') + episode_items, sort, ascending = self._map_to_episode_items(api_json.get('results', []), titletype='recent', filtered=filtered) + content = 'episodes' if path: if '.relevant/' in path: @@ -132,28 +157,34 @@ 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): + 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, @@ -161,6 +192,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 + display_options = episode.get('displayOptions', dict()) # NOTE: Hard-code showing seasons because it is unreliable (i.e; Thuis or Down the Road have it disabled) @@ -215,6 +249,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')) @@ -226,6 +271,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 diff --git a/resources/lib/vrtplayer/vrtplayer.py b/resources/lib/vrtplayer/vrtplayer.py index a51422712..62f500dad 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): @@ -133,31 +165,32 @@ 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='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='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( - 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._kodiwrapper.show_listing(episode_items, sort=sort, ascending=ascending, content='episodes', cache=False) + 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) def play(self, params): from resources.lib.vrtplayer import streamservice, tokenresolver diff --git a/resources/settings.xml b/resources/settings.xml index 87df01f72..6645c8fe4 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,8 +1,9 @@ - + + @@ -18,6 +19,7 @@ + diff --git a/test/apihelpertests.py b/test/apihelpertests.py index dc2c6db48..fea4aaf24 100644 --- a/test/apihelpertests.py +++ b/test/apihelpertests.py @@ -26,28 +26,32 @@ class ApiHelperTests(unittest.TestCase): _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/') + 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..44ac93c4a --- /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() diff --git a/test/vrtplayertests.py b/test/vrtplayertests.py index 666a459b9..d8d6f7d05 100644 --- a/test/vrtplayertests.py +++ b/test/vrtplayertests.py @@ -42,58 +42,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 ''' @@ -117,8 +123,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__':