diff --git a/resources/lib/addon.py b/resources/lib/addon.py index d616069f..903ddb4f 100644 --- a/resources/lib/addon.py +++ b/resources/lib/addon.py @@ -10,40 +10,39 @@ except ImportError: # Python 2 from urllib import unquote_plus -from kodiwrapper import KodiWrapper +from kodiutils import localize, log_access, notification, refresh_caches from statichelper import from_unicode, to_unicode # pylint: disable=invalid-name plugin = Plugin() -kodi = KodiWrapper(globals()) @plugin.route('/') def main_menu(): ''' The VRT NU plugin main menu ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_main_menu() + VRTPlayer().show_main_menu() @plugin.route('/cache/delete') @plugin.route('/cache/delete/') def delete_cache(cache_file='*.json'): ''' The API interface to delete caches ''' - kodi.refresh_caches(cache_file=cache_file) + refresh_caches(cache_file=cache_file) @plugin.route('/tokens/delete') def delete_tokens(): ''' The API interface to delete all VRT tokens ''' from tokenresolver import TokenResolver - TokenResolver(kodi).delete_tokens() + TokenResolver().delete_tokens() @plugin.route('/follow//') def follow(program, title): ''' The API interface to follow a program used by the context menu ''' from favorites import Favorites - Favorites(kodi).follow(program=program, title=to_unicode(unquote_plus(from_unicode(title)))) + Favorites().follow(program=program, title=to_unicode(unquote_plus(from_unicode(title)))) @plugin.route('/unfollow/<program>/<title>') @@ -51,42 +50,42 @@ def unfollow(program, title): ''' The API interface to unfollow a program used by the context menu ''' move_down = bool(plugin.args.get('move_down')) from favorites import Favorites - Favorites(kodi).unfollow(program=program, title=to_unicode(unquote_plus(from_unicode(title))), move_down=move_down) + Favorites().unfollow(program=program, title=to_unicode(unquote_plus(from_unicode(title))), move_down=move_down) @plugin.route('/watchlater/<path:url>/<uuid>/<title>') def watchlater(uuid, title, url): ''' The API interface to watch an episode used by the context menu ''' from resumepoints import ResumePoints - ResumePoints(kodi).watchlater(uuid=uuid, title=to_unicode(unquote_plus(from_unicode(title))), url=url) + ResumePoints().watchlater(uuid=uuid, title=to_unicode(unquote_plus(from_unicode(title))), url=url) @plugin.route('/unwatchlater/<path:url>/<uuid>/<title>') def unwatchlater(uuid, title, url): ''' The API interface to unwatch an episode used by the context menu ''' from resumepoints import ResumePoints - ResumePoints(kodi).unwatchlater(uuid=uuid, title=to_unicode(unquote_plus(from_unicode(title))), url=url) + ResumePoints().unwatchlater(uuid=uuid, title=to_unicode(unquote_plus(from_unicode(title))), url=url) @plugin.route('/favorites') def favorites_menu(): ''' The My favorites menu ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_favorites_menu() + VRTPlayer().show_favorites_menu() @plugin.route('/favorites/programs') def favorites_programs(): ''' The favorites 'My programs' listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_tvshow_menu(use_favorites=True) + VRTPlayer().show_tvshow_menu(use_favorites=True) @plugin.route('/favorites/docu') def favorites_docu(): ''' The favorites docu listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_favorites_docu_menu() + VRTPlayer().show_favorites_docu_menu() @plugin.route('/favorites/recent') @@ -94,7 +93,7 @@ def favorites_docu(): def favorites_recent(page=1): ''' The favorites recent listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_recent_menu(page=page, use_favorites=True) + VRTPlayer().show_recent_menu(page=page, use_favorites=True) @plugin.route('/favorites/offline') @@ -102,44 +101,44 @@ def favorites_recent(page=1): def favorites_offline(page=1): ''' The favorites offline listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_offline_menu(page=page, use_favorites=True) + VRTPlayer().show_offline_menu(page=page, use_favorites=True) @plugin.route('/favorites/refresh') def favorites_refresh(): ''' The API interface to refresh the favorites cache ''' from favorites import Favorites - Favorites(kodi).refresh(ttl=0) - kodi.show_notification(message=kodi.localize(30982)) + Favorites().refresh(ttl=0) + notification(message=localize(30982)) @plugin.route('/favorites/manage') def favorites_manage(): ''' The API interface to manage your favorites ''' from favorites import Favorites - Favorites(kodi).manage() + Favorites().manage() @plugin.route('/resumepoints/continue') def resumepoints_continue(): ''' The resumepoints continue listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_continue_menu(page=1) + VRTPlayer().show_continue_menu(page=1) @plugin.route('/resumepoints/refresh') def resumepoints_refresh(): ''' The API interface to refresh the resumepoints cache ''' from resumepoints import ResumePoints - ResumePoints(kodi).refresh(ttl=0) - kodi.show_notification(message=kodi.localize(30983)) + ResumePoints().refresh(ttl=0) + notification(message=localize(30983)) @plugin.route('/resumepoints/watchlater') def resumepoints_watchlater(): ''' The resumepoints watchlater listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_watchlater_menu(page=1) + VRTPlayer().show_watchlater_menu(page=1) @plugin.route('/programs') @@ -149,9 +148,9 @@ def programs(program=None, season=None): ''' The Programs / Seasons / Episodes listing ''' from vrtplayer import VRTPlayer if program: - VRTPlayer(kodi).show_episodes_menu(program=program, season=season) + VRTPlayer().show_episodes_menu(program=program, season=season) else: - VRTPlayer(kodi).show_tvshow_menu() + VRTPlayer().show_tvshow_menu() @plugin.route('/categories') @@ -159,7 +158,7 @@ def programs(program=None, season=None): def categories(category=None): ''' The categories menu and listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_category_menu(category=category) + VRTPlayer().show_category_menu(category=category) @plugin.route('/channels') @@ -167,14 +166,14 @@ def categories(category=None): def channels(channel=None): ''' The channels menu and listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_channels_menu(channel=channel) + VRTPlayer().show_channels_menu(channel=channel) @plugin.route('/livetv') def livetv(): ''' The livetv menu ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_livetv_menu() + VRTPlayer().show_livetv_menu() @plugin.route('/recent') @@ -182,7 +181,7 @@ def livetv(): def recent(page=1): ''' The most recent items listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_recent_menu(page=page) + VRTPlayer().show_recent_menu(page=page) @plugin.route('/offline') @@ -190,7 +189,7 @@ def recent(page=1): def offline(page=1): ''' The soon offline listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_offline_menu(page=page) + VRTPlayer().show_offline_menu(page=page) @plugin.route('/featured') @@ -198,7 +197,7 @@ def offline(page=1): def featured(feature=None): ''' The featured menu and listing ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).show_featured_menu(feature=feature) + VRTPlayer().show_featured_menu(feature=feature) @plugin.route('/tvguide') @@ -208,7 +207,7 @@ def featured(feature=None): def tvguide(date=None, channel=None): ''' The TV guide menu and listings by date ''' from tvguide import TVGuide - TVGuide(kodi).show_tvguide(date=date, channel=channel) + TVGuide().show_tvguide(date=date, channel=channel) @plugin.route('/tvguide/channel') @@ -217,35 +216,35 @@ def tvguide(date=None, channel=None): def tvguide_channel(channel=None, date=None): ''' The TV guide menu and listings by channel ''' from tvguide import TVGuide - TVGuide(kodi).show_tvguide(channel=channel, date=date) + TVGuide().show_tvguide(channel=channel, date=date) @plugin.route('/search') def search(): ''' The Search menu and history ''' from search import Search - Search(kodi).search_menu() + Search().search_menu() @plugin.route('/search/clear') def clear_search(): ''' Clear the search history ''' from search import Search - Search(kodi).clear() + Search().clear() @plugin.route('/search/add/<keywords>') def add_search(keywords): ''' Add to search history ''' from search import Search - Search(kodi).add(keywords) + Search().add(keywords) @plugin.route('/search/remove/<keywords>') def remove_search(keywords): ''' Remove from search history ''' from search import Search - Search(kodi).remove(keywords) + Search().remove(keywords) @plugin.route('/search/query') @@ -254,7 +253,7 @@ def remove_search(keywords): def search_query(keywords=None, page=1): ''' The Search interface and query listing ''' from search import Search - Search(kodi).search(keywords=keywords, page=page) + Search().search(keywords=keywords, page=page) @plugin.route('/play/id/<video_id>') @@ -262,28 +261,28 @@ def search_query(keywords=None, page=1): def play_id(video_id, publication_id=None): ''' The API interface to play a video by video_id and/or publication_id ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).play(dict(video_id=video_id, publication_id=publication_id)) + VRTPlayer().play(dict(video_id=video_id, publication_id=publication_id)) @plugin.route('/play/url/<path:video_url>') def play_url(video_url): ''' The API interface to play a video by using a URL ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).play(dict(video_url=video_url)) + VRTPlayer().play(dict(video_url=video_url)) @plugin.route('/play/latest/<program>') def play_latest(program): ''' The API interface to play the latest episode of a program ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).play_latest_episode(program=program) + VRTPlayer().play_latest_episode(program=program) @plugin.route('/play/whatson/<whatson_id>') def play_whatson(whatson_id): ''' The API interface to play a video by whatson_id ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).play_whatson(whatson_id=whatson_id) + VRTPlayer().play_whatson(whatson_id=whatson_id) @plugin.route('/play/airdate/<channel>/<start_date>') @@ -291,10 +290,10 @@ def play_whatson(whatson_id): def play_by_air_date(channel, start_date, end_date=None): ''' The API interface to play an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00) ''' from vrtplayer import VRTPlayer - VRTPlayer(kodi).play_episode_by_air_date(channel, start_date, end_date) + VRTPlayer().play_episode_by_air_date(channel, start_date, end_date) def run(argv): ''' Addon entry point from wrapper ''' - kodi.log_access(argv[0]) + log_access(argv[0]) plugin.run(argv) diff --git a/resources/lib/addon_entry.py b/resources/lib/addon_entry.py index db66fd11..07691471 100644 --- a/resources/lib/addon_entry.py +++ b/resources/lib/addon_entry.py @@ -3,6 +3,6 @@ ''' This is the actual VRT NU video plugin entry point ''' from __future__ import absolute_import, division, unicode_literals -import sys +from sys import argv from addon import run -run(sys.argv) +run(argv) diff --git a/resources/lib/apihelper.py b/resources/lib/apihelper.py index 7dfe9688..e84569ec 100644 --- a/resources/lib/apihelper.py +++ b/resources/lib/apihelper.py @@ -15,6 +15,9 @@ import statichelper from data import CHANNELS, SECONDS_MARGIN from helperobjects import TitleItem +from kodiutils import (delete_cached_thumbnail, get_cache, get_global_setting, get_proxies, get_setting, + has_addon, localize, localize_from_data, log, log_error, ok_dialog, update_cache, + url_for) from metadata import Metadata @@ -26,15 +29,12 @@ class ApiHelper: _VRTNU_SUGGEST_URL = 'https://vrtnu-api.vrt.be/suggest' _VRTNU_SCREENSHOT_URL = 'https://vrtnu-api.vrt.be/screenshots' - def __init__(self, _kodi, _favorites, _resumepoints): + def __init__(self, _favorites, _resumepoints): ''' Constructor for the ApiHelper class ''' - self._kodi = _kodi self._favorites = _favorites self._resumepoints = _resumepoints - self._metadata = Metadata(_kodi, _favorites, _resumepoints) - - self._proxies = _kodi.get_proxies() - install_opener(build_opener(ProxyHandler(self._proxies))) + self._metadata = Metadata(_favorites, _resumepoints) + install_opener(build_opener(ProxyHandler(get_proxies()))) def get_tvshows(self, category=None, channel=None, feature=None): ''' Get all TV shows for a given category, channel or feature, optionally filtered by favorites ''' @@ -56,14 +56,14 @@ def get_tvshows(self, category=None, channel=None, feature=None): if not category and not channel and not feature: params['facets[transcodingStatus]'] = 'AVAILABLE' # Required for getting results in Suggests API cache_file = 'programs.json' - tvshows = self._kodi.get_cache(cache_file, ttl=60 * 60) # Try the cache if it is fresh + tvshows = get_cache(cache_file, ttl=60 * 60) # Try the cache if it is fresh if not tvshows: - import json + from json import load querystring = '&'.join('{}={}'.format(key, value) for key, value in list(params.items())) suggest_url = self._VRTNU_SUGGEST_URL + '?' + querystring - self._kodi.log(2, 'URL get: {url}', url=unquote(suggest_url)) - tvshows = json.load(urlopen(suggest_url)) - self._kodi.update_cache(cache_file, tvshows) + log(2, 'URL get: {url}', url=unquote(suggest_url)) + tvshows = load(urlopen(suggest_url)) + update_cache(cache_file, tvshows) return tvshows @@ -74,7 +74,7 @@ def list_tvshows(self, category=None, channel=None, feature=None, use_favorites= tvshows = self.get_tvshows(category=category, channel=channel, feature=feature) # Get oneoffs - if self._kodi.get_setting('showoneoff', 'true') == 'true': + if get_setting('showoneoff', 'true') == 'true': cache_file = 'oneoff.json' oneoffs = self.get_episodes(variety='oneoff', cache_file=cache_file) else: @@ -94,7 +94,7 @@ def tvshow_to_listitem(self, tvshow, program, cache_file): return TitleItem( title=label, - path=self._kodi.url_for('programs', program=program), + path=url_for('programs', program=program), art_dict=self._metadata.get_art(tvshow), info_dict=self._metadata.get_info_labels(tvshow), context_menu=context_menu, @@ -173,10 +173,10 @@ def __map_seasons(self, program, seasons, episodes): ascending = False # Add an "* All seasons" list item - if self._kodi.get_global_setting('videolibrary.showallitems') is True: + if get_global_setting('videolibrary.showallitems') is True: season_items.append(TitleItem( - title=self._kodi.localize(30133), - path=self._kodi.url_for('programs', program=program, season='allseasons'), + title=localize(30133), + path=url_for('programs', program=program, season='allseasons'), art_dict=self._metadata.get_art(episode, season='allseasons'), info_dict=info_labels, )) @@ -192,10 +192,10 @@ def __map_seasons(self, program, seasons, episodes): except IndexError: episode = episodes[0] - label = '%s %s' % (self._kodi.localize(30131), season_key) + label = '%s %s' % (localize(30131), season_key) season_items.append(TitleItem( title=label, - path=self._kodi.url_for('programs', program=program, season=season_key), + path=url_for('programs', program=program, season=season_key), art_dict=self._metadata.get_art(episode, season=True), info_dict=info_labels, prop_dict=self._metadata.get_properties(episode), @@ -242,7 +242,7 @@ def episode_to_listitem(self, episode, program, cache_file, titletype): return TitleItem( title=label, - path=self._kodi.url_for('play_id', video_id=episode.get('videoId'), publication_id=episode.get('publicationId')), + path=url_for('play_id', video_id=episode.get('videoId'), publication_id=episode.get('publicationId')), art_dict=self._metadata.get_art(episode), info_dict=info_labels, prop_dict=self._metadata.get_properties(episode), @@ -346,11 +346,11 @@ def get_upnext(self, info): if upnext.get('current'): if upnext.get('current').get('episodeNumber') == upnext.get('current').get('seasonNbOfEpisodes'): - self._kodi.log_error(message='[Up Next] Last episode of season, next season not implemented for "{program} S{season}E{episode}"', - program=program, season=season, episode=current_ep_no) + log_error(message='[Up Next] Last episode of season, next season not implemented for "{program} S{season}E{episode}"', + program=program, season=season, episode=current_ep_no) return None - self._kodi.log_error(message='[Up Next] No api data found for "{program}s S{season}E{episode}"', - program=program, season=season, episode=current_ep_no) + log_error(message='[Up Next] No api data found for "{program}s S{season}E{episode}"', + program=program, season=season, episode=current_ep_no) return None def get_single_episode(self, whatson_id): @@ -370,14 +370,14 @@ def get_single_episode(self, whatson_id): def get_episode_by_air_date(self, channel_name, start_date, end_date=None): ''' Get an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00) ''' - import json + channel = statichelper.find_entry(CHANNELS, 'name', channel_name) + if not channel: + return None + from datetime import datetime, timedelta import dateutil.parser import dateutil.tz offairdate = None - channel = statichelper.find_entry(CHANNELS, 'name', channel_name) - if not channel: - return None try: onairdate = dateutil.parser.parse(start_date, default=datetime.now(dateutil.tz.gettz('Europe/Brussels'))) except ValueError: @@ -397,7 +397,8 @@ def get_episode_by_air_date(self, channel_name, start_date, end_date=None): schedule_date = onairdate schedule_datestr = schedule_date.isoformat().split('T')[0] url = 'https://www.vrt.be/bin/epg/schedule.%s.json' % schedule_datestr - schedule_json = json.load(urlopen(url)) + from json import load + schedule_json = load(urlopen(url)) episodes = schedule_json.get(channel.get('id'), []) if offairdate: mindate = min(abs(offairdate - dateutil.parser.parse(episode.get('endTime'))) for episode in episodes) @@ -449,7 +450,7 @@ def get_latest_episode(self, program): api_data = self.get_episodes(program=program, variety='single') if len(api_data) == 1: episode = api_data[0] - self._kodi.log(2, str(episode)) + log(2, str(episode)) video_item = TitleItem( title=self._metadata.get_label(episode), art_dict=self._metadata.get_art(episode), @@ -510,7 +511,7 @@ def get_episodes(self, program=None, season=None, episodes=None, category=None, program_urls = [statichelper.program_to_url(p, 'medium') for p in self._favorites.programs()] params['facets[programUrl]'] = '[%s]' % (','.join(program_urls)) elif variety in ('offline', 'recent'): - channel_filter = [channel.get('name') for channel in CHANNELS if self._kodi.get_setting(channel.get('name'), 'true') == 'true'] + channel_filter = [channel.get('name') for channel in CHANNELS if get_setting(channel.get('name'), 'true') == 'true'] params['facets[programBrands]'] = '[%s]' % (','.join(channel_filter)) if program: @@ -550,32 +551,32 @@ def get_episodes(self, program=None, season=None, episodes=None, category=None, querystring = '&'.join('{}={}'.format(key, value) for key, value in list(params.items())) search_url = self._VRTNU_SEARCH_URL + '?' + querystring.replace(' ', '%20') # Only encode spaces to minimize url length - import json + from json import load if cache_file: # Get api data from cache if it is fresh - search_json = self._kodi.get_cache(cache_file, ttl=60 * 60) + search_json = get_cache(cache_file, ttl=60 * 60) if not search_json: - self._kodi.log(2, 'URL get: {url}', url=unquote(search_url)) + log(2, 'URL get: {url}', url=unquote(search_url)) req = Request(search_url) try: - search_json = json.load(urlopen(req)) + search_json = load(urlopen(req)) except HTTPError as exc: url_length = len(req.get_selector()) if exc.code == 413 and url_length > 8192: - self._kodi.show_ok_dialog(heading='HTTP Error 413', message=self._kodi.localize(30967)) - self._kodi.log_error('HTTP Error 413: Exceeded maximum url length: ' - 'VRT Search API url has a length of {length} characters.', length=url_length) + ok_dialog(heading='HTTP Error 413', message=localize(30967)) + log_error('HTTP Error 413: Exceeded maximum url length: ' + 'VRT Search API url has a length of {length} characters.', length=url_length) return [] if exc.code == 400 and 7600 <= url_length <= 8192: - self._kodi.show_ok_dialog(heading='HTTP Error 400', message=self._kodi.localize(30967)) - self._kodi.log_error('HTTP Error 400: Probably exceeded maximum url length: ' - 'VRT Search API url has a length of {length} characters.', length=url_length) + ok_dialog(heading='HTTP Error 400', message=localize(30967)) + log_error('HTTP Error 400: Probably exceeded maximum url length: ' + 'VRT Search API url has a length of {length} characters.', length=url_length) return [] raise - self._kodi.update_cache(cache_file, search_json) + update_cache(cache_file, search_json) else: - self._kodi.log(2, 'URL get: {url}', url=unquote(search_url)) - search_json = json.load(urlopen(search_url)) + log(2, 'URL get: {url}', url=unquote(search_url)) + search_json = load(urlopen(search_url)) # Check for multiple seasons seasons = None @@ -597,7 +598,7 @@ def get_episodes(self, program=None, season=None, episodes=None, category=None, if all_items and total_results > api_page_size: for api_page in range(1, api_pages): api_page_url = search_url + '&from=' + str(api_page * api_page_size + 1) - api_page_json = json.load(urlopen(api_page_url)) + api_page_json = load(urlopen(api_page_url)) episodes += api_page_json.get('results', [{}]) # Return episodes @@ -606,13 +607,13 @@ def get_episodes(self, program=None, season=None, episodes=None, category=None, def get_live_screenshot(self, channel): ''' Get a live screenshot for a given channel, only supports Eén, Canvas and Ketnet ''' url = '%s/%s.jpg' % (self._VRTNU_SCREENSHOT_URL, channel) - self._kodi.delete_cached_thumbnail(url) + delete_cached_thumbnail(url) return url def list_channels(self, channels=None, live=True): ''' Construct a list of channel ListItems, either for Live TV or the TV Guide listing ''' from tvguide import TVGuide - _tvguide = TVGuide(self._kodi) + _tvguide = TVGuide() channel_items = [] for channel in CHANNELS: @@ -623,13 +624,13 @@ def list_channels(self, channels=None, live=True): art_dict = dict() # Try to use the white icons for thumbnails (used for icons as well) - if self._kodi.get_cond_visibility('System.HasAddon(resource.images.studios.white)') == 1: + if has_addon('resource.images.studios.white'): art_dict['thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format(**channel) else: art_dict['thumb'] = 'DefaultTags.png' if not live: - path = self._kodi.url_for('channels', channel=channel.get('name')) + path = url_for('channels', channel=channel.get('name')) label = channel.get('label') plot = '[B]%s[/B]' % channel.get('label') is_playable = False @@ -637,10 +638,10 @@ def list_channels(self, channels=None, live=True): stream_dict = [] elif channel.get('live_stream') or channel.get('live_stream_id'): if channel.get('live_stream_id'): - path = self._kodi.url_for('play_id', video_id=channel.get('live_stream_id')) + path = url_for('play_id', video_id=channel.get('live_stream_id')) elif channel.get('live_stream'): - path = self._kodi.url_for('play_url', video_url=channel.get('live_stream')) - label = self._kodi.localize(30141, **channel) # Channel live + path = url_for('play_url', video_url=channel.get('live_stream')) + label = localize(30141, **channel) # Channel live playing_now = _tvguide.playing_now(channel.get('name')) if playing_now: label += ' [COLOR yellow]| %s[/COLOR]' % playing_now @@ -649,17 +650,17 @@ def list_channels(self, channels=None, live=True): label = '[B]%s[/B]' % label is_playable = True if channel.get('name') in ['een', 'canvas', 'ketnet']: - if self._kodi.get_setting('showfanart', 'true') == 'true': + if get_setting('showfanart', 'true') == 'true': art_dict['fanart'] = self.get_live_screenshot(channel.get('name', art_dict.get('fanart'))) - plot = '%s\n\n%s' % (self._kodi.localize(30142, **channel), _tvguide.live_description(channel.get('name'))) + plot = '%s\n\n%s' % (localize(30142, **channel), _tvguide.live_description(channel.get('name'))) else: - plot = self._kodi.localize(30142, **channel) # Watch live + plot = localize(30142, **channel) # Watch live # NOTE: Playcount is required to not have live streams as "Watched" info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0, duration=0) stream_dict = dict(duration=0) context_menu.append(( - self._kodi.localize(30413), - 'RunPlugin(%s)' % self._kodi.url_for('delete_cache', cache_file='channel.%s.json' % channel) + localize(30413), + 'RunPlugin(%s)' % url_for('delete_cache', cache_file='channel.%s.json' % channel) )) else: # Not a playable channel @@ -677,12 +678,13 @@ def list_channels(self, channels=None, live=True): return channel_items - def list_youtube(self, channels=None): + @staticmethod + def list_youtube(channels=None): ''' Construct a list of youtube ListItems, either for Live TV or the TV Guide listing ''' youtube_items = [] - if self._kodi.get_cond_visibility('System.HasAddon(plugin.video.youtube)') == 0 or self._kodi.get_setting('showyoutube', 'true') == 'false': + if not has_addon('plugin.video.youtube') or get_setting('showyoutube', 'true') == 'false': return youtube_items for channel in CHANNELS: @@ -693,23 +695,23 @@ def list_youtube(self, channels=None): art_dict = dict() # Try to use the white icons for thumbnails (used for icons as well) - if self._kodi.get_cond_visibility('System.HasAddon(resource.images.studios.white)') == 1: + if has_addon('resource.images.studios.white'): art_dict['thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format(**channel) else: art_dict['thumb'] = 'DefaultTags.png' if channel.get('youtube'): path = channel.get('youtube') - label = self._kodi.localize(30143, **channel) # Channel on YouTube + label = localize(30143, **channel) # Channel on YouTube # A single Live channel means it is the entry for channel's TV Show listing, so make it stand out if channels and len(channels) == 1: label = '[B]%s[/B]' % label - plot = self._kodi.localize(30144, **channel) # Watch on YouTube + plot = localize(30144, **channel) # Watch on YouTube # NOTE: Playcount is required to not have live streams as "Watched" info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0) context_menu.append(( - self._kodi.localize(30413), - 'RunPlugin(%s)' % self._kodi.url_for('delete_cache', cache_file='channel.%s.json' % channel) + localize(30413), + 'RunPlugin(%s)' % url_for('delete_cache', cache_file='channel.%s.json' % channel) )) else: # Not a playable channel @@ -735,13 +737,14 @@ def list_featured(self): featured_name = feature.get('name') featured_items.append(TitleItem( title=featured_name, - path=self._kodi.url_for('featured', feature=feature.get('id')), + path=url_for('featured', feature=feature.get('id')), art_dict=dict(thumb='DefaultCountry.png'), info_dict=dict(plot='[B]%s[/B]' % feature.get('name'), studio='VRT'), )) return featured_items - def localize_features(self, featured): + @staticmethod + def localize_features(featured): ''' Return a localized and sorted listing ''' from copy import deepcopy features = deepcopy(featured) @@ -749,7 +752,7 @@ def localize_features(self, featured): for feature in features: for key, val in list(feature.items()): if key == 'name': - feature[key] = self._kodi.localize_from_data(val, featured) + feature[key] = localize_from_data(val, featured) return sorted(features, key=lambda x: x.get('name')) @@ -758,7 +761,7 @@ def list_categories(self): categories = [] # Try the cache if it is fresh - categories = self._kodi.get_cache('categories.json', ttl=7 * 24 * 60 * 60) + categories = get_cache('categories.json', ttl=7 * 24 * 60 * 60) # Try to scrape from the web if not categories: @@ -767,11 +770,11 @@ def list_categories(self): except Exception: # pylint: disable=broad-except categories = [] else: - self._kodi.update_cache('categories.json', categories) + update_cache('categories.json', categories) # Use the cache anyway (better than hard-coded) if not categories: - categories = self._kodi.get_cache('categories.json', ttl=None) + categories = get_cache('categories.json', ttl=None) # Fall back to internal hard-coded categories if all else fails from data import CATEGORIES @@ -780,32 +783,33 @@ def list_categories(self): category_items = [] for category in self.localize_categories(categories, CATEGORIES): - if self._kodi.get_setting('showfanart', 'true') == 'true': + if get_setting('showfanart', 'true') == 'true': thumbnail = category.get('thumbnail', 'DefaultGenre.png') else: thumbnail = 'DefaultGenre.png' category_items.append(TitleItem( title=category.get('name'), - path=self._kodi.url_for('categories', category=category.get('id')), + path=url_for('categories', category=category.get('id')), art_dict=dict(thumb=thumbnail, icon='DefaultGenre.png'), info_dict=dict(plot='[B]%s[/B]' % category.get('name'), studio='VRT'), )) return category_items - def localize_categories(self, categories, categories2): + @staticmethod + def localize_categories(categories, categories2): ''' Return a localized and sorted listing ''' for category in categories: for key, val in list(category.items()): if key == 'name': - category[key] = self._kodi.localize_from_data(val, categories2) + category[key] = localize_from_data(val, categories2) return sorted(categories, key=lambda x: x.get('name')) def get_categories(self): ''' Return a list of categories by scraping the website ''' from bs4 import BeautifulSoup, SoupStrainer - self._kodi.log(2, 'URL get: https://www.vrt.be/vrtnu/categorieen/') + log(2, 'URL get: https://www.vrt.be/vrtnu/categorieen/') response = urlopen('https://www.vrt.be/vrtnu/categorieen/') tiles = SoupStrainer('nui-list--content') soup = BeautifulSoup(response.read(), 'html.parser', parse_only=tiles) @@ -820,9 +824,10 @@ def get_categories(self): return categories - def get_category_thumbnail(self, element): + @staticmethod + def get_category_thumbnail(element): ''' Return a category thumbnail, if available ''' - if self._kodi.get_setting('showfanart', 'true') == 'true': + if get_setting('showfanart', 'true') == 'true': raw_thumbnail = element.find(class_='media').get('data-responsive-image', 'DefaultGenre.png') return statichelper.add_https_method(raw_thumbnail) return 'DefaultGenre.png' diff --git a/resources/lib/favorites.py b/resources/lib/favorites.py index 97547ff9..ae25a35a 100644 --- a/resources/lib/favorites.py +++ b/resources/lib/favorites.py @@ -11,30 +11,32 @@ except ImportError: # Python 2 from urllib2 import build_opener, install_opener, ProxyHandler, Request, unquote, urlopen +from kodiutils import (container_refresh, get_cache, get_proxies, get_setting, has_credentials, + input_down, invalidate_caches, localize, log, log_error, multiselect, + notification, ok_dialog, update_cache) + class Favorites: ''' Track, cache and manage VRT favorites ''' - def __init__(self, _kodi): + def __init__(self): ''' Initialize favorites, relies on XBMC vfs and a special VRT token ''' - self._kodi = _kodi - self._proxies = _kodi.get_proxies() - install_opener(build_opener(ProxyHandler(self._proxies))) - # This is our internal representation - self._favorites = dict() + self._favorites = dict() # Our internal representation + install_opener(build_opener(ProxyHandler(get_proxies()))) - def is_activated(self): + @staticmethod + def is_activated(): ''' Is favorites activated in the menu and do we have credentials ? ''' - return self._kodi.get_setting('usefavorites') == 'true' and self._kodi.credentials_filled_in() + return get_setting('usefavorites') == 'true' and has_credentials() def refresh(self, ttl=None): ''' Get a cached copy or a newer favorites from VRT, or fall back to a cached file ''' if not self.is_activated(): return - favorites_json = self._kodi.get_cache('favorites.json', ttl) + favorites_json = get_cache('favorites.json', ttl) if not favorites_json: from tokenresolver import TokenResolver - xvrttoken = TokenResolver(self._kodi).get_xvrttoken(token_variant='user') + xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') if xvrttoken: headers = { 'authorization': 'Bearer ' + xvrttoken, @@ -42,15 +44,15 @@ def refresh(self, ttl=None): 'Referer': 'https://www.vrt.be/vrtnu', } req = Request('https://video-user-data.vrt.be/favorites', headers=headers) - self._kodi.log(2, 'URL post: https://video-user-data.vrt.be/favorites') - import json + log(2, 'URL post: https://video-user-data.vrt.be/favorites') + from json import load try: - favorites_json = json.load(urlopen(req)) + favorites_json = load(urlopen(req)) except (TypeError, ValueError): # No JSON object could be decoded # Force favorites from cache - favorites_json = self._kodi.get_cache('favorites.json', ttl=None) + favorites_json = get_cache('favorites.json', ttl=None) else: - self._kodi.update_cache('favorites.json', favorites_json) + update_cache('favorites.json', favorites_json) if favorites_json: self._favorites = favorites_json @@ -63,10 +65,10 @@ def update(self, program, title, value=True): return True from tokenresolver import TokenResolver - xvrttoken = TokenResolver(self._kodi).get_xvrttoken(token_variant='user') + xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') if xvrttoken is None: - self._kodi.log_error('Failed to get favorites token from VRT NU') - self._kodi.show_notification(message=self._kodi.localize(30975)) + log_error('Failed to get favorites token from VRT NU') + notification(message=localize(30975)) return False headers = { @@ -77,20 +79,20 @@ def update(self, program, title, value=True): from statichelper import program_to_url payload = dict(isFavorite=value, programUrl=program_to_url(program, 'short'), title=title) - import json - data = json.dumps(payload).encode('utf-8') + from json import dumps + data = dumps(payload).encode('utf-8') program_uuid = self.program_to_uuid(program) - self._kodi.log(2, 'URL post: https://video-user-data.vrt.be/favorites/{uuid}', uuid=program_uuid) + log(2, 'URL post: https://video-user-data.vrt.be/favorites/{uuid}', uuid=program_uuid) req = Request('https://video-user-data.vrt.be/favorites/%s' % program_uuid, data=data, headers=headers) result = urlopen(req) if result.getcode() != 200: - self._kodi.log_error("Failed to (un)follow program '{program}' at VRT NU", program=program) - self._kodi.show_notification(message=self._kodi.localize(30976, program=program)) + log_error("Failed to (un)follow program '{program}' at VRT NU", program=program) + notification(message=localize(30976, program=program)) return False # NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it self._favorites[program_uuid] = dict(value=payload) - self._kodi.update_cache('favorites.json', self._favorites) - # self.invalidate_caches() + update_cache('favorites.json', self._favorites) + invalidate_caches('my-offline-*.json', 'my-recent-*.json') return True def is_favorite(self, program): @@ -105,18 +107,18 @@ def follow(self, program, title): ''' Follow your favorite program ''' succeeded = self.update(program, title, True) if succeeded: - self._kodi.show_notification(message=self._kodi.localize(30411, title=title)) - self._kodi.container_refresh() + notification(message=localize(30411, title=title)) + container_refresh() def unfollow(self, program, title, move_down=False): ''' Unfollow your favorite program ''' succeeded = self.update(program, title, False) if succeeded: - self._kodi.show_notification(message=self._kodi.localize(30412, title=title)) + notification(message=localize(30412, title=title)) # If the current item is selected and we need to move down before removing if move_down: - self._kodi.input_down() - self._kodi.container_refresh() + input_down() + container_refresh() @staticmethod def program_to_uuid(program): @@ -132,16 +134,12 @@ def programs(self): from statichelper import url_to_program return [url_to_program(value.get('value').get('programUrl')) for value in list(self._favorites.values()) if value.get('value').get('isFavorite')] - def invalidate_caches(self): - ''' Invalidate caches that rely on favorites ''' - self._kodi.invalidate_caches('favorites.json', 'my-offline-*.json', 'my-recent-*.json') - def manage(self): ''' Allow the user to unselect favorites to be removed from the listing ''' from statichelper import url_to_program self.refresh(ttl=0) if not self._favorites: - self._kodi.show_ok_dialog(heading=self._kodi.localize(30418), message=self._kodi.localize(30419)) # No favorites found + ok_dialog(heading=localize(30418), message=localize(30419)) # No favorites found return def by_title(item): @@ -153,7 +151,7 @@ def by_title(item): enabled=value.get('value').get('isFavorite')) for value in list(sorted(list(self._favorites.values()), key=by_title))] titles = [item['title'] for item in items] preselect = [idx for idx in range(0, len(items) - 1) if items[idx]['enabled']] - selected = self._kodi.show_multiselect(self._kodi.localize(30420), options=titles, preselect=preselect) # Please select/unselect to follow/unfollow + selected = multiselect(localize(30420), options=titles, preselect=preselect) # Please select/unselect to follow/unfollow if selected is not None: for idx in set(preselect).difference(set(selected)): self.unfollow(program=items[idx]['program'], title=items[idx]['title']) diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py new file mode 100644 index 00000000..18db9549 --- /dev/null +++ b/resources/lib/kodiutils.py @@ -0,0 +1,793 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +''' All functionality that requires Kodi imports ''' + +# pylint: disable=too-many-function-args + +from __future__ import absolute_import, division, unicode_literals +from contextlib import contextmanager +import xbmc +import xbmcplugin +from statichelper import from_unicode, to_unicode + +SORT_METHODS = dict( + # date=xbmcplugin.SORT_METHOD_DATE, + dateadded=xbmcplugin.SORT_METHOD_DATEADDED, + duration=xbmcplugin.SORT_METHOD_DURATION, + episode=xbmcplugin.SORT_METHOD_EPISODE, + # genre=xbmcplugin.SORT_METHOD_GENRE, + # label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE, + label=xbmcplugin.SORT_METHOD_LABEL, + # none=xbmcplugin.SORT_METHOD_UNSORTED, + # FIXME: We would like to be able to sort by unprefixed title (ignore date/episode prefix) + # title=xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE, + unsorted=xbmcplugin.SORT_METHOD_UNSORTED, +) + +WEEKDAY_LONG = { + '0': xbmc.getLocalizedString(17), + '1': xbmc.getLocalizedString(11), + '2': xbmc.getLocalizedString(12), + '3': xbmc.getLocalizedString(13), + '4': xbmc.getLocalizedString(14), + '5': xbmc.getLocalizedString(15), + '6': xbmc.getLocalizedString(16), +} + +MONTH_LONG = { + '01': xbmc.getLocalizedString(21), + '02': xbmc.getLocalizedString(22), + '03': xbmc.getLocalizedString(23), + '04': xbmc.getLocalizedString(24), + '05': xbmc.getLocalizedString(25), + '06': xbmc.getLocalizedString(26), + '07': xbmc.getLocalizedString(27), + '08': xbmc.getLocalizedString(28), + '09': xbmc.getLocalizedString(29), + '10': xbmc.getLocalizedString(30), + '11': xbmc.getLocalizedString(31), + '12': xbmc.getLocalizedString(32), +} + +WEEKDAY_SHORT = { + '0': xbmc.getLocalizedString(47), + '1': xbmc.getLocalizedString(41), + '2': xbmc.getLocalizedString(42), + '3': xbmc.getLocalizedString(43), + '4': xbmc.getLocalizedString(44), + '5': xbmc.getLocalizedString(45), + '6': xbmc.getLocalizedString(46), +} + +MONTH_SHORT = { + '01': xbmc.getLocalizedString(51), + '02': xbmc.getLocalizedString(52), + '03': xbmc.getLocalizedString(53), + '04': xbmc.getLocalizedString(54), + '05': xbmc.getLocalizedString(55), + '06': xbmc.getLocalizedString(56), + '07': xbmc.getLocalizedString(57), + '08': xbmc.getLocalizedString(58), + '09': xbmc.getLocalizedString(59), + '10': xbmc.getLocalizedString(60), + '11': xbmc.getLocalizedString(61), + '12': xbmc.getLocalizedString(62), +} + + +def has_socks(): + ''' Test if socks is installed, and remember this information ''' + if hasattr(has_socks, 'cached'): + return has_socks.cached + try: + import socks # noqa: F401; pylint: disable=unused-variable,unused-import + except ImportError: + has_socks.cached = False + return None # Detect if this is the first run + has_socks.cached = True + return True + + +class SafeDict(dict): + ''' A safe dictionary implementation that does not break down on missing keys ''' + def __missing__(self, key): + ''' Replace missing keys with the original placeholder ''' + return '{' + key + '}' + + +def url_for(name, *args, **kwargs): + ''' Wrapper for routing.url_for() to lookup by name ''' + import addon + return addon.plugin.url_for(getattr(addon, name), *args, **kwargs) + + +def show_listing(list_items, category=None, sort='unsorted', ascending=True, content=None, cache=None, selected=None): + ''' Show a virtual directory in Kodi ''' + from addon import plugin + from xbmcgui import ListItem + + xbmcplugin.setPluginFanart(handle=plugin.handle, image=from_unicode(addon_fanart())) + + usemenucaching = get_setting('usemenucaching', 'true') == 'true' + if cache is None: + cache = usemenucaching + elif usemenucaching is False: + cache = False + + if content: + # content is one of: files, songs, artists, albums, movies, tvshows, episodes, musicvideos + xbmcplugin.setContent(plugin.handle, content=content) + + # Jump through hoops to get a stable breadcrumbs implementation + category_label = '' + if category: + if not content: + category_label = 'VRT NU / ' + if plugin.path.startswith(('/favorites/', '/resumepoints/')): + category_label += localize(30428) + ' / ' # My + if isinstance(category, int): + category_label += localize(category) + else: + category_label += category + elif not content: + category_label = 'VRT NU' + xbmcplugin.setPluginCategory(handle=plugin.handle, category=category_label) + + # FIXME: Since there is no way to influence descending order, we force it here + if not ascending: + sort = 'unsorted' + + # NOTE: When showing tvshow listings and 'showoneoff' was set, force 'unsorted' + if get_setting('showoneoff', 'true') == 'true' and sort == 'label' and content == 'tvshows': + sort = 'unsorted' + + # Add all sort methods to GUI (start with preferred) + xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[sort]) + for key in sorted(SORT_METHODS): + if key != sort: + xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[key]) + + # FIXME: This does not appear to be working, we have to order it ourselves +# xbmcplugin.setProperty(handle=plugin.handle, key='sort.ascending', value='true' if ascending else 'false') +# if ascending: +# xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS[sort])) +# else: +# # NOTE: When descending, use unsorted +# xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS['unsorted'])) + + listing = [] + for title_item in list_items: + # Three options: + # - item is a virtual directory/folder (not playable, path) + # - item is a playable file (playable, path) + # - item is non-actionable item (not playable, no path) + is_folder = bool(not title_item.is_playable and title_item.path) + is_playable = bool(title_item.is_playable and title_item.path) + + list_item = ListItem(label=title_item.title) + + if title_item.prop_dict: + # FIXME: The setProperties method is new in Kodi18, so we cannot use it just yet. + # list_item.setProperties(values=title_item.prop_dict) + for key, value in list(title_item.prop_dict.items()): + list_item.setProperty(key=key, value=str(value)) + list_item.setProperty(key='IsInternetStream', value='true' if is_playable else 'false') + list_item.setProperty(key='IsPlayable', value='true' if is_playable else 'false') + + # FIXME: The setIsFolder method is new in Kodi18, so we cannot use it just yet. + # list_item.setIsFolder(is_folder) + + if title_item.art_dict: + list_item.setArt(dict(fanart=addon_fanart())) + list_item.setArt(title_item.art_dict) + + if title_item.info_dict: + # type is one of: video, music, pictures, game + list_item.setInfo(type='video', infoLabels=title_item.info_dict) + + if title_item.stream_dict: + # type is one of: video, audio, subtitle + list_item.addStreamInfo('video', title_item.stream_dict) + + if title_item.context_menu: + list_item.addContextMenuItems(title_item.context_menu) + + url = None + if title_item.path: + url = title_item.path + + listing.append((url, list_item, is_folder)) + + # Jump to specific item + if selected is not None: + pass +# from xbmcgui import getCurrentWindowId, Window +# wnd = Window(getCurrentWindowId()) +# wnd.getControl(wnd.getFocusId()).selectItem(selected) + + succeeded = xbmcplugin.addDirectoryItems(plugin.handle, listing, len(listing)) + xbmcplugin.endOfDirectory(plugin.handle, succeeded, updateListing=False, cacheToDisc=cache) + + +def play(stream, video=None): + ''' Create a virtual directory listing to play its only item ''' + try: # Python 3 + from urllib.parse import unquote + except ImportError: # Python 2 + from urllib2 import unquote + + from addon import plugin + from xbmcgui import ListItem + play_item = ListItem(path=stream.stream_url) + if video and hasattr(video, 'info_dict'): + play_item.setProperty('subtitle', video.title) + play_item.setArt(video.art_dict) + play_item.setInfo( + type='video', + infoLabels=video.info_dict + ) + play_item.setProperty('inputstream.adaptive.max_bandwidth', str(get_max_bandwidth() * 1000)) + play_item.setProperty('network.bandwidth', str(get_max_bandwidth() * 1000)) + if stream.stream_url is not None and stream.use_inputstream_adaptive: + play_item.setProperty('inputstreamaddon', 'inputstream.adaptive') + play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd') + play_item.setMimeType('application/dash+xml') + play_item.setContentLookup(False) + if stream.license_key is not None: + import inputstreamhelper + is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha') + if is_helper.check_inputstream(): + play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') + play_item.setProperty('inputstream.adaptive.license_key', stream.license_key) + + subtitles_visible = get_setting('showsubtitles', 'true') == 'true' + # Separate subtitle url for hls-streams + if subtitles_visible and stream.subtitle_url is not None: + log(2, 'Subtitle URL: {url}', url=unquote(stream.subtitle_url)) + play_item.setSubtitles([stream.subtitle_url]) + + log(1, 'Play: {url}', url=unquote(stream.stream_url)) + xbmcplugin.setResolvedUrl(plugin.handle, bool(stream.stream_url), listitem=play_item) + + while not xbmc.Player().isPlaying() and not xbmc.Monitor().abortRequested(): + xbmc.sleep(100) + xbmc.Player().showSubtitles(subtitles_visible) + + +def addon_id(): + ''' Cache and return VRT NU Add-on ID ''' + if not hasattr(addon_id, 'cached'): + from xbmcaddon import Addon + addon_id.cached = to_unicode(Addon().getAddonInfo('id')) + return addon_id.cached + + +def addon_fanart(): + ''' Cache and return VRT NU Add-on fanart ''' + if not hasattr(addon_fanart, 'cached'): + from xbmcaddon import Addon + addon_fanart.cached = to_unicode(Addon().getAddonInfo('fanart')) + return addon_fanart.cached + + +def addon_name(): + ''' Cache and return VRT NU Add-on name ''' + if not hasattr(addon_name, 'cached'): + from xbmcaddon import Addon + addon_name.cached = to_unicode(Addon().getAddonInfo('name')) + return addon_name.cached + + +def get_search_string(): + ''' Ask the user for a search string ''' + search_string = None + keyboard = xbmc.Keyboard('', localize(30134)) + keyboard.doModal() + if keyboard.isConfirmed(): + search_string = to_unicode(keyboard.getText()) + return search_string + + +def ok_dialog(heading='', message=''): + ''' Show Kodi's OK dialog ''' + from xbmcgui import Dialog + if not heading: + heading = addon_name() + return Dialog().ok(heading=heading, line1=message) + + +def notification(heading='', message='', icon='info', time=4000): + ''' Show a Kodi notification ''' + from xbmcgui import Dialog + if not heading: + heading = addon_name() + Dialog().notification(heading=heading, message=message, icon=icon, time=time) + + +def multiselect(heading='', options=None, autoclose=0, preselect=None, use_details=False): + ''' Show a Kodi multi-select dialog ''' + from xbmcgui import Dialog + if not heading: + heading = addon_name() + return Dialog().multiselect(heading=heading, options=options, autoclose=autoclose, preselect=preselect, useDetails=use_details) + + +def set_locale(): + ''' Load the proper locale for date strings, only once ''' + if hasattr(set_locale, 'cached'): + return set_locale.cached + from locale import LC_ALL, setlocale + locale_lang = get_global_setting('locale.language').split('.')[-1] + try: + # NOTE: This only works if the platform supports the Kodi configured locale + setlocale(LC_ALL, locale_lang) + except Exception as exc: # pylint: disable=broad-except + if locale_lang != 'en_gb': + log(3, "Your system does not support locale '{locale}': {error}", locale=locale_lang, error=exc) + set_locale.cached = False + return False + set_locale.cached = True + return True + + +def localize(string_id, **kwargs): + ''' Return the translated string from the .po language files, optionally translating variables ''' + from xbmcaddon import Addon + if kwargs: + from string import Formatter + return Formatter().vformat(Addon().getLocalizedString(string_id), (), SafeDict(**kwargs)) + + return Addon().getLocalizedString(string_id) + + +def localize_date(date, strftime): + ''' Return a localized date, even if the system does not support your locale ''' + has_locale = set_locale() + # When locale is supported, return original format + if has_locale: + return date.strftime(strftime) + # When locale is unsupported, translate weekday and month + if '%A' in strftime: + strftime = strftime.replace('%A', WEEKDAY_LONG[date.strftime('%w')]) + elif '%a' in strftime: + strftime = strftime.replace('%a', WEEKDAY_SHORT[date.strftime('%w')]) + if '%B' in strftime: + strftime = strftime.replace('%B', MONTH_LONG[date.strftime('%m')]) + elif '%b' in strftime: + strftime = strftime.replace('%b', MONTH_SHORT[date.strftime('%m')]) + return date.strftime(strftime) + + +def localize_datelong(date): + ''' Return a localized long date string ''' + return localize_date(date, xbmc.getRegion('datelong')) + + +def localize_from_data(name, data): + ''' Return a localized name string from a Dutch data object ''' + # Return if Kodi language is Dutch + if get_global_setting('locale.language') == 'resource.language.nl_nl': + return name + return next((localize(item.get('msgctxt')) for item in data if item.get('name') == name), name) + + +def get_setting(setting_id, default=None): + ''' Get an add-on setting ''' + from xbmcaddon import Addon + value = to_unicode(Addon().getSetting(setting_id)) + if value == '' and default is not None: + return default + return value + + +def set_setting(setting_id, setting_value): + ''' Set an add-on setting ''' + from xbmcaddon import Addon + return Addon().setSetting(setting_id, setting_value) + + +def open_settings(): + ''' Open the add-in settings window, shows Credentials ''' + from xbmcaddon import Addon + Addon().openSettings() + + +def get_global_setting(setting): + ''' Get a Kodi setting ''' + result = jsonrpc(method='Settings.GetSettingValue', params=dict(setting=setting)) + return result.get('result', {}).get('value') + + +def notify(sender, message, data): + ''' Send a notification to Kodi using JSON RPC ''' + result = jsonrpc(method='JSONRPC.NotifyAll', params=dict( + sender=sender, + message=message, + data=data, + )) + if result.get('result') != 'OK': + log_error('Failed to send notification: {error}', error=result.get('error').get('message')) + return False + log(2, 'Succesfully sent notification') + return True + + +def get_playerid(): + ''' Get current playerid ''' + result = dict() + while not result.get('result'): + result = jsonrpc(method='Player.GetActivePlayers') + return result.get('result', [{}])[0].get('playerid') + + +def get_max_bandwidth(): + ''' Get the max bandwidth based on Kodi and VRT NU add-on settings ''' + vrtnu_max_bandwidth = int(get_setting('max_bandwidth', '0')) + global_max_bandwidth = int(get_global_setting('network.bandwidth')) + if vrtnu_max_bandwidth != 0 and global_max_bandwidth != 0: + return min(vrtnu_max_bandwidth, global_max_bandwidth) + if vrtnu_max_bandwidth != 0: + return vrtnu_max_bandwidth + if global_max_bandwidth != 0: + return global_max_bandwidth + return 0 + + +def get_proxies(): + ''' Return a usable proxies dictionary from Kodi proxy settings ''' + usehttpproxy = get_global_setting('network.usehttpproxy') + if usehttpproxy is not True: + return None + + try: + httpproxytype = int(get_global_setting('network.httpproxytype')) + except ValueError: + httpproxytype = 0 + + socks_supported = has_socks() + if httpproxytype != 0 and not socks_supported: + # Only open the dialog the first time (to avoid multiple popups) + if socks_supported is None: + ok_dialog('', localize(30966)) # Requires PySocks + return None + + proxy_types = ['http', 'socks4', 'socks4a', 'socks5', 'socks5h'] + if 0 <= httpproxytype < 5: + httpproxyscheme = proxy_types[httpproxytype] + else: + httpproxyscheme = 'http' + + httpproxyserver = get_global_setting('network.httpproxyserver') + httpproxyport = get_global_setting('network.httpproxyport') + httpproxyusername = get_global_setting('network.httpproxyusername') + httpproxypassword = get_global_setting('network.httpproxypassword') + + if httpproxyserver and httpproxyport and httpproxyusername and httpproxypassword: + proxy_address = '%s://%s:%s@%s:%s' % (httpproxyscheme, httpproxyusername, httpproxypassword, httpproxyserver, httpproxyport) + elif httpproxyserver and httpproxyport and httpproxyusername: + proxy_address = '%s://%s@%s:%s' % (httpproxyscheme, httpproxyusername, httpproxyserver, httpproxyport) + elif httpproxyserver and httpproxyport: + proxy_address = '%s://%s:%s' % (httpproxyscheme, httpproxyserver, httpproxyport) + elif httpproxyserver: + proxy_address = '%s://%s' % (httpproxyscheme, httpproxyserver) + else: + return None + + return dict(http=proxy_address, https=proxy_address) + + +def get_cond_visibility(condition): + ''' Test a condition in XBMC ''' + return xbmc.getCondVisibility(condition) + + +def has_inputstream_adaptive(): + ''' Whether InputStream Adaptive is installed and enabled in add-on settings ''' + return get_setting('useinputstreamadaptive', 'true') == 'true' and has_addon('inputstream.adaptive') + + +def has_addon(addon): + ''' Checks if add-on is installed ''' + return xbmc.getCondVisibility('System.HasAddon(%s)' % addon) == 1 + + +def has_credentials(): + ''' Whether the add-on has credentials filled in ''' + return bool(get_setting('username') and get_setting('password')) + + +def kodi_version(): + ''' Returns major Kodi version ''' + return int(xbmc.getInfoLabel('System.BuildVersion').split('.')[0]) + + +def can_play_drm(): + ''' Whether this Kodi can do DRM using InputStream Adaptive ''' + return get_setting('usedrm', 'true') == 'true' and get_setting('useinputstreamadaptive', 'true') == 'true' and supports_drm() + + +def supports_drm(): + ''' Whether this Kodi version supports DRM decryption using InputStream Adaptive ''' + return kodi_version() > 17 + + +def get_userdata_path(): + ''' Cache and return the profile's userdata path ''' + if not hasattr(get_userdata_path, 'cached'): + from xbmcaddon import Addon + get_userdata_path.cached = to_unicode(xbmc.translatePath(Addon().getAddonInfo('profile'))) + return get_userdata_path.cached + + +def get_tokens_path(): + ''' Cache and return the userdata tokens path ''' + if not hasattr(get_tokens_path, 'cached'): + get_tokens_path.cached = get_userdata_path() + 'tokens/' + return get_tokens_path.cached + + +def get_cache_path(): + ''' Cache and return the userdata cache path ''' + if not hasattr(get_cache_path, 'cached'): + get_cache_path.cached = get_userdata_path() + 'cache/' + return get_cache_path.cached + + +def get_addon_info(key): + ''' Return addon information ''' + from xbmcaddon import Addon + return Addon().getAddonInfo(key) + + +def listdir(path): + ''' Return all files in a directory (using xbmcvfs)''' + from xbmcvfs import listdir as vfslistdir + return vfslistdir(path) + + +def mkdir(path): + ''' Create a directory (using xbmcvfs) ''' + from xbmcvfs import mkdir as vfsmkdir + log(3, "Create directory '{path}'.", path=path) + return vfsmkdir(path) + + +def mkdirs(path): + ''' Create directory including parents (using xbmcvfs) ''' + from xbmcvfs import mkdirs as vfsmkdirs + log(3, "Recursively create directory '{path}'.", path=path) + return vfsmkdirs(path) + + +def exists(path): + ''' Whether the path exists (using xbmcvfs)''' + from xbmcvfs import exists as vfsexists + return vfsexists(path) + + +@contextmanager +def open_file(path, flags='r'): + ''' Open a file (using xbmcvfs) ''' + from xbmcvfs import File + fdesc = File(path, flags) + yield fdesc + fdesc.close() + + +def stat_file(path): + ''' Return information about a file (using xbmcvfs) ''' + from xbmcvfs import Stat + return Stat(path) + + +def delete(path): + ''' Remove a file (using xbmcvfs) ''' + from xbmcvfs import delete as vfsdelete + log(3, "Delete file '{path}'.", path=path) + return vfsdelete(path) + + +def delete_cached_thumbnail(url): + ''' Remove a cached thumbnail from Kodi in an attempt to get a realtime live screenshot ''' + # Get texture + result = jsonrpc(method='Textures.GetTextures', params=dict( + filter=dict( + field='url', + operator='is', + value=url, + ), + )) + if result.get('result', {}).get('textures') is None: + log_error('URL {url} not found in texture cache', url=url) + return False + + texture_id = next((texture.get('textureid') for texture in result.get('result').get('textures')), None) + if not texture_id: + log_error('URL {url} not found in texture cache', url=url) + return False + log(2, 'found texture_id {id} for url {url} in texture cache', id=texture_id, url=url) + + # Remove texture + result = jsonrpc(method='Textures.RemoveTexture', params=dict(textureid=texture_id)) + if result.get('result') != 'OK': + log_error('failed to remove {url} from texture cache: {error}', url=url, error=result.get('error', {}).get('message')) + return False + + log(2, 'succesfully removed {url} from texture cache', url=url) + return True + + +def human_delta(seconds): + ''' Return a human-readable representation of the TTL ''' + from math import floor + days = int(floor(seconds / (24 * 60 * 60))) + seconds = seconds % (24 * 60 * 60) + hours = int(floor(seconds / (60 * 60))) + seconds = seconds % (60 * 60) + if days: + return '%d day%s and %d hour%s' % (days, 's' if days != 1 else '', hours, 's' if hours != 1 else '') + minutes = int(floor(seconds / 60)) + seconds = seconds % 60 + if hours: + return '%d hour%s and %d minute%s' % (hours, 's' if hours != 1 else '', minutes, 's' if minutes != 1 else '') + if minutes: + return '%d minute%s and %d second%s' % (minutes, 's' if minutes != 1 else '', seconds, 's' if seconds != 1 else '') + return '%d second%s' % (seconds, 's' if seconds != 1 else '') + + +def get_cache(path, ttl=None): + ''' Get the content from cache, if it's still fresh ''' + if get_setting('usehttpcaching', 'true') == 'false': + return None + + fullpath = get_cache_path() + path + if not exists(fullpath): + return None + + from time import localtime, mktime + mtime = stat_file(fullpath).st_mtime() + now = mktime(localtime()) + if ttl is None or now - mtime < ttl: + from json import load + if ttl is None: + log(3, "Cache '{path}' is forced from cache.", path=path) + else: + log(3, "Cache '{path}' is fresh, expires in {time}.", path=path, time=human_delta(mtime + ttl - now)) + with open_file(fullpath, 'r') as fdesc: + try: + # return load(fdesc, encoding='utf-8') + return load(fdesc) + except (TypeError, ValueError): + return None + + return None + + +def update_cache(path, data): + ''' Update the cache, if necessary ''' + if get_setting('usehttpcaching', 'true') == 'false': + return + + from hashlib import md5 + from json import dump, dumps + fullpath = get_cache_path() + path + if exists(fullpath): + with open_file(fullpath) as fdesc: + cachefile = fdesc.read().encode('utf-8') + md5_cache = md5(cachefile) + else: + md5_cache = 0 + # Create cache directory if missing + if not exists(get_cache_path()): + mkdirs(get_cache_path()) + + # Avoid writes if possible (i.e. SD cards) + if md5_cache != md5(dumps(data).encode('utf-8')): + log(3, "Write cache '{path}'.", path=path) + with open_file(fullpath, 'w') as fdesc: + # dump(data, fdesc, encoding='utf-8') + dump(data, fdesc) + else: + # Update timestamp + from os import utime + log(3, "Cache '{path}' has not changed, updating mtime only.", path=path) + utime(path) + + +def refresh_caches(cache_file=None): + ''' Invalidate the needed caches and refresh container ''' + files = ['favorites.json', 'oneoff.json', 'resume_points.json'] + if cache_file and cache_file not in files: + files.append(cache_file) + invalidate_caches(*files) + container_refresh() + notification(message=localize(30981)) + + +def invalidate_caches(*caches): + ''' Invalidate multiple cache files ''' + import fnmatch + _, files = listdir(get_cache_path()) + # Invalidate caches related to menu list refreshes + removes = set() + for expr in caches: + removes.update(fnmatch.filter(files, expr)) + for filename in removes: + delete(get_cache_path() + filename) + + +def input_down(): + ''' Move the cursor down ''' + jsonrpc(method='Input.Down') + + +def current_container_url(): + ''' Get current container plugin:// url ''' + url = xbmc.getInfoLabel('Container.FolderPath') + if url == '': + url = None + return url + + +def container_refresh(url=None): + ''' Refresh the current container or (re)load a container by URL ''' + if url: + log(3, 'Execute: Container.Refresh({url})', url=url) + xbmc.executebuiltin('Container.Refresh({url})'.format(url=url)) + else: + log(3, 'Execute: Container.Refresh') + xbmc.executebuiltin('Container.Refresh') + + +def container_update(url=None): + ''' Update the current container with respect for the path history. ''' + if url: + log(3, 'Execute: Container.Update({url})', url=url) + xbmc.executebuiltin('Container.Update({url})'.format(url=url)) + else: + # URL is a mandatory argument for Container.Update, use Container.Refresh instead + container_refresh() + + +def end_of_directory(): + ''' Close a virtual directory, required to avoid a waiting Kodi ''' + from addon import plugin + xbmcplugin.endOfDirectory(handle=plugin.handle, succeeded=False, updateListing=False, cacheToDisc=False) + + +def log(level=1, message='', **kwargs): + ''' Log info messages to Kodi ''' + debug_logging = get_global_setting('debug.showloginfo') # Returns a boolean + max_log_level = int(get_setting('max_log_level', 0)) + if not debug_logging and not (level <= max_log_level and max_log_level != 0): + return + if kwargs: + from string import Formatter + message = Formatter().vformat(message, (), SafeDict(**kwargs)) + message = '[{addon}] {message}'.format(addon=addon_id(), message=message) + xbmc.log(from_unicode(message), level % 3 if debug_logging else 2) + + +def log_access(url, query_string=None): + ''' Log addon access ''' + message = 'Access: %s' % (url + ('?' + query_string if query_string else '')) + log(1, message) + + +def log_error(message, **kwargs): + ''' Log error messages to Kodi ''' + if kwargs: + from string import Formatter + message = Formatter().vformat(message, (), SafeDict(**kwargs)) + message = '[{addon}] {message}'.format(addon=addon_id(), message=message) + xbmc.log(from_unicode(message), 4) + + +def jsonrpc(**kwargs): + ''' Perform JSONRPC calls ''' + from json import dumps, loads + if 'id' not in kwargs: + kwargs.update(id=1) + if 'jsonrpc' not in kwargs: + kwargs.update(jsonrpc='2.0') + return loads(xbmc.executeJSONRPC(dumps(kwargs))) diff --git a/resources/lib/kodiwrapper.py b/resources/lib/kodiwrapper.py deleted file mode 100644 index efb9270a..00000000 --- a/resources/lib/kodiwrapper.py +++ /dev/null @@ -1,732 +0,0 @@ -# -*- coding: utf-8 -*- -# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -''' All functionality that requires Kodi imports ''' - -# pylint: disable=too-many-function-args - -from __future__ import absolute_import, division, unicode_literals -from contextlib import contextmanager -import xbmc -import xbmcplugin -from xbmcaddon import Addon -from statichelper import from_unicode, to_unicode - -try: # Python 3 - from urllib.parse import unquote -except ImportError: # Python 2 - from urllib2 import unquote - -SORT_METHODS = dict( - # date=xbmcplugin.SORT_METHOD_DATE, - dateadded=xbmcplugin.SORT_METHOD_DATEADDED, - duration=xbmcplugin.SORT_METHOD_DURATION, - episode=xbmcplugin.SORT_METHOD_EPISODE, - # genre=xbmcplugin.SORT_METHOD_GENRE, - # label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE, - label=xbmcplugin.SORT_METHOD_LABEL, - # none=xbmcplugin.SORT_METHOD_UNSORTED, - # FIXME: We would like to be able to sort by unprefixed title (ignore date/episode prefix) - # title=xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE, - unsorted=xbmcplugin.SORT_METHOD_UNSORTED, -) - -WEEKDAY_LONG = { - '0': xbmc.getLocalizedString(17), - '1': xbmc.getLocalizedString(11), - '2': xbmc.getLocalizedString(12), - '3': xbmc.getLocalizedString(13), - '4': xbmc.getLocalizedString(14), - '5': xbmc.getLocalizedString(15), - '6': xbmc.getLocalizedString(16), -} - -MONTH_LONG = { - '01': xbmc.getLocalizedString(21), - '02': xbmc.getLocalizedString(22), - '03': xbmc.getLocalizedString(23), - '04': xbmc.getLocalizedString(24), - '05': xbmc.getLocalizedString(25), - '06': xbmc.getLocalizedString(26), - '07': xbmc.getLocalizedString(27), - '08': xbmc.getLocalizedString(28), - '09': xbmc.getLocalizedString(29), - '10': xbmc.getLocalizedString(30), - '11': xbmc.getLocalizedString(31), - '12': xbmc.getLocalizedString(32), -} - -WEEKDAY_SHORT = { - '0': xbmc.getLocalizedString(47), - '1': xbmc.getLocalizedString(41), - '2': xbmc.getLocalizedString(42), - '3': xbmc.getLocalizedString(43), - '4': xbmc.getLocalizedString(44), - '5': xbmc.getLocalizedString(45), - '6': xbmc.getLocalizedString(46), -} - -MONTH_SHORT = { - '01': xbmc.getLocalizedString(51), - '02': xbmc.getLocalizedString(52), - '03': xbmc.getLocalizedString(53), - '04': xbmc.getLocalizedString(54), - '05': xbmc.getLocalizedString(55), - '06': xbmc.getLocalizedString(56), - '07': xbmc.getLocalizedString(57), - '08': xbmc.getLocalizedString(58), - '09': xbmc.getLocalizedString(59), - '10': xbmc.getLocalizedString(60), - '11': xbmc.getLocalizedString(61), - '12': xbmc.getLocalizedString(62), -} - - -def has_socks(): - ''' Test if socks is installed, and remember this information ''' - if hasattr(has_socks, 'installed'): - return has_socks.installed - try: - import socks # noqa: F401; pylint: disable=unused-variable,unused-import - has_socks.installed = True - return True - except ImportError: - has_socks.installed = False - return None # Detect if this is the first run - - -class SafeDict(dict): - ''' A safe dictionary implementation that does not break down on missing keys ''' - def __missing__(self, key): - ''' Replace missing keys with the original placeholder ''' - return '{' + key + '}' - - -class KodiWrapper: - ''' A wrapper around all Kodi functionality ''' - - def __init__(self, addon): - ''' Initialize the Kodi wrapper ''' - if addon: - self.addon = addon - self.plugin = addon['plugin'] - self._handle = self.plugin.handle - self._url = self.plugin.base_url - self._addon = Addon() - self._addon_id = to_unicode(self._addon.getAddonInfo('id')) - self._addon_fanart = to_unicode(self._addon.getAddonInfo('fanart')) - self._cache_path = self.get_userdata_path() + 'cache/' - self._tokens_path = self.get_userdata_path() + 'tokens/' - self._system_locale_works = None - - def url_for(self, name, *args, **kwargs): - ''' Wrapper for routing.url_for() to lookup by name ''' - return self.plugin.url_for(self.addon[name], *args, **kwargs) - - def show_listing(self, list_items, category=None, sort='unsorted', ascending=True, content=None, cache=None, selected=None): - ''' Show a virtual directory in Kodi ''' - from xbmcgui import ListItem - - xbmcplugin.setPluginFanart(handle=self._handle, image=from_unicode(self._addon_fanart)) - - usemenucaching = self.get_setting('usemenucaching', 'true') == 'true' - if cache is None: - cache = usemenucaching - elif usemenucaching is False: - cache = False - - if content: - # content is one of: files, songs, artists, albums, movies, tvshows, episodes, musicvideos - xbmcplugin.setContent(self._handle, content=content) - - # Jump through hoops to get a stable breadcrumbs implementation - category_label = '' - if category: - if not content: - category_label = 'VRT NU / ' - from addon import plugin - if plugin.path.startswith(('/favorites/', '/resumepoints/')): - category_label += self.localize(30428) + ' / ' # My - if isinstance(category, int): - category_label += self.localize(category) - else: - category_label += category - elif not content: - category_label = 'VRT NU' - xbmcplugin.setPluginCategory(handle=self._handle, category=category_label) - - # FIXME: Since there is no way to influence descending order, we force it here - if not ascending: - sort = 'unsorted' - - # NOTE: When showing tvshow listings and 'showoneoff' was set, force 'unsorted' - if self.get_setting('showoneoff', 'true') == 'true' and sort == 'label' and content == 'tvshows': - sort = 'unsorted' - - # Add all sort methods to GUI (start with preferred) - xbmcplugin.addSortMethod(handle=self._handle, sortMethod=SORT_METHODS[sort]) - for key in sorted(SORT_METHODS): - if key != sort: - xbmcplugin.addSortMethod(handle=self._handle, sortMethod=SORT_METHODS[key]) - - # FIXME: This does not appear to be working, we have to order it ourselves -# xbmcplugin.setProperty(handle=self._handle, key='sort.ascending', value='true' if ascending else 'false') -# if ascending: -# xbmcplugin.setProperty(handle=self._handle, key='sort.order', value=str(SORT_METHODS[sort])) -# else: -# # NOTE: When descending, use unsorted -# xbmcplugin.setProperty(handle=self._handle, key='sort.order', value=str(SORT_METHODS['unsorted'])) - - listing = [] - for title_item in list_items: - # Three options: - # - item is a virtual directory/folder (not playable, path) - # - item is a playable file (playable, path) - # - item is non-actionable item (not playable, no path) - is_folder = bool(not title_item.is_playable and title_item.path) - is_playable = bool(title_item.is_playable and title_item.path) - - list_item = ListItem(label=title_item.title) - - if title_item.prop_dict: - # FIXME: The setProperties method is new in Kodi18, so we cannot use it just yet. - # list_item.setProperties(values=title_item.prop_dict) - for key, value in list(title_item.prop_dict.items()): - list_item.setProperty(key=key, value=str(value)) - list_item.setProperty(key='IsInternetStream', value='true' if is_playable else 'false') - list_item.setProperty(key='IsPlayable', value='true' if is_playable else 'false') - - # FIXME: The setIsFolder method is new in Kodi18, so we cannot use it just yet. - # list_item.setIsFolder(is_folder) - - if title_item.art_dict: - list_item.setArt(dict(fanart=self._addon_fanart)) - list_item.setArt(title_item.art_dict) - - if title_item.info_dict: - # type is one of: video, music, pictures, game - list_item.setInfo(type='video', infoLabels=title_item.info_dict) - - if title_item.stream_dict: - # type is one of: video, audio, subtitle - list_item.addStreamInfo('video', title_item.stream_dict) - - if title_item.context_menu: - list_item.addContextMenuItems(title_item.context_menu) - - url = None - if title_item.path: - url = title_item.path - - listing.append((url, list_item, is_folder)) - - # Jump to specific item - if selected is not None: - pass -# from xbmcgui import getCurrentWindowId, Window -# wnd = Window(getCurrentWindowId()) -# wnd.getControl(wnd.getFocusId()).selectItem(selected) - - succeeded = xbmcplugin.addDirectoryItems(self._handle, listing, len(listing)) - xbmcplugin.endOfDirectory(self._handle, succeeded, updateListing=False, cacheToDisc=cache) - - def play(self, stream, video=None): - ''' Create a virtual directory listing to play its only item ''' - from xbmcgui import ListItem - play_item = ListItem(path=stream.stream_url) - if video and hasattr(video, 'info_dict'): - play_item.setProperty('subtitle', video.title) - play_item.setArt(video.art_dict) - play_item.setInfo( - type='video', - infoLabels=video.info_dict - ) - play_item.setProperty('inputstream.adaptive.max_bandwidth', str(self.get_max_bandwidth() * 1000)) - play_item.setProperty('network.bandwidth', str(self.get_max_bandwidth() * 1000)) - if stream.stream_url is not None and stream.use_inputstream_adaptive: - play_item.setProperty('inputstreamaddon', 'inputstream.adaptive') - play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd') - play_item.setMimeType('application/dash+xml') - play_item.setContentLookup(False) - if stream.license_key is not None: - import inputstreamhelper - is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha') - if is_helper.check_inputstream(): - play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') - play_item.setProperty('inputstream.adaptive.license_key', stream.license_key) - - subtitles_visible = self.get_setting('showsubtitles', 'true') == 'true' - # Separate subtitle url for hls-streams - if subtitles_visible and stream.subtitle_url is not None: - self.log(2, 'Subtitle URL: {url}', url=unquote(stream.subtitle_url)) - play_item.setSubtitles([stream.subtitle_url]) - - self.log(1, 'Play: {url}', url=unquote(stream.stream_url)) - xbmcplugin.setResolvedUrl(self._handle, bool(stream.stream_url), listitem=play_item) - - while not xbmc.Player().isPlaying() and not xbmc.Monitor().abortRequested(): - xbmc.sleep(100) - xbmc.Player().showSubtitles(subtitles_visible) - - def addon_id(self): - ''' Return VRT NU Add-on ID ''' - return self._addon_id - - def get_search_string(self): - ''' Ask the user for a search string ''' - search_string = None - keyboard = xbmc.Keyboard('', self.localize(30134)) - keyboard.doModal() - if keyboard.isConfirmed(): - search_string = to_unicode(keyboard.getText()) - return search_string - - def show_ok_dialog(self, heading='', message=''): - ''' Show Kodi's OK dialog ''' - from xbmcgui import Dialog - if not heading: - heading = self._addon.getAddonInfo('name') - return Dialog().ok(heading=heading, line1=message) - - def show_notification(self, heading='', message='', icon='info', time=4000): - ''' Show a Kodi notification ''' - from xbmcgui import Dialog - if not heading: - heading = self._addon.getAddonInfo('name') - Dialog().notification(heading=heading, message=message, icon=icon, time=time) - - def show_multiselect(self, heading='', options=None, autoclose=0, preselect=None, use_details=False): - ''' Show a Kodi multi-select dialog ''' - from xbmcgui import Dialog - if not heading: - heading = self._addon.getAddonInfo('name') - return Dialog().multiselect(heading=heading, options=options, autoclose=autoclose, preselect=preselect, useDetails=use_details) - - def set_locale(self): - ''' Load the proper locale for date strings ''' - import locale - locale_lang = self.get_global_setting('locale.language').split('.')[-1] - try: - # NOTE: This only works if the platform supports the Kodi configured locale - locale.setlocale(locale.LC_ALL, locale_lang) - return True - except Exception as exc: # pylint: disable=broad-except - if locale_lang == 'en_gb': - return True - self.log(3, "Your system does not support locale '{locale}': {error}", locale=locale_lang, error=exc) - return False - - def localize(self, string_id, **kwargs): - ''' Return the translated string from the .po language files, optionally translating variables ''' - if kwargs: - import string - return string.Formatter().vformat(self._addon.getLocalizedString(string_id), (), SafeDict(**kwargs)) - - return self._addon.getLocalizedString(string_id) - - def localize_date(self, date, strftime): - ''' Return a localized date, even if the system does not support your locale ''' - if self._system_locale_works is None: - self._system_locale_works = self.set_locale() - if not self._system_locale_works: - if '%a' in strftime: - strftime = strftime.replace('%a', WEEKDAY_SHORT[date.strftime('%w')]) - elif '%A' in strftime: - strftime = strftime.replace('%A', WEEKDAY_LONG[date.strftime('%w')]) - if '%b' in strftime: - strftime = strftime.replace('%b', MONTH_SHORT[date.strftime('%m')]) - elif '%B' in strftime: - strftime = strftime.replace('%B', MONTH_LONG[date.strftime('%m')]) - return date.strftime(strftime) - - def localize_datelong(self, date): - ''' Return a localized long date string ''' - return self.localize_date(date, xbmc.getRegion('datelong')) - - def localize_from_data(self, name, data): - ''' Return a localized name string from a Dutch data object ''' - # Return if Kodi language is Dutch - if self.get_global_setting('locale.language') == 'resource.language.nl_nl': - return name - return next((self.localize(item.get('msgctxt')) for item in data if item.get('name') == name), name) - - def get_setting(self, setting_id, default=None): - ''' Get an add-on setting ''' - value = to_unicode(self._addon.getSetting(setting_id)) - if value == '' and default is not None: - return default - return value - - def set_setting(self, setting_id, setting_value): - ''' Set an add-on setting ''' - return self._addon.setSetting(setting_id, setting_value) - - def open_settings(self): - ''' Open the add-in settings window, shows Credentials ''' - self._addon.openSettings() - - def get_global_setting(self, setting): - ''' Get a Kodi setting ''' - result = self.jsonrpc(method='Settings.GetSettingValue', params=dict(setting=setting)) - return result.get('result', {}).get('value') - - def notify(self, sender, message, data): - ''' Send a notification to Kodi using JSON RPC ''' - result = self.jsonrpc(method='JSONRPC.NotifyAll', params=dict( - sender=sender, - message=message, - data=data, - )) - if result.get('result') != 'OK': - self.log_error('Failed to send notification: {error}', error=result.get('error').get('message')) - return False - self.log(2, 'Succesfully sent notification') - return True - - def get_playerid(self): - ''' Get current playerid ''' - result = dict() - while not result.get('result'): - result = self.jsonrpc(method='Player.GetActivePlayers') - return result.get('result', [{}])[0].get('playerid') - - def get_max_bandwidth(self): - ''' Get the max bandwidth based on Kodi and VRT NU add-on settings ''' - vrtnu_max_bandwidth = int(self.get_setting('max_bandwidth', '0')) - global_max_bandwidth = int(self.get_global_setting('network.bandwidth')) - if vrtnu_max_bandwidth != 0 and global_max_bandwidth != 0: - return min(vrtnu_max_bandwidth, global_max_bandwidth) - if vrtnu_max_bandwidth != 0: - return vrtnu_max_bandwidth - if global_max_bandwidth != 0: - return global_max_bandwidth - return 0 - - def get_proxies(self): - ''' Return a usable proxies dictionary from Kodi proxy settings ''' - usehttpproxy = self.get_global_setting('network.usehttpproxy') - if usehttpproxy is not True: - return None - - try: - httpproxytype = int(self.get_global_setting('network.httpproxytype')) - except ValueError: - httpproxytype = 0 - - socks_supported = has_socks() - if httpproxytype != 0 and not socks_supported: - # Only open the dialog the first time (to avoid multiple popups) - if socks_supported is None: - self.show_ok_dialog('', self.localize(30966)) # Requires PySocks - return None - - proxy_types = ['http', 'socks4', 'socks4a', 'socks5', 'socks5h'] - if 0 <= httpproxytype < 5: - httpproxyscheme = proxy_types[httpproxytype] - else: - httpproxyscheme = 'http' - - httpproxyserver = self.get_global_setting('network.httpproxyserver') - httpproxyport = self.get_global_setting('network.httpproxyport') - httpproxyusername = self.get_global_setting('network.httpproxyusername') - httpproxypassword = self.get_global_setting('network.httpproxypassword') - - if httpproxyserver and httpproxyport and httpproxyusername and httpproxypassword: - proxy_address = '%s://%s:%s@%s:%s' % (httpproxyscheme, httpproxyusername, httpproxypassword, httpproxyserver, httpproxyport) - elif httpproxyserver and httpproxyport and httpproxyusername: - proxy_address = '%s://%s@%s:%s' % (httpproxyscheme, httpproxyusername, httpproxyserver, httpproxyport) - elif httpproxyserver and httpproxyport: - proxy_address = '%s://%s:%s' % (httpproxyscheme, httpproxyserver, httpproxyport) - elif httpproxyserver: - proxy_address = '%s://%s' % (httpproxyscheme, httpproxyserver) - else: - return None - - return dict(http=proxy_address, https=proxy_address) - - @staticmethod - def get_cond_visibility(condition): - ''' Test a condition in XBMC ''' - return xbmc.getCondVisibility(condition) - - def has_inputstream_adaptive(self): - ''' Whether InputStream Adaptive is installed and enabled in add-on settings ''' - return self.get_setting('useinputstreamadaptive', 'true') == 'true' and xbmc.getCondVisibility('System.HasAddon(inputstream.adaptive)') == 1 - - @staticmethod - def has_addon(addon_id): - ''' Checks if add-on is installed ''' - return xbmc.getCondVisibility('System.HasAddon(%s)' % addon_id) == 1 - - def credentials_filled_in(self): - ''' Whether the add-on has credentials filled in ''' - return bool(self.get_setting('username') and self.get_setting('password')) - - @staticmethod - def kodi_version(): - ''' Returns major Kodi version ''' - return int(xbmc.getInfoLabel('System.BuildVersion').split('.')[0]) - - def can_play_drm(self): - ''' Whether this Kodi can do DRM using InputStream Adaptive ''' - return self.get_setting('usedrm', 'true') == 'true' and self.get_setting('useinputstreamadaptive', 'true') == 'true' and self.supports_drm() - - def supports_drm(self): - ''' Whether this Kodi version supports DRM decryption using InputStream Adaptive ''' - return self.kodi_version() > 17 - - def get_userdata_path(self): - ''' Return the profile's userdata path ''' - return to_unicode(xbmc.translatePath(self._addon.getAddonInfo('profile'))) - - def get_tokens_path(self): - ''' Return the userdata tokens path ''' - return self._tokens_path - - def get_addon_info(self, key): - ''' Return addon information ''' - return self._addon.getAddonInfo(key) - - @staticmethod - def listdir(path): - ''' Return all files in a directory (using xbmcvfs)''' - from xbmcvfs import listdir - return listdir(path) - - def mkdir(self, path): - ''' Create a directory (using xbmcvfs) ''' - from xbmcvfs import mkdir - self.log(3, "Create directory '{path}'.", path=path) - return mkdir(path) - - def mkdirs(self, path): - ''' Create directory including parents (using xbmcvfs) ''' - from xbmcvfs import mkdirs - self.log(3, "Recursively create directory '{path}'.", path=path) - return mkdirs(path) - - @staticmethod - def check_if_path_exists(path): - ''' Whether the path exists (using xbmcvfs)''' - from xbmcvfs import exists - return exists(path) - - @staticmethod - @contextmanager - def open_file(path, flags='r'): - ''' Open a file (using xbmcvfs) ''' - from xbmcvfs import File - fdesc = File(path, flags) - yield fdesc - fdesc.close() - - @staticmethod - def stat_file(path): - ''' Return information about a file (using xbmcvfs) ''' - from xbmcvfs import Stat - return Stat(path) - - def delete_file(self, path): - ''' Remove a file (using xbmcvfs) ''' - from xbmcvfs import delete - self.log(3, "Delete file '{path}'.", path=path) - return delete(path) - - def delete_cached_thumbnail(self, url): - ''' Remove a cached thumbnail from Kodi in an attempt to get a realtime live screenshot ''' - # Get texture - result = self.jsonrpc(method='Textures.GetTextures', params=dict( - filter=dict( - field='url', - operator='is', - value=url, - ), - )) - if result.get('result', {}).get('textures') is None: - self.log_error('URL {url} not found in texture cache', url=url) - return False - - texture_id = next((texture.get('textureid') for texture in result.get('result').get('textures')), None) - if not texture_id: - self.log_error('URL {url} not found in texture cache', url=url) - return False - self.log(2, 'found texture_id {id} for url {url} in texture cache', id=texture_id, url=url) - - # Remove texture - result = self.jsonrpc(method='Textures.RemoveTexture', params=dict(textureid=texture_id)) - if result.get('result') != 'OK': - self.log_error('failed to remove {url} from texture cache: {error}', url=url, error=result.get('error', {}).get('message')) - return False - - self.log(2, 'succesfully removed {url} from texture cache', url=url) - return True - - @staticmethod - def md5(data): - ''' Return an MD5 checksum ''' - import hashlib - return hashlib.md5(data) - - @staticmethod - def human_delta(seconds): - ''' Return a human-readable representation of the TTL ''' - from math import floor - days = int(floor(seconds / (24 * 60 * 60))) - seconds = seconds % (24 * 60 * 60) - hours = int(floor(seconds / (60 * 60))) - seconds = seconds % (60 * 60) - if days: - return '%d day%s and %d hour%s' % (days, 's' if days != 1 else '', hours, 's' if hours != 1 else '') - minutes = int(floor(seconds / 60)) - seconds = seconds % 60 - if hours: - return '%d hour%s and %d minute%s' % (hours, 's' if hours != 1 else '', minutes, 's' if minutes != 1 else '') - if minutes: - return '%d minute%s and %d second%s' % (minutes, 's' if minutes != 1 else '', seconds, 's' if seconds != 1 else '') - return '%d second%s' % (seconds, 's' if seconds != 1 else '') - - def get_cache(self, path, ttl=None): - ''' Get the content from cache, if it's still fresh ''' - if self.get_setting('usehttpcaching', 'true') == 'false': - return None - - fullpath = self._cache_path + path - if not self.check_if_path_exists(fullpath): - return None - - import time - mtime = self.stat_file(fullpath).st_mtime() - now = time.mktime(time.localtime()) - if ttl is None or now - mtime < ttl: - import json - if ttl is None: - self.log(3, "Cache '{path}' is forced from cache.", path=path) - else: - self.log(3, "Cache '{path}' is fresh, expires in {time}.", path=path, time=self.human_delta(mtime + ttl - now)) - with self.open_file(fullpath, 'r') as fdesc: - try: - # return json.load(fdesc, encoding='utf-8') - return json.load(fdesc) - except (TypeError, ValueError): - return None - - return None - - def update_cache(self, path, data): - ''' Update the cache, if necessary ''' - if self.get_setting('usehttpcaching', 'true') == 'false': - return - - import hashlib - import json - fullpath = self._cache_path + path - if self.check_if_path_exists(fullpath): - with self.open_file(fullpath) as fdesc: - cachefile = fdesc.read().encode('utf-8') - md5 = self.md5(cachefile) - else: - md5 = 0 - # Create cache directory if missing - if not self.check_if_path_exists(self._cache_path): - self.mkdirs(self._cache_path) - - # Avoid writes if possible (i.e. SD cards) - if md5 != hashlib.md5(json.dumps(data).encode('utf-8')): - self.log(3, "Write cache '{path}'.", path=path) - with self.open_file(fullpath, 'w') as fdesc: - # json.dump(data, fdesc, encoding='utf-8') - json.dump(data, fdesc) - else: - # Update timestamp - import os - self.log(3, "Cache '{path}' has not changed, updating mtime only.", path=path) - os.utime(path) - - def refresh_caches(self, cache_file=None): - ''' Invalidate the needed caches and refresh container ''' - files = ['favorites.json', 'oneoff.json', 'resume_points.json'] - if cache_file and cache_file not in files: - files.append(cache_file) - self.invalidate_caches(*files) - self.container_refresh() - self.show_notification(message=self.localize(30981)) - - def invalidate_caches(self, *caches): - ''' Invalidate multiple cache files ''' - import fnmatch - _, files = self.listdir(self._cache_path) - # Invalidate caches related to menu list refreshes - removes = set() - for expr in caches: - removes.update(fnmatch.filter(files, expr)) - for filename in removes: - self.delete_file(self._cache_path + filename) - - def input_down(self): - ''' Move the cursor down ''' - self.jsonrpc(method='Input.Down') - - @staticmethod - def current_container_url(): - ''' Get current container plugin:// url ''' - url = xbmc.getInfoLabel('Container.FolderPath') - if url == '': - url = None - return url - - def container_refresh(self, url=None): - ''' Refresh the current container or (re)load a container by url ''' - if url: - self.log(3, 'Execute: Container.Refresh({url})', url=url) - xbmc.executebuiltin('Container.Refresh({url})'.format(url=url)) - else: - self.log(3, 'Execute: Container.Refresh') - xbmc.executebuiltin('Container.Refresh') - - def container_update(self, url=None): - ''' Update the current container with respect for the path history. ''' - if url: - self.log(3, 'Execute: Container.Update({url})', url=url) - xbmc.executebuiltin('Container.Update({url})'.format(url=url)) - else: - # Url is a mandatory argument for Container.Update, use Container.Refresh instead - self.log(3, 'Execute: Container.Refresh') - xbmc.executebuiltin('Container.Refresh') - - def end_of_directory(self): - ''' Close a virtual directory, required to avoid a waiting Kodi ''' - xbmcplugin.endOfDirectory(handle=self._handle, succeeded=False, updateListing=False, cacheToDisc=False) - - def log(self, level=1, message='', **kwargs): - ''' Log info messages to Kodi ''' - debug_logging = self.get_global_setting('debug.showloginfo') # Returns a boolean - max_log_level = int(self.get_setting('max_log_level', 0)) - if not debug_logging and not (level <= max_log_level and max_log_level != 0): - return - if kwargs: - import string - message = string.Formatter().vformat(message, (), SafeDict(**kwargs)) - message = '[{addon}] {message}'.format(addon=self._addon_id, message=message) - xbmc.log(from_unicode(message), level % 3 if debug_logging else 2) - - def log_access(self, url, query_string=None): - ''' Log addon access ''' - message = 'Access: %s' % (url + ('?' + query_string if query_string else '')) - self.log(1, message) - - def log_error(self, message, **kwargs): - ''' Log error messages to Kodi ''' - if kwargs: - import string - message = string.Formatter().vformat(message, (), SafeDict(**kwargs)) - message = '[{addon}] {message}'.format(addon=self._addon_id, message=message) - xbmc.log(from_unicode(message), 4) - - @staticmethod - def jsonrpc(**kwargs): - ''' Perform JSONRPC calls ''' - import json - if 'id' not in kwargs: - kwargs.update(id=1) - if 'jsonrpc' not in kwargs: - kwargs.update(jsonrpc='2.0') - return json.loads(xbmc.executeJSONRPC(json.dumps(kwargs))) diff --git a/resources/lib/metadata.py b/resources/lib/metadata.py index c01f8220..cfa4f9c1 100644 --- a/resources/lib/metadata.py +++ b/resources/lib/metadata.py @@ -13,13 +13,13 @@ import statichelper from data import CHANNELS, SECONDS_MARGIN +from kodiutils import get_setting, localize, localize_datelong, log, url_for class Metadata: ''' This class creates appropriate Kodi ListItem metadata from single item json api data ''' - def __init__(self, _kodi, _favorites, _resumepoints): - self._kodi = _kodi + def __init__(self, _favorites, _resumepoints): self._favorites = _favorites self._resumepoints = _resumepoints @@ -82,15 +82,15 @@ def get_context_menu(self, api_data, program, cache_file): extras = dict(move_down=True) # Unwatch context menu context_menu.append(( - statichelper.capitalize(self._kodi.localize(30402)), - 'RunPlugin(%s)' % self._kodi.url_for('unwatchlater', uuid=assetuuid, title=program_title, url=url, **extras) + statichelper.capitalize(localize(30402)), + 'RunPlugin(%s)' % url_for('unwatchlater', uuid=assetuuid, title=program_title, url=url, **extras) )) watchlater_marker = '[COLOR yellow]ᶫ[/COLOR]' else: # Watch context menu context_menu.append(( - statichelper.capitalize(self._kodi.localize(30401)), - 'RunPlugin(%s)' % self._kodi.url_for('watchlater', uuid=assetuuid, title=program_title, url=url) + statichelper.capitalize(localize(30401)), + 'RunPlugin(%s)' % url_for('watchlater', uuid=assetuuid, title=program_title, url=url) )) # FOLLOW PROGRAM @@ -100,7 +100,7 @@ def get_context_menu(self, api_data, program, cache_file): if api_data.get('type') == 'episode': program_title = api_data.get('program') program_type = api_data.get('programType') - follow_suffix = self._kodi.localize(30410) if program_type != 'oneoff' else '' # program + follow_suffix = localize(30410) if program_type != 'oneoff' else '' # program follow_enabled = True # VRT NU Suggest API @@ -112,7 +112,7 @@ def get_context_menu(self, api_data, program, cache_file): # VRT NU Schedule API (some are missing vrt.whatson-id) elif api_data.get('vrt.whatson-id') or api_data.get('startTime'): program_title = api_data.get('title') - follow_suffix = self._kodi.localize(30410) # program + follow_suffix = localize(30410) # program follow_enabled = bool(api_data.get('url')) if follow_enabled: @@ -123,14 +123,14 @@ def get_context_menu(self, api_data, program, cache_file): if plugin.path.startswith('/favorites'): extras = dict(move_down=True) context_menu.append(( - self._kodi.localize(30412, title=follow_suffix), # Unfollow - 'RunPlugin(%s)' % self._kodi.url_for('unfollow', program=program, title=program_title, **extras) + localize(30412, title=follow_suffix), # Unfollow + 'RunPlugin(%s)' % url_for('unfollow', program=program, title=program_title, **extras) )) favorite_marker = '[COLOR yellow]ᵛ[/COLOR]' else: context_menu.append(( - self._kodi.localize(30411, title=follow_suffix), # Follow - 'RunPlugin(%s)' % self._kodi.url_for('follow', program=program, title=program_title) + localize(30411, title=follow_suffix), # Follow + 'RunPlugin(%s)' % url_for('follow', program=program, title=program_title) )) # GO TO PROGRAM @@ -138,14 +138,14 @@ def get_context_menu(self, api_data, program, cache_file): if plugin.path.startswith(('/favorites/offline', '/favorites/recent', '/offline', '/recent', '/resumepoints/continue', '/resumepoints/watchlater', '/tvguide')): context_menu.append(( - self._kodi.localize(30417), # Go to program - 'Container.Update(%s)' % self._kodi.url_for('programs', program=program, season='allseasons') + localize(30417), # Go to program + 'Container.Update(%s)' % url_for('programs', program=program, season='allseasons') )) # REFRESH MENU context_menu.append(( - self._kodi.localize(30413), # Refresh menu - 'RunPlugin(%s)' % self._kodi.url_for('delete_cache', cache_file=cache_file) + localize(30413), # Refresh menu + 'RunPlugin(%s)' % url_for('delete_cache', cache_file=cache_file) )) return context_menu, favorite_marker, watchlater_marker @@ -194,7 +194,7 @@ def get_properties(self, api_data): total = self._resumepoints.get_total(assetuuid) if position and total and SECONDS_MARGIN < position < total - SECONDS_MARGIN: properties['resumetime'] = position - self._kodi.log(2, '[Metadata] manual resumetime set to %d' % position) + log(2, '[Metadata] manual resumetime set to %d' % position) duration = self.get_duration(api_data) if duration: @@ -269,7 +269,7 @@ def get_plot(self, api_data, season=False, date=None): # Add additional metadata to plot plot_meta = '' if api_data.get('allowedRegion') == 'BE': - plot_meta += self._kodi.localize(30201) + '\n\n' # Geo-blocked + plot_meta += localize(30201) + '\n\n' # Geo-blocked plot = '%s[B]%s[/B]\n%s' % (plot_meta, api_data.get('program'), plot) return plot @@ -286,24 +286,24 @@ def get_plot(self, api_data, season=False, date=None): if remaining.days / 365 > 5: pass # If it is available for more than 5 years, do not show elif remaining.days / 365 > 2: - plot_meta += self._kodi.localize(30202, years=int(remaining.days / 365)) # X years remaining + plot_meta += localize(30202, years=int(remaining.days / 365)) # X years remaining elif remaining.days / 30.5 > 3: - plot_meta += self._kodi.localize(30203, months=int(remaining.days / 30.5)) # X months remaining + plot_meta += localize(30203, months=int(remaining.days / 30.5)) # X months remaining elif remaining.days > 1: - plot_meta += self._kodi.localize(30204, days=remaining.days) # X days to go + plot_meta += localize(30204, days=remaining.days) # X days to go elif remaining.days == 1: - plot_meta += self._kodi.localize(30205) # 1 day to go + plot_meta += localize(30205) # 1 day to go elif remaining.seconds // 3600 > 1: - plot_meta += self._kodi.localize(30206, hours=remaining.seconds // 3600) # X hours to go + plot_meta += localize(30206, hours=remaining.seconds // 3600) # X hours to go elif remaining.seconds // 3600 == 1: - plot_meta += self._kodi.localize(30207) # 1 hour to go + plot_meta += localize(30207) # 1 hour to go else: - plot_meta += self._kodi.localize(30208, minutes=remaining.seconds // 60) # X minutes to go + plot_meta += localize(30208, minutes=remaining.seconds // 60) # X minutes to go if api_data.get('allowedRegion') == 'BE': if plot_meta: plot_meta += ' ' - plot_meta += self._kodi.localize(30201) # Geo-blocked + plot_meta += localize(30201) # Geo-blocked plot = statichelper.convert_html_to_kodilabel(api_data.get('description')) @@ -311,7 +311,7 @@ def get_plot(self, api_data, season=False, date=None): plot = '%s\n\n%s' % (plot_meta, plot) permalink = statichelper.shorten_link(api_data.get('permalink')) or api_data.get('externalPermalink') - if permalink and self._kodi.get_setting('showpermalink', 'false') == 'true': + if permalink and get_setting('showpermalink', 'false') == 'true': plot = '%s\n\n[COLOR yellow]%s[/COLOR]' % (plot, permalink) return plot @@ -319,7 +319,7 @@ def get_plot(self, api_data, season=False, date=None): if api_data.get('type') == 'program': plot = statichelper.unescape(api_data.get('description', '???')) # permalink = statichelper.shorten_link(api_data.get('programUrl')) - # if permalink and self._kodi.get_setting('showpermalink', 'false') == 'true': + # if permalink and get_setting('showpermalink', 'false') == 'true': # plot = '%s\n\n[COLOR yellow]%s[/COLOR]' % (plot, permalink) return plot @@ -328,7 +328,7 @@ def get_plot(self, api_data, season=False, date=None): title = api_data.get('title') now = datetime.now(dateutil.tz.tzlocal()) epg = self.parse(date, now) - datelong = self._kodi.localize_datelong(epg) + datelong = localize_datelong(epg) start = api_data.get('start') end = api_data.get('end') plot = '[B]%s[/B]\n%s\n%s - %s' % (title, datelong, start, end) @@ -508,14 +508,15 @@ def get_year(api_data): # Not Found return '' - def get_art(self, api_data, season=False): + @staticmethod + def get_art(api_data, season=False): ''' Get art dict from single item json api data ''' art_dict = dict() # VRT NU Search API if api_data.get('type') == 'episode': if season: - if self._kodi.get_setting('showfanart', 'true') == 'true': + if get_setting('showfanart', 'true') == 'true': art_dict['fanart'] = statichelper.add_https_method(api_data.get('programImageUrl', 'DefaultSets.png')) art_dict['banner'] = art_dict.get('fanart') if season != 'allseasons': @@ -525,7 +526,7 @@ def get_art(self, api_data, season=False): else: art_dict['thumb'] = 'DefaultSets.png' else: - if self._kodi.get_setting('showfanart', 'true') == 'true': + if get_setting('showfanart', 'true') == 'true': art_dict['thumb'] = statichelper.add_https_method(api_data.get('videoThumbnailUrl', 'DefaultAddonVideo.png')) art_dict['fanart'] = statichelper.add_https_method(api_data.get('programImageUrl', art_dict.get('thumb'))) art_dict['banner'] = art_dict.get('fanart') @@ -536,7 +537,7 @@ def get_art(self, api_data, season=False): # VRT NU Suggest API if api_data.get('type') == 'program': - if self._kodi.get_setting('showfanart', 'true') == 'true': + if get_setting('showfanart', 'true') == 'true': art_dict['thumb'] = statichelper.add_https_method(api_data.get('thumbnail', 'DefaultAddonVideo.png')) art_dict['fanart'] = art_dict.get('thumb') art_dict['banner'] = art_dict.get('fanart') @@ -547,7 +548,7 @@ def get_art(self, api_data, season=False): # VRT NU Schedule API (some are missing vrt.whatson-id) if api_data.get('vrt.whatson-id') or api_data.get('startTime'): - if self._kodi.get_setting('showfanart', 'true') == 'true': + if get_setting('showfanart', 'true') == 'true': art_dict['thumb'] = api_data.get('image', 'DefaultAddonVideo.png') art_dict['fanart'] = art_dict.get('thumb') art_dict['banner'] = art_dict.get('fanart') @@ -612,7 +613,8 @@ def get_info_labels(self, api_data, season=False, date=None, channel=None): # Not Found return dict() - def get_label(self, api_data, titletype=None, return_sort=False): + @staticmethod + def get_label(api_data, titletype=None, return_sort=False): ''' Get an appropriate label string matching the type of listing and VRT NU provided displayOptions from single item json api data ''' # VRT NU Search API @@ -657,7 +659,7 @@ def get_label(self, api_data, titletype=None, return_sort=False): sort = 'episode' elif display_options.get('showEpisodeNumber') and api_data.get('episodeNumber') and ascending: # NOTE: Do not prefix with "Episode X" when sorting by episode - # label = '%s %s: %s' % (self._kodi.localize(30132), api_data.get('episodeNumber'), label) + # label = '%s %s: %s' % (localize(30132), api_data.get('episodeNumber'), label) sort = 'episode' elif display_options.get('showBroadcastDate') and api_data.get('formattedBroadcastShortDate'): label = '%s - %s' % (api_data.get('formattedBroadcastShortDate'), label) @@ -689,11 +691,11 @@ def get_label(self, api_data, titletype=None, return_sort=False): end_date = dateutil.parser.parse(api_data.get('endTime')) if api_data.get('url'): if start_date <= now <= end_date: # Now playing - label = '[COLOR yellow]%s[/COLOR] %s' % (label, self._kodi.localize(30301)) + label = '[COLOR yellow]%s[/COLOR] %s' % (label, localize(30301)) else: # This is a non-actionable item if start_date < now <= end_date: # Now playing - label = '[COLOR gray]%s[/COLOR] %s' % (label, self._kodi.localize(30301)) + label = '[COLOR gray]%s[/COLOR] %s' % (label, localize(30301)) else: label = '[COLOR gray]%s[/COLOR]' % label @@ -706,13 +708,14 @@ def get_label(self, api_data, titletype=None, return_sort=False): return label - def get_tag(self, api_data): + @staticmethod + def get_tag(api_data): ''' Return categories for a given episode ''' # VRT NU Search API if api_data.get('type') == 'episode': from data import CATEGORIES - return sorted([self._kodi.localize(statichelper.find_entry(CATEGORIES, 'id', category).get('msgctxt')) + return sorted([localize(statichelper.find_entry(CATEGORIES, 'id', category).get('msgctxt')) for category in api_data.get('categories')]) # VRT NU Suggest API diff --git a/resources/lib/playerinfo.py b/resources/lib/playerinfo.py index e649c1d4..61050ee3 100644 --- a/resources/lib/playerinfo.py +++ b/resources/lib/playerinfo.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals from threading import Event, Thread -from kodiwrapper import KodiWrapper +from kodiutils import log from xbmc import getInfoLabel, Player from statichelper import to_unicode @@ -14,7 +14,6 @@ class PlayerInfo(Player): def __init__(self, **kwargs): ''' PlayerInfo initialisation ''' - self._kodi = KodiWrapper(None) self._info = kwargs['info'] self._path = None self._paused = False @@ -23,12 +22,12 @@ def __init__(self, **kwargs): self._stop = Event() from random import randint self._id = randint(1, 101) - self._kodi.log(2, '[PlayerInfo] %d initialized' % self._id) + log(2, '[PlayerInfo] %d initialized' % self._id) Player.__init__(self) def onAVStarted(self): # pylint: disable=invalid-name ''' called when Kodi has a video or audiostream ''' - self._kodi.log(2, '[PlayerInfo] %d onAVStarted' % self._id) + log(2, '[PlayerInfo] %d onAVStarted' % self._id) self._stop.clear() self._last_pos = 0 self._total = self.getTotalTime() @@ -45,7 +44,7 @@ def onAVStarted(self): # pylint: disable=invalid-name def onPlayBackStopped(self): # pylint: disable=invalid-name ''' called when user stops Kodi playing a file ''' - self._kodi.log(2, '[PlayerInfo] %d onPlayBackStopped' % self._id) + log(2, '[PlayerInfo] %d onPlayBackStopped' % self._id) self._info(dict(path=self._path, position=self._last_pos, total=self._total, event='playbackstopped')) self._stop.set() @@ -54,26 +53,26 @@ def onAVChange(self): # pylint: disable=invalid-name def onPlayBackEnded(self): # pylint: disable=invalid-name ''' called when Kodi has ended playing a file ''' - self._kodi.log(2, '[PlayerInfo] %d onPlayBackEnded' % self._id) + log(2, '[PlayerInfo] %d onPlayBackEnded' % self._id) self._info(dict(path=self._path, position=self._total, total=self._total, event='playbackended')) self._stop.set() def onPlayBackError(self): # pylint: disable=invalid-name ''' called when playback stops due to an error ''' - self._kodi.log(2, '[PlayerInfo] %d onPlayBackError' % self._id) + log(2, '[PlayerInfo] %d onPlayBackError' % self._id) self._info(dict(path=self._path, position=self._last_pos, total=self._total, event='playbackerror')) self._stop.set() def onPlayBackPaused(self): # pylint: disable=invalid-name ''' called when user pauses a playing file ''' - self._kodi.log(2, '[PlayerInfo] %d onPlayBackPaused' % self._id) + log(2, '[PlayerInfo] %d onPlayBackPaused' % self._id) self._paused = True self._info(dict(path=self._path, position=self._last_pos, total=self._total, event='playbackpaused')) def onPlayBackResumed(self): # pylint: disable=invalid-name '''called when user resumes a paused file or a next playlist item is started ''' suffix = 'after pausing' if self._paused else 'after playlist change' - self._kodi.log(2, '[PlayerInfo] %d onPlayBackResumed %s' % (self._id, suffix)) + log(2, '[PlayerInfo] %d onPlayBackResumed %s' % (self._id, suffix)) if not self._paused: self._info(dict(path=self._path, position=self._last_pos, total=self._total, event='playbackresumed')) self._paused = False @@ -84,4 +83,4 @@ def stream_position(self): self._last_pos = self.getTime() if self._stop.wait(timeout=0.5): break - self._kodi.log(2, '[PlayerInfo] %d stream position loop exited' % self._id) + log(2, '[PlayerInfo] %d stream position loop exited' % self._id) diff --git a/resources/lib/resumepoints.py b/resources/lib/resumepoints.py index 27613c18..b34eccb9 100644 --- a/resources/lib/resumepoints.py +++ b/resources/lib/resumepoints.py @@ -12,31 +12,32 @@ from urllib2 import build_opener, install_opener, ProxyHandler, Request, HTTPError, urlopen from data import SECONDS_MARGIN +from kodiutils import (container_refresh, get_cache, get_proxies, get_setting, has_credentials, + input_down, invalidate_caches, localize, log, log_error, notification, + update_cache) class ResumePoints: ''' Track, cache and manage VRT resume points and watch list ''' - def __init__(self, _kodi): + def __init__(self): ''' Initialize resumepoints, relies on XBMC vfs and a special VRT token ''' - self._kodi = _kodi - self._proxies = _kodi.get_proxies() - install_opener(build_opener(ProxyHandler(self._proxies))) - # This is our internal representation - self._resumepoints = dict() + self._resumepoints = dict() # Our internal representation + install_opener(build_opener(ProxyHandler(get_proxies()))) - def is_activated(self): + @staticmethod + def is_activated(): ''' Is resumepoints activated in the menu and do we have credentials ? ''' - return self._kodi.get_setting('useresumepoints') == 'true' and self._kodi.credentials_filled_in() + return get_setting('useresumepoints') == 'true' and has_credentials() def refresh(self, ttl=None): ''' Get a cached copy or a newer resumepoints from VRT, or fall back to a cached file ''' if not self.is_activated(): return - resumepoints_json = self._kodi.get_cache('resume_points.json', ttl) + resumepoints_json = get_cache('resume_points.json', ttl) if not resumepoints_json: from tokenresolver import TokenResolver - xvrttoken = TokenResolver(self._kodi).get_xvrttoken(token_variant='user') + xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') if xvrttoken: headers = { 'authorization': 'Bearer ' + xvrttoken, @@ -44,22 +45,20 @@ def refresh(self, ttl=None): 'Referer': 'https://www.vrt.be/vrtnu', } req = Request('https://video-user-data.vrt.be/resume_points', headers=headers) - self._kodi.log(2, 'URL post: https://video-user-data.vrt.be/resume_points') - import json + log(2, 'URL post: https://video-user-data.vrt.be/resume_points') + from json import load try: - resumepoints_json = json.load(urlopen(req)) + resumepoints_json = load(urlopen(req)) except (TypeError, ValueError): # No JSON object could be decoded # Force resumepoints from cache - resumepoints_json = self._kodi.get_cache('resume_points.json', ttl=None) + resumepoints_json = get_cache('resume_points.json', ttl=None) else: - self._kodi.update_cache('resume_points.json', resumepoints_json) + update_cache('resume_points.json', resumepoints_json) if resumepoints_json: self._resumepoints = resumepoints_json def update(self, uuid, title, url, watch_later=None, position=None, total=None): ''' Set program resumepoint or watchLater status and update local copy ''' - removes = [] - self.refresh(ttl=0) # The video has no assetPath, so we cannot update resumepoints if uuid is None: @@ -68,6 +67,7 @@ def update(self, uuid, title, url, watch_later=None, position=None, total=None): if position is not None and position >= total - 30: watch_later = False + self.refresh(ttl=0) if watch_later is not None and position is None and total is None and watch_later is self.is_watchlater(uuid): # watchLater status is not changed, nothing to do return True @@ -78,10 +78,10 @@ def update(self, uuid, title, url, watch_later=None, position=None, total=None): # Collect header info for POST Request from tokenresolver import TokenResolver - xvrttoken = TokenResolver(self._kodi).get_xvrttoken(token_variant='user') + xvrttoken = TokenResolver().get_xvrttoken(token_variant='user') if xvrttoken is None: - self._kodi.log_error('Failed to get usertoken from VRT NU') - self._kodi.show_notification(message=self._kodi.localize(30975) + title) + log_error('Failed to get usertoken from VRT NU') + notification(message=localize(30975) + title) return False headers = { @@ -104,6 +104,7 @@ def update(self, uuid, title, url, watch_later=None, position=None, total=None): if total is not None: payload['total'] = total + removes = [] if position is not None or total is not None: removes.append('continue-*.json') @@ -112,22 +113,22 @@ def update(self, uuid, title, url, watch_later=None, position=None, total=None): payload['watchLater'] = watch_later removes.append('watchlater-*.json') - import json - data = json.dumps(payload).encode() - self._kodi.log(2, 'URL post: https://video-user-data.vrt.be/resume_points/{uuid}', uuid=uuid) - self._kodi.log(2, 'URL post data:: {data}', data=data) + from json import dumps + data = dumps(payload).encode() + log(2, 'URL post: https://video-user-data.vrt.be/resume_points/{uuid}', uuid=uuid) + log(2, 'URL post data:: {data}', data=data) try: req = Request('https://video-user-data.vrt.be/resume_points/%s' % uuid, data=data, headers=headers) urlopen(req) except HTTPError as exc: - self._kodi.log_error('Failed to (un)watch episode at VRT NU ({error})', error=exc) - self._kodi.show_notification(message=self._kodi.localize(30977)) + log_error('Failed to (un)watch episode at VRT NU ({error})', error=exc) + notification(message=localize(30977)) return False # NOTE: Updates to resumepoints take a longer time to take effect, so we keep our own cache and use it self._resumepoints[uuid] = dict(value=payload) - self._kodi.update_cache('resume_points.json', self._resumepoints) - self._kodi.invalidate_caches(*removes) + update_cache('resume_points.json', self._resumepoints) + invalidate_caches(*removes) return True def is_watchlater(self, uuid): @@ -138,18 +139,18 @@ def watchlater(self, uuid, title, url): ''' Watch an episode later ''' succeeded = self.update(uuid=uuid, title=title, url=url, watch_later=True) if succeeded: - self._kodi.show_notification(message=self._kodi.localize(30403, title=title)) - self._kodi.container_refresh() + notification(message=localize(30403, title=title)) + container_refresh() def unwatchlater(self, uuid, title, url, move_down=False): ''' Unwatch an episode later ''' succeeded = self.update(uuid=uuid, title=title, url=url, watch_later=False) if succeeded: - self._kodi.show_notification(message=self._kodi.localize(30404, title=title)) + notification(message=localize(30404, title=title)) # If the current item is selected and we need to move down before removing if move_down: - self._kodi.input_down() - self._kodi.container_refresh() + input_down() + container_refresh() def get_position(self, uuid): ''' Return the stored position of a video ''' @@ -180,8 +181,3 @@ def watchlater_urls(self): def resumepoints_urls(self): ''' Return all urls that have not been finished watching ''' return [self.get_url(uuid) for uuid in self._resumepoints if SECONDS_MARGIN < self.get_position(uuid) < (self.get_total(uuid) - SECONDS_MARGIN)] - - def invalidate_caches(self): - ''' Invalidate caches that rely on favorites ''' - # Delete resumepoints-related caches - self._kodi.invalidate_caches('continue-*.json', 'resume_points.json', 'watchlater-*.json') diff --git a/resources/lib/search.py b/resources/lib/search.py index ab2c558d..fd135034 100644 --- a/resources/lib/search.py +++ b/resources/lib/search.py @@ -4,45 +4,46 @@ ''' Implementation of Search class ''' from __future__ import absolute_import, division, unicode_literals -import json from favorites import Favorites from resumepoints import ResumePoints -from helperobjects import TitleItem +from kodiutils import (container_refresh, end_of_directory, get_search_string, get_userdata_path, + localize, ok_dialog, open_file, show_listing, url_for) class Search: ''' Search and cache search queries ''' - def __init__(self, _kodi): + def __init__(self): ''' Initialize searchtes, relies on XBMC vfs ''' - self._kodi = _kodi - self._favorites = Favorites(_kodi) - self._resumepoints = ResumePoints(_kodi) - - self._search_history = _kodi.get_userdata_path() + 'search_history.json' + self._favorites = Favorites() + self._resumepoints = ResumePoints() + self._search_history = get_userdata_path() + 'search_history.json' def read_history(self): ''' Read search history from disk ''' + from json import load try: - with self._kodi.open_file(self._search_history, 'r') as fdesc: - history = json.load(fdesc) + with open_file(self._search_history, 'r') as fdesc: + history = load(fdesc) except (TypeError, ValueError): # No JSON object could be decoded history = [] return history def write_history(self, history): ''' Write search history to disk ''' - with self._kodi.open_file(self._search_history, 'w') as fdesc: - json.dump(history, fdesc) + from json import dump + with open_file(self._search_history, 'w') as fdesc: + dump(history, fdesc) def search_menu(self): ''' Main search menu ''' + from helperobjects import TitleItem menu_items = [ TitleItem( - title=self._kodi.localize(30424), # New search... - path=self._kodi.url_for('search_query'), + title=localize(30424), # New search... + path=url_for('search_query'), art_dict=dict(thumb='DefaultAddonsSearch.png'), - info_dict=dict(plot=self._kodi.localize(30425)), + info_dict=dict(plot=localize(30425)), is_playable=False, ) ] @@ -51,30 +52,30 @@ def search_menu(self): for keywords in history: menu_items.append(TitleItem( title=keywords, - path=self._kodi.url_for('search_query', keywords=keywords), + path=url_for('search_query', keywords=keywords), art_dict=dict(thumb='DefaultAddonsSearch.png'), is_playable=False, - context_menu=[(self._kodi.localize(30030), 'RunPlugin(%s)' % self._kodi.url_for('remove_search', keywords=keywords))] + context_menu=[(localize(30030), 'RunPlugin(%s)' % url_for('remove_search', keywords=keywords))] )) if history: menu_items.append(TitleItem( - title=self._kodi.localize(30426), # Clear search history - path=self._kodi.url_for('clear_search'), - info_dict=dict(plot=self._kodi.localize(30427)), + title=localize(30426), # Clear search history + path=url_for('clear_search'), + info_dict=dict(plot=localize(30427)), art_dict=dict(thumb='icons/infodialogs/uninstall.png'), is_playable=False, )) - self._kodi.show_listing(menu_items, category=30031, cache=False) + show_listing(menu_items, category=30031, cache=False) def search(self, keywords=None, page=None): ''' The VRT NU add-on Search functionality and results ''' if keywords is None: - keywords = self._kodi.get_search_string() + keywords = get_search_string() if not keywords: - self._kodi.end_of_directory() + end_of_directory() return from statichelper import realpage @@ -83,28 +84,29 @@ def search(self, keywords=None, page=None): self.add(keywords) from apihelper import ApiHelper - search_items, sort, ascending, content = ApiHelper(self._kodi, self._favorites, self._resumepoints).list_search(keywords, page=page) + search_items, sort, ascending, content = ApiHelper(self._favorites, self._resumepoints).list_search(keywords, page=page) if not search_items: - self._kodi.show_ok_dialog(heading=self._kodi.localize(30135), message=self._kodi.localize(30136, keywords=keywords)) - self._kodi.end_of_directory() + ok_dialog(heading=localize(30135), message=localize(30136, keywords=keywords)) + end_of_directory() return # Add 'More...' entry at the end + from helperobjects import TitleItem if len(search_items) == 50: search_items.append(TitleItem( - title=self._kodi.localize(30300), - path=self._kodi.url_for('search_query', keywords=keywords, page=page + 1), + title=localize(30300), + path=url_for('search_query', keywords=keywords, page=page + 1), art_dict=dict(thumb='DefaultAddonSearch.png'), info_dict=dict(), )) self._favorites.refresh(ttl=60 * 60) - self._kodi.show_listing(search_items, category=30032, sort=sort, ascending=ascending, content=content, cache=False) + show_listing(search_items, category=30032, sort=sort, ascending=ascending, content=content, cache=False) def clear(self): ''' Clear the search history ''' self.write_history([]) - self._kodi.end_of_directory() + end_of_directory() def add(self, keywords): ''' Add new keywords to search history ''' @@ -132,4 +134,4 @@ def remove(self, keywords): # If keywords was successfully removed, write to disk self.write_history(history) - self._kodi.container_refresh() + container_refresh() diff --git a/resources/lib/service.py b/resources/lib/service.py index 548d9c42..0f270f6e 100644 --- a/resources/lib/service.py +++ b/resources/lib/service.py @@ -8,7 +8,8 @@ from xbmc import Monitor from apihelper import ApiHelper from favorites import Favorites -from kodiwrapper import KodiWrapper +from kodiutils import (addon_id, container_refresh, container_update, current_container_url, + get_setting, has_addon, invalidate_caches, jsonrpc, log, notify) from playerinfo import PlayerInfo from resumepoints import ResumePoints from statichelper import play_url_to_id, to_unicode, url_to_episode @@ -20,8 +21,7 @@ class VrtMonitor(Monitor): def __init__(self): ''' VRT Monitor initialisiation ''' - self._kodi = KodiWrapper(None) - self._resumepoints = ResumePoints(self._kodi) + self._resumepoints = ResumePoints() self._container = None self._playerinfo = None self._favorites = None @@ -42,16 +42,16 @@ def init_watching_activity(self): if not self._playerinfo: self._playerinfo = PlayerInfo(info=self.handle_info) if not self._favorites: - self._favorites = Favorites(self._kodi) + self._favorites = Favorites() if not self._apihelper: - self._apihelper = ApiHelper(self._kodi, self._favorites, self._resumepoints) + self._apihelper = ApiHelper(self._favorites, self._resumepoints) def onNotification(self, sender, method, data): # pylint: disable=invalid-name ''' Handler for notifications ''' - self._kodi.log(2, '[Notification] sender={sender}, method={method}, data={data}', sender=sender, method=method, data=to_unicode(data)) + log(2, '[Notification] sender={sender}, method={method}, data={data}', sender=sender, method=method, data=to_unicode(data)) if method.endswith('source_container'): - import json - self._container = json.loads(data).get('container') + from json import loads + self._container = loads(data).get('container') return if not sender.startswith('upnextprovider'): @@ -59,34 +59,34 @@ def onNotification(self, sender, method, data): # pylint: disable=invalid-name if not method.endswith('plugin.video.vrt.nu_play_action'): return - import json - hexdata = json.loads(data) + from json import loads + hexdata = loads(data) if not hexdata: return from binascii import unhexlify - data = json.loads(unhexlify(hexdata[0])) - self._kodi.log(2, '[Up Next notification] sender={sender}, method={method}, data={data}', sender=sender, method=method, data=to_unicode(data)) - self._kodi.jsonrpc(method='Player.Open', params=dict(item=dict(file='plugin://plugin.video.vrt.nu/play/whatson/%s' % data.get('whatson_id')))) + data = loads(unhexlify(hexdata[0])) + log(2, '[Up Next notification] sender={sender}, method={method}, data={data}', sender=sender, method=method, data=to_unicode(data)) + jsonrpc(method='Player.Open', params=dict(item=dict(file='plugin://plugin.video.vrt.nu/play/whatson/%s' % data.get('whatson_id')))) def onSettingsChanged(self): # pylint: disable=invalid-name ''' Handler for changes to settings ''' - self._kodi.log(1, 'Settings changed') - TokenResolver(self._kodi).refresh_login() + log(1, 'Settings changed') + TokenResolver().refresh_login() - self._kodi.invalidate_caches('continue-*.json', 'favorites.json', 'my-offline-*.json', 'my-recent-*.json', 'resume_points.json', 'watchlater-*.json') + invalidate_caches('continue-*.json', 'favorites.json', 'my-offline-*.json', 'my-recent-*.json', 'resume_points.json', 'watchlater-*.json') # Init watching activity again when settings change self.init_watching_activity() # Refresh container when settings change - self._kodi.container_refresh() + container_refresh() def handle_info(self, info): ''' Handle information from PlayerInfo class ''' - self._kodi.log(2, 'Got VRT NU Player info: {info}', info=str(info)) + log(2, 'Got VRT NU Player info: {info}', info=str(info)) # Push resume position if info.get('position'): @@ -114,19 +114,19 @@ def push_position(self, info): # Push resumepoint to VRT NU self._resumepoints.update(uuid=uuid, title=title, url=url, watch_later=None, position=info.get('position'), total=info.get('total')) # Only update container if the play action was initiated from it - current_container = self._kodi.current_container_url() - self._kodi.log(2, '[PlayerPosition] resumepoint update {info} {container}', info=episode.get('title'), container=current_container) + current_container = current_container_url() + log(2, '[PlayerPosition] resumepoint update {info} {container}', info=episode.get('title'), container=current_container) if current_container is None or self._container == current_container: - self._kodi.log(2, '[PlayerPosition] update container {info}', info=self._container) - self._kodi.container_update(self._container) + log(2, '[PlayerPosition] update container {info}', info=self._container) + container_update(self._container) def push_upnext(self, info): ''' Push episode info to Up Next service add-on''' - if self._kodi.has_addon('service.upnext') and self._kodi.get_setting('useupnext', 'true') == 'true': + if has_addon('service.upnext') and get_setting('useupnext', 'true') == 'true': next_info = self._apihelper.get_upnext(info) if next_info: from binascii import hexlify - import json - data = [to_unicode(hexlify(json.dumps(next_info).encode()))] - sender = '%s.SIGNAL' % self._kodi.addon_id() - self._kodi.notify(sender=sender, message='upnext_data', data=data) + from json import dumps + data = [to_unicode(hexlify(dumps(next_info).encode()))] + sender = '%s.SIGNAL' % addon_id() + notify(sender=sender, message='upnext_data', data=data) diff --git a/resources/lib/statichelper.py b/resources/lib/statichelper.py index d67b17b6..27f22611 100644 --- a/resources/lib/statichelper.py +++ b/resources/lib/statichelper.py @@ -139,16 +139,18 @@ def play_url_to_id(url): return play_id -def to_unicode(text, encoding='utf-8'): +def to_unicode(text, encoding='utf-8', errors='strict'): ''' Force text to unicode ''' - return text.decode(encoding) if isinstance(text, bytes) else text + if isinstance(text, bytes): + return text.decode(encoding, errors=errors) + return text -def from_unicode(text, encoding='utf-8'): +def from_unicode(text, encoding='utf-8', errors='strict'): ''' Force unicode to text ''' import sys if sys.version_info.major == 2 and isinstance(text, unicode): # noqa: F821; pylint: disable=undefined-variable - return text.encode(encoding) + return text.encode(encoding, errors) return text diff --git a/resources/lib/streamservice.py b/resources/lib/streamservice.py index b26f5666..04e87999 100644 --- a/resources/lib/streamservice.py +++ b/resources/lib/streamservice.py @@ -3,8 +3,6 @@ ''' This module collects and prepares stream info for Kodi Player. ''' from __future__ import absolute_import, division, unicode_literals -import json -import re try: # Python 3 from urllib.error import HTTPError @@ -15,6 +13,9 @@ from urllib2 import build_opener, install_opener, urlopen, ProxyHandler, quote, unquote, HTTPError from helperobjects import ApiData, StreamURLS +from kodiutils import (can_play_drm, exists, end_of_directory, get_max_bandwidth, get_proxies, + get_setting, get_userdata_path, has_inputstream_adaptive, kodi_version, + localize, log, log_error, mkdir, ok_dialog, open_settings, supports_drm) class StreamService: @@ -28,26 +29,26 @@ class StreamService: _INCOMPLETE_ROAMING_CONFIG = 'INCOMPLETE_ROAMING_CONFIG' _GEOBLOCK_ERROR_CODES = (_INCOMPLETE_ROAMING_CONFIG, _INVALID_LOCATION) - def __init__(self, _kodi, _tokenresolver): + def __init__(self, _tokenresolver): ''' Initialize Stream Service class ''' - self._kodi = _kodi - self._proxies = _kodi.get_proxies() - install_opener(build_opener(ProxyHandler(self._proxies))) + install_opener(build_opener(ProxyHandler(get_proxies()))) self._tokenresolver = _tokenresolver self._create_settings_dir() - self._can_play_drm = _kodi.can_play_drm() + self._can_play_drm = can_play_drm() self._vualto_license_url = None def _get_vualto_license_url(self): ''' Get Widevine license URL from Vualto API ''' - self._kodi.log(2, 'URL get: {url}', url=unquote(self._VUPLAY_API_URL)) - self._vualto_license_url = json.load(urlopen(self._VUPLAY_API_URL)).get('drm_providers', dict()).get('widevine', dict()).get('la_url') + from json import load + log(2, 'URL get: {url}', url=unquote(self._VUPLAY_API_URL)) + self._vualto_license_url = load(urlopen(self._VUPLAY_API_URL)).get('drm_providers', dict()).get('widevine', dict()).get('la_url') - def _create_settings_dir(self): + @staticmethod + def _create_settings_dir(): ''' Create settings directory ''' - settingsdir = self._kodi.get_userdata_path() - if not self._kodi.check_if_path_exists(settingsdir): - self._kodi.mkdir(settingsdir) + settingsdir = get_userdata_path() + if not exists(settingsdir): + mkdir(settingsdir) @staticmethod def _get_license_key(key_url, key_type='R', key_headers=None, key_value=None): @@ -108,7 +109,7 @@ def _get_api_data(self, video): def _webscrape_api_data(self, video_url): ''' Scrape api data from VRT NU html page ''' from bs4 import BeautifulSoup, SoupStrainer - self._kodi.log(2, 'URL get: {url}', url=unquote(video_url)) + log(2, 'URL get: {url}', url=unquote(video_url)) html_page = urlopen(video_url).read() strainer = SoupStrainer(['section', 'div'], {'class': ['video-player', 'livestream__player']}) soup = BeautifulSoup(html_page, 'html.parser', parse_only=strainer) @@ -116,12 +117,12 @@ def _webscrape_api_data(self, video_url): video_data = soup.find(lambda tag: tag.name == 'nui-media').attrs except Exception as exc: # pylint: disable=broad-except # Web scraping failed, log error - self._kodi.log_error('Web scraping api data failed: {error}', error=exc) + log_error('Web scraping api data failed: {error}', error=exc) return None # Web scraping failed, log error if not video_data: - self._kodi.log_error('Web scraping api data failed, empty video_data') + log_error('Web scraping api data failed, empty video_data') return None # Store required html data attributes @@ -138,7 +139,7 @@ def _webscrape_api_data(self, video_url): publication_id += quote('$') if client is None or media_api_url is None or (video_id is None and publication_id is None): - self._kodi.log_error('Web scraping api data failed, required attributes missing') + log_error('Web scraping api data failed, required attributes missing') return None return ApiData(client, media_api_url, video_id, publication_id, is_live_stream) @@ -154,13 +155,14 @@ def _get_stream_json(self, api_data, roaming=False): # Construct api_url and get video json stream_json = None if playertoken: + from json import load api_url = api_data.media_api_url + '/videos/' + api_data.publication_id + \ api_data.video_id + '?vrtPlayerToken=' + playertoken + '&client=' + api_data.client - self._kodi.log(2, 'URL get: {url}', url=unquote(api_url)) + log(2, 'URL get: {url}', url=unquote(api_url)) try: - stream_json = json.load(urlopen(api_url)) + stream_json = load(urlopen(api_url)) except HTTPError as exc: - stream_json = json.load(exc) + stream_json = load(exc) return stream_json @@ -198,10 +200,10 @@ def get_stream(self, video, roaming=False, api_data=None): # Roaming token failed if roaming: - message = self._kodi.localize(30964) # Geoblock error: Cannot be played, need Belgian phone number validation + message = localize(30964) # Geoblock error: Cannot be played, need Belgian phone number validation return self._handle_stream_api_error(message) # X-VRT-Token failed - message = self._kodi.localize(30963) # You need a VRT NU account to play this stream. + message = localize(30963) # You need a VRT NU account to play this stream. return self._handle_stream_api_error(message) if 'targetUrls' in stream_json: @@ -213,7 +215,7 @@ def get_stream(self, video, roaming=False, api_data=None): drm_stream = (vudrm_token or uplynk) # Select streaming protocol - if not drm_stream and self._kodi.has_inputstream_adaptive(): + if not drm_stream and has_inputstream_adaptive(): protocol = 'mpeg_dash' elif drm_stream and self._can_play_drm: protocol = 'mpeg_dash' @@ -236,7 +238,7 @@ def get_stream(self, video, roaming=False, api_data=None): # Prepare stream for Kodi player if protocol == 'mpeg_dash' and drm_stream: - self._kodi.log(2, 'Protocol: mpeg_dash drm') + log(2, 'Protocol: mpeg_dash drm') if vudrm_token: if self._vualto_license_url is None: self._get_vualto_license_url() @@ -250,10 +252,10 @@ def get_stream(self, video, roaming=False, api_data=None): stream = StreamURLS(manifest_url, license_key=license_key, use_inputstream_adaptive=True) elif protocol == 'mpeg_dash': - self._kodi.log(2, 'Protocol: mpeg_dash') + log(2, 'Protocol: mpeg_dash') stream = StreamURLS(manifest_url, use_inputstream_adaptive=True) else: - self._kodi.log(2, 'Protocol: {protocol}', protocol=protocol) + log(2, 'Protocol: {protocol}', protocol=protocol) # Fix 720p quality for HLS livestreams manifest_url += '?hd' if '.m3u8?' not in manifest_url else '&hd' stream = self._select_hls_substreams(manifest_url, protocol) @@ -261,48 +263,50 @@ def get_stream(self, video, roaming=False, api_data=None): # VRT Geoblock: failed to get stream, now try again with roaming enabled if stream_json.get('code') in self._GEOBLOCK_ERROR_CODES: - self._kodi.log(2, 'VRT Geoblock: {msg}', msg=stream_json.get('message')) + log(2, 'VRT Geoblock: {msg}', msg=stream_json.get('message')) if not roaming: return self.get_stream(video, roaming=True, api_data=api_data) if stream_json.get('code') == self._INVALID_LOCATION: - message = self._kodi.localize(30965) # Geoblock error: Blocked on your geographical location based on your IP address + message = localize(30965) # Geoblock error: Blocked on your geographical location based on your IP address return self._handle_stream_api_error(message, stream_json) - message = self._kodi.localize(30964) # Geoblock error: Cannot be played, need Belgian phone number validation + message = localize(30964) # Geoblock error: Cannot be played, need Belgian phone number validation return self._handle_stream_api_error(message, stream_json) # Failed to get stream, handle error - message = self._kodi.localize(30954) # Whoops something went wrong + message = localize(30954) # Whoops something went wrong return self._handle_stream_api_error(message, stream_json) - def _handle_stream_api_error(self, message, video_json=None): + @staticmethod + def _handle_stream_api_error(message, video_json=None): ''' Show localized stream api error messages in Kodi GUI ''' if video_json: - self._kodi.log_error(video_json.get('message')) - self._kodi.show_ok_dialog(message=message) - self._kodi.end_of_directory() + log_error(video_json.get('message')) + ok_dialog(message=message) + end_of_directory() - def _handle_bad_stream_error(self, protocol, code=None, reason=None): + @staticmethod + def _handle_bad_stream_error(protocol, code=None, reason=None): ''' Show a localized error message in Kodi GUI for a failing VRT NU stream based on protocol: hls, hls_aes, mpeg_dash) message: VRT NU stream <stream_type> problem, try again with (InputStream Adaptive) (and) (DRM) enabled/disabled: 30959=and DRM, 30960=disabled, 30961=enabled ''' # HLS AES DRM failed - if protocol == 'hls_aes' and not self._kodi.supports_drm(): - message = self._kodi.localize(30962, protocol=protocol.upper(), version=self._kodi.kodi_version()) - elif protocol == 'hls_aes' and not self._kodi.has_inputstream_adaptive() and self._kodi.get_setting('usedrm', 'true') == 'false': - message = self._kodi.localize(30958, protocol=protocol.upper(), component=self._kodi.localize(30959), state=self._kodi.localize(30961)) - elif protocol == 'hls_aes' and self._kodi.has_inputstream_adaptive(): - message = self._kodi.localize(30958, protocol=protocol.upper(), component='Widevine DRM', state=self._kodi.localize(30961)) - elif protocol == 'hls_aes' and self._kodi.get_setting('usedrm', 'true') == 'true': - message = self._kodi.localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=self._kodi.localize(30961)) + if protocol == 'hls_aes' and not supports_drm(): + message = localize(30962, protocol=protocol.upper(), version=kodi_version()) + elif protocol == 'hls_aes' and not has_inputstream_adaptive() and get_setting('usedrm', 'true') == 'false': + message = localize(30958, protocol=protocol.upper(), component=localize(30959), state=localize(30961)) + elif protocol == 'hls_aes' and has_inputstream_adaptive(): + message = localize(30958, protocol=protocol.upper(), component='Widevine DRM', state=localize(30961)) + elif protocol == 'hls_aes' and get_setting('usedrm', 'true') == 'true': + message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30961)) else: - message = self._kodi.localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=self._kodi.localize(30960)) + message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30960)) heading = 'HTTP Error %s: %s' % (code, reason) if code and reason else None - self._kodi.log_error('Unable to play stream. {error}', error=heading) - self._kodi.show_ok_dialog(heading=heading, message=message) - self._kodi.end_of_directory() + log_error('Unable to play stream. {error}', error=heading) + ok_dialog(heading=heading, message=message) + end_of_directory() def _select_hls_substreams(self, master_hls_url, protocol): ''' Select HLS substreams to speed up Kodi player start, workaround for slower kodi selection ''' @@ -311,7 +315,7 @@ def _select_hls_substreams(self, master_hls_url, protocol): hls_audio_id = None hls_subtitle_id = None hls_base_url = master_hls_url.split('.m3u8')[0] - self._kodi.log(2, 'URL get: {url}', url=unquote(master_hls_url)) + log(2, 'URL get: {url}', url=unquote(master_hls_url)) try: hls_playlist = urlopen(master_hls_url).read().decode('utf-8') except HTTPError as exc: @@ -319,10 +323,11 @@ def _select_hls_substreams(self, master_hls_url, protocol): self._handle_bad_stream_error(protocol, exc.code, exc.reason) return None raise - max_bandwidth = self._kodi.get_max_bandwidth() + max_bandwidth = get_max_bandwidth() stream_bandwidth = None # Get hls variant url based on max_bandwith setting + import re hls_variant_regex = re.compile(r'#EXT-X-STREAM-INF:[\w\-.,=\"]*?BANDWIDTH=(?P<BANDWIDTH>\d+),' r'[\w\-.,=\"]+\d,(?:AUDIO=\"(?P<AUDIO>[\w\-]+)\",)?(?:SUBTITLES=\"' r'(?P<SUBTITLES>\w+)\",)?[\w\-.,=\"]+?[\r\n](?P<URI>[\w:\/\-.=?&]+)') @@ -339,9 +344,9 @@ def _select_hls_substreams(self, master_hls_url, protocol): break if stream_bandwidth > max_bandwidth and not hls_variant_url: - message = self._kodi.localize(30057, max=max_bandwidth, min=stream_bandwidth) - self._kodi.show_ok_dialog(message=message) - self._kodi.open_settings() + message = localize(30057, max=max_bandwidth, min=stream_bandwidth) + ok_dialog(message=message) + open_settings() # Get audio url if hls_audio_id: @@ -352,7 +357,7 @@ def _select_hls_substreams(self, master_hls_url, protocol): hls_variant_url = hls_base_url + match_audio.group('AUDIO_URI') + '-' + hls_variant_url.split('-')[-1] # Get subtitle url, works only for on demand streams - if self._kodi.get_setting('showsubtitles', 'true') == 'true' and '/live/' not in master_hls_url and hls_subtitle_id: + if get_setting('showsubtitles', 'true') == 'true' and '/live/' not in master_hls_url and hls_subtitle_id: subtitle_regex = re.compile(r'#EXT-X-MEDIA:TYPE=SUBTITLES[\w\-=,\.\"\/]+?GROUP-ID=\"' + hls_subtitle_id + '' r'\"[\w\-=,\.\"\/]+URI=\"(?P<SUBTITLE_URI>[\w\-=]+)\.m3u8\"') match_subtitle = re.search(subtitle_regex, hls_playlist) diff --git a/resources/lib/tokenresolver.py b/resources/lib/tokenresolver.py index 2d6e0304..79e59b6e 100644 --- a/resources/lib/tokenresolver.py +++ b/resources/lib/tokenresolver.py @@ -3,8 +3,11 @@ ''' This module contains all functionality for VRT NU API authentication. ''' from __future__ import absolute_import, division, unicode_literals -import json from statichelper import from_unicode +from kodiutils import (delete, exists, get_proxies, get_setting, get_tokens_path, + get_userdata_path, has_credentials, invalidate_caches, listdir, localize, log, + log_error, mkdir, notification, ok_dialog, open_file, open_settings, + set_setting) try: # Python 3 import http.cookiejar as cookielib @@ -35,10 +38,9 @@ class TokenResolver: _USER_TOKEN_GATEWAY_URL = 'https://token.vrt.be/vrtnuinitlogin?provider=site&destination=https://www.vrt.be/vrtnu/' _ROAMING_TOKEN_GATEWAY_URL = 'https://token.vrt.be/vrtnuinitloginEU?destination=https://www.vrt.be/vrtnu/' - def __init__(self, _kodi): + def __init__(self): ''' Initialize Token Resolver class ''' - self._kodi = _kodi - self._proxies = _kodi.get_proxies() + self._proxies = get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) def get_playertoken(self, token_url, token_variant=None, roaming=False): @@ -49,7 +51,7 @@ def get_playertoken(self, token_url, token_variant=None, roaming=False): xvrttoken_variant = 'roaming' # Delete cached playertoken path = self._get_token_path('vrtPlayerToken', token_variant) - self._kodi.delete_file(path) + delete(path) else: token = self._get_cached_token('vrtPlayerToken', token_variant) @@ -81,49 +83,53 @@ def get_xvrttoken(self, token_variant=None): token = self.login(token_variant=token_variant) return token - def _get_token_path(self, token_name, token_variant): + @staticmethod + def _get_token_path(token_name, token_variant): ''' Create token path following predefined file naming rules ''' prefix = token_variant + '_' if token_variant else '' - token_path = self._kodi.get_tokens_path() + prefix + token_name.replace('-', '') + '.tkn' + token_path = get_tokens_path() + prefix + token_name.replace('-', '') + '.tkn' return token_path def _get_cached_token(self, token_name, token_variant=None): ''' Return a cached token ''' + from json import load cached_token = None path = self._get_token_path(token_name, token_variant) - if self._kodi.check_if_path_exists(path): + if exists(path): from datetime import datetime import dateutil.parser import dateutil.tz - with self._kodi.open_file(path) as fdesc: - token = json.load(fdesc) + with open_file(path) as fdesc: + token = load(fdesc) now = datetime.now(dateutil.tz.tzlocal()) exp = dateutil.parser.parse(token.get('expirationDate')) if exp > now: - self._kodi.log(3, "Got cached token '{path}'", path=path) + log(3, "Got cached token '{path}'", path=path) cached_token = token.get(token_name) else: - self._kodi.log(2, "Cached token '{path}' deleted", path=path) - self._kodi.delete_file(path) + log(2, "Cached token '{path}' deleted", path=path) + delete(path) return cached_token def _set_cached_token(self, token, token_variant=None): ''' Save token to cache''' + from json import dump token_name = list(token.keys())[0] path = self._get_token_path(token_name, token_variant) - if not self._kodi.check_if_path_exists(self._kodi.get_tokens_path()): - self._kodi.mkdir(self._kodi.get_tokens_path()) + if not exists(get_tokens_path()): + mkdir(get_tokens_path()) - with self._kodi.open_file(path, 'w') as fdesc: - json.dump(token, fdesc) + with open_file(path, 'w') as fdesc: + dump(token, fdesc) def _get_new_playertoken(self, token_url, headers, token_variant=None): ''' Get new playertoken from VRT Token API ''' - self._kodi.log(2, 'URL post: {url}', url=unquote(token_url)) + from json import load + log(2, 'URL post: {url}', url=unquote(token_url)) req = Request(token_url, data=b'', headers=headers) - playertoken = json.load(urlopen(req)) + playertoken = load(urlopen(req)) if playertoken is not None: self._set_cached_token(playertoken, token_variant) return playertoken.get('vrtPlayerToken') @@ -131,10 +137,10 @@ def _get_new_playertoken(self, token_url, headers, token_variant=None): def login(self, refresh=False, token_variant=None): ''' Kodi GUI login flow ''' # If no credentials, ask user for credentials - if not self._kodi.credentials_filled_in(): + if not has_credentials(): if refresh: - return self._kodi.open_settings() - self._kodi.open_settings() + return open_settings() + open_settings() if not self._credentials_changed(): return None @@ -145,17 +151,17 @@ def login(self, refresh=False, token_variant=None): while login_json.get('errorCode') != 0: # Show localized login error messages in Kodi GUI message = login_json.get('errorDetails') - self._kodi.log_error('Login failed: {msg}', msg=message) + log_error('Login failed: {msg}', msg=message) if message == 'invalid loginID or password': - message = self._kodi.localize(30953) # Invalid login! + message = localize(30953) # Invalid login! elif message == 'loginID must be provided': - message = self._kodi.localize(30955) # Please fill in username + message = localize(30955) # Please fill in username elif message == 'Missing required parameter: password': - message = self._kodi.localize(30956) # Please fill in password - self._kodi.show_ok_dialog(heading=self._kodi.localize(30951), message=message) # Login failed! + message = localize(30956) # Please fill in password + ok_dialog(heading=localize(30951), message=message) # Login failed! if refresh: - return self._kodi.open_settings() - self._kodi.open_settings() + return open_settings() + open_settings() if not self._credentials_changed(): return None login_json = self._get_login_json() @@ -166,17 +172,18 @@ def login(self, refresh=False, token_variant=None): def _get_login_json(self): ''' Get login json ''' + from json import load payload = dict( - loginID=from_unicode(self._kodi.get_setting('username')), - password=from_unicode(self._kodi.get_setting('password')), + loginID=from_unicode(get_setting('username')), + password=from_unicode(get_setting('password')), sessionExpiration='-1', APIKey=self._API_KEY, targetEnv='jssdk', ) data = urlencode(payload).encode() - self._kodi.log(2, 'URL post: {url}', url=unquote(self._LOGIN_URL)) + log(2, 'URL post: {url}', url=unquote(self._LOGIN_URL)) req = Request(self._LOGIN_URL, data=data) - login_json = json.load(urlopen(req)) + login_json = load(urlopen(req)) return login_json def _get_new_xvrttoken(self, login_json, token_variant=None): @@ -184,16 +191,17 @@ def _get_new_xvrttoken(self, login_json, token_variant=None): token = None login_token = login_json.get('sessionInfo', dict()).get('login_token') if login_token: + from json import dumps login_cookie = 'glt_%s=%s' % (self._API_KEY, login_token) payload = dict( uid=login_json.get('UID'), uidsig=login_json.get('UIDSignature'), ts=login_json.get('signatureTimestamp'), - email=from_unicode(self._kodi.get_setting('username')), + email=from_unicode(get_setting('username')), ) - data = json.dumps(payload).encode() + data = dumps(payload).encode() headers = {'Content-Type': 'application/json', 'Cookie': login_cookie} - self._kodi.log(2, 'URL post: {url}', url=unquote(self._TOKEN_GATEWAY_URL)) + log(2, 'URL post: {url}', url=unquote(self._TOKEN_GATEWAY_URL)) req = Request(self._TOKEN_GATEWAY_URL, data=data, headers=headers) try: # Python 3 setcookie_header = urlopen(req).info().get('Set-Cookie') @@ -205,7 +213,7 @@ def _get_new_xvrttoken(self, login_json, token_variant=None): if xvrttoken is not None: token = xvrttoken.get('X-VRT-Token') self._set_cached_token(xvrttoken, token_variant) - self._kodi.show_notification(message=self._kodi.localize(30952)) # Login succeeded. + notification(message=localize(30952)) # Login succeeded. return token def _get_new_user_xvrttoken(self): @@ -228,9 +236,9 @@ def _get_new_user_xvrttoken(self): data = urlencode(payload).encode() cookiejar = cookielib.CookieJar() opener = build_opener(HTTPCookieProcessor(cookiejar), ProxyHandler(self._proxies)) - self._kodi.log(2, 'URL get: {url}', url=unquote(self._USER_TOKEN_GATEWAY_URL)) + log(2, 'URL get: {url}', url=unquote(self._USER_TOKEN_GATEWAY_URL)) opener.open(self._USER_TOKEN_GATEWAY_URL) - self._kodi.log(2, 'URL post: {url}', url=unquote(self._VRT_LOGIN_URL)) + log(2, 'URL post: {url}', url=unquote(self._VRT_LOGIN_URL)) opener.open(self._VRT_LOGIN_URL, data=data) xvrttoken = TokenResolver._create_token_dictionary(cookiejar) refreshtoken = TokenResolver._create_token_dictionary(cookiejar, cookie_name='vrtlogin-rt') @@ -249,7 +257,7 @@ def _get_fresh_token(self, refresh_token, token_name, token_variant=None): headers = {'Cookie': cookie_value} cookiejar = cookielib.CookieJar() opener = build_opener(HTTPCookieProcessor(cookiejar), ProxyHandler(self._proxies)) - self._kodi.log(2, 'URL get: {url}', url=refresh_url) + log(2, 'URL get: {url}', url=refresh_url) req = Request(refresh_url, headers=headers) opener.open(req) token = TokenResolver._create_token_dictionary(cookiejar, token_name) @@ -264,7 +272,7 @@ def _get_roaming_xvrttoken(self, xvrttoken): cookie_value = 'X-VRT-Token=' + xvrttoken.get('X-VRT-Token') headers = {'Cookie': cookie_value} opener = build_opener(NoRedirection, ProxyHandler(self._proxies)) - self._kodi.log(2, 'URL get: {url}', url=unquote(self._ROAMING_TOKEN_GATEWAY_URL)) + log(2, 'URL get: {url}', url=unquote(self._ROAMING_TOKEN_GATEWAY_URL)) req = Request(self._ROAMING_TOKEN_GATEWAY_URL, headers=headers) req_info = opener.open(req).info() try: # Python 3 @@ -273,14 +281,14 @@ def _get_roaming_xvrttoken(self, xvrttoken): except AttributeError: # Python 2 cookie_value += '; state=' + req_info.getheader('Set-Cookie').split('state=')[1].split('; ')[0] url = req_info.getheader('Location') - self._kodi.log(2, 'URL get: {url}', url=unquote(url)) + log(2, 'URL get: {url}', url=unquote(url)) try: # Python 3 url = opener.open(url).info().get('Location') except AttributeError: # Python 2 url = opener.open(url).info().getheader('Location') headers = {'Cookie': cookie_value} if url is not None: - self._kodi.log(2, 'URL get: {url}', url=unquote(url)) + log(2, 'URL get: {url}', url=unquote(url)) req = Request(url, headers=headers) try: # Python 3 setcookie_header = opener.open(req).info().get('Set-Cookie') @@ -312,30 +320,31 @@ def _create_token_dictionary(cookie_data, cookie_name='X-VRT-Token'): } return token_dictionary - def delete_tokens(self): + @staticmethod + def delete_tokens(): ''' Delete all cached tokens ''' # Remove old tokens # FIXME: Deprecate and simplify this part in a future version - dirs, files = self._kodi.listdir(self._kodi.get_userdata_path()) # pylint: disable=unused-variable + dirs, files = listdir(get_userdata_path()) # pylint: disable=unused-variable token_files = [item for item in files if item.endswith('.tkn')] # Empty userdata/tokens/ directory - if self._kodi.check_if_path_exists(self._kodi.get_tokens_path()): - dirs, files = self._kodi.listdir(self._kodi.get_tokens_path()) # pylint: disable=unused-variable + if exists(get_tokens_path()): + dirs, files = listdir(get_tokens_path()) # pylint: disable=unused-variable token_files += ['tokens/' + item for item in files] if token_files: for item in token_files: - self._kodi.delete_file(self._kodi.get_userdata_path() + item) - self._kodi.show_notification(message=self._kodi.localize(30985)) + delete(get_userdata_path() + item) + notification(message=localize(30985)) def refresh_login(self): ''' Refresh login if necessary ''' - if self._credentials_changed() and self._kodi.credentials_filled_in(): - self._kodi.log(2, 'Credentials have changed, cleaning up userdata') + if self._credentials_changed() and has_credentials(): + log(2, 'Credentials have changed, cleaning up userdata') self.cleanup_userdata() # Refresh login - self._kodi.log(2, 'Refresh login') + log(2, 'Refresh login') self.login(refresh=True) def cleanup_userdata(self): @@ -345,21 +354,23 @@ def cleanup_userdata(self): self.delete_tokens() # Delete user-related caches - self._kodi.invalidate_caches('continue-*.json', 'favorites.json', 'my-offline-*.json', 'my-recent-*.json', 'resume_points.json', 'watchlater-*.json') + invalidate_caches('continue-*.json', 'favorites.json', 'my-offline-*.json', 'my-recent-*.json', 'resume_points.json', 'watchlater-*.json') def logged_in(self): ''' Whether there is an active login ''' return bool(self._get_cached_token('X-VRT-Token')) - def _credentials_changed(self): + @staticmethod + def _credentials_changed(): ''' Check if credentials have changed ''' - old_hash = self._kodi.get_setting('credentials_hash') - username = self._kodi.get_setting('username') - password = self._kodi.get_setting('password') + old_hash = get_setting('credentials_hash') + username = get_setting('username') + password = get_setting('password') new_hash = '' if username or password: - new_hash = self._kodi.md5((username + password).encode('utf-8')).hexdigest() + from hashlib import md5 + new_hash = md5((username + password).encode('utf-8')).hexdigest() if new_hash != old_hash: - self._kodi.set_setting('credentials_hash', new_hash) + set_setting('credentials_hash', new_hash) return True return False diff --git a/resources/lib/tvguide.py b/resources/lib/tvguide.py index 9f24b223..35dfc13b 100644 --- a/resources/lib/tvguide.py +++ b/resources/lib/tvguide.py @@ -4,7 +4,6 @@ ''' Implements a VRT NU TV guide ''' from __future__ import absolute_import, division, unicode_literals -import json from datetime import datetime, timedelta import dateutil.parser import dateutil.tz @@ -20,6 +19,8 @@ from metadata import Metadata from resumepoints import ResumePoints from statichelper import find_entry +from kodiutils import (get_cache, get_proxies, has_addon, localize, localize_datelong, log, + show_listing, update_cache, url_for) class TVGuide: @@ -27,42 +28,40 @@ class TVGuide: VRT_TVGUIDE = 'https://www.vrt.be/bin/epg/schedule.%Y-%m-%d.json' - def __init__(self, _kodi): + def __init__(self): ''' Initializes TV-guide object ''' - self._kodi = _kodi - self._favorites = Favorites(_kodi) - self._resumepoints = ResumePoints(_kodi) - self._metadata = Metadata(self._kodi, self._favorites, self._resumepoints) - - self._proxies = _kodi.get_proxies() - install_opener(build_opener(ProxyHandler(self._proxies))) + self._favorites = Favorites() + self._resumepoints = ResumePoints() + self._metadata = Metadata(self._favorites, self._resumepoints) + install_opener(build_opener(ProxyHandler(get_proxies()))) def show_tvguide(self, date=None, channel=None): ''' Offer a menu depending on the information provided ''' if not date and not channel: date_items = self.get_date_items() - self._kodi.show_listing(date_items, category=30026, content='files') # TV guide + show_listing(date_items, category=30026, content='files') # TV guide elif not channel: channel_items = self.get_channel_items(date=date) entry = find_entry(RELATIVE_DATES, 'id', date) - date_name = self._kodi.localize(entry.get('msgctxt')) if entry else date - self._kodi.show_listing(channel_items, category=date_name) + date_name = localize(entry.get('msgctxt')) if entry else date + show_listing(channel_items, category=date_name) elif not date: date_items = self.get_date_items(channel=channel) channel_name = find_entry(CHANNELS, 'name', channel).get('label') - self._kodi.show_listing(date_items, category=channel_name, content='files', selected=7) + show_listing(date_items, category=channel_name, content='files', selected=7) else: episode_items = self.get_episode_items(date, channel) channel_name = find_entry(CHANNELS, 'name', channel).get('label') entry = find_entry(RELATIVE_DATES, 'id', date) - date_name = self._kodi.localize(entry.get('msgctxt')) if entry else date - self._kodi.show_listing(episode_items, category='%s / %s' % (channel_name, date_name), content='episodes', cache=False) + date_name = localize(entry.get('msgctxt')) if entry else date + show_listing(episode_items, category='%s / %s' % (channel_name, date_name), content='episodes', cache=False) - def get_date_items(self, channel=None): + @staticmethod + def get_date_items(channel=None): ''' Offer a menu to select the TV-guide date ''' epg = datetime.now(dateutil.tz.tzlocal()) @@ -72,13 +71,13 @@ def get_date_items(self, channel=None): date_items = [] for offset in range(7, -30, -1): day = epg + timedelta(days=offset) - title = self._kodi.localize_datelong(day) + title = localize_datelong(day) date = day.strftime('%Y-%m-%d') # Highlight today with context of 2 days entry = find_entry(RELATIVE_DATES, 'offset', offset) if entry: - date_name = self._kodi.localize(entry.get('msgctxt')) + date_name = localize(entry.get('msgctxt')) if entry.get('permalink'): date = entry.get('id') if offset == 0: @@ -88,17 +87,17 @@ def get_date_items(self, channel=None): # Show channel list or channel episodes if channel: - path = self._kodi.url_for('tvguide', date=date, channel=channel) + path = url_for('tvguide', date=date, channel=channel) else: - path = self._kodi.url_for('tvguide', date=date) + path = url_for('tvguide', date=date) cache_file = 'schedule.%s.json' % date date_items.append(TitleItem( title=title, path=path, art_dict=dict(thumb='DefaultYear.png'), - info_dict=dict(plot=self._kodi.localize_datelong(day)), - context_menu=[(self._kodi.localize(30413), 'RunPlugin(%s)' % self._kodi.url_for('delete_cache', cache_file=cache_file))], + info_dict=dict(plot=localize_datelong(day)), + context_menu=[(localize(30413), 'RunPlugin(%s)' % url_for('delete_cache', cache_file=cache_file))], )) return date_items @@ -107,7 +106,7 @@ def get_channel_items(self, date=None, channel=None): if date: now = datetime.now(dateutil.tz.tzlocal()) epg = self.parse(date, now) - datelong = self._kodi.localize_datelong(epg) + datelong = localize_datelong(epg) channel_items = [] for chan in CHANNELS: @@ -122,19 +121,19 @@ def get_channel_items(self, date=None, channel=None): art_dict = {} # Try to use the white icons for thumbnails (used for icons as well) - if self._kodi.get_cond_visibility('System.HasAddon(resource.images.studios.white)') == 1: + if has_addon('resource.images.studios.white'): art_dict['thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format(**chan) else: art_dict['thumb'] = 'DefaultTags.png' if date: title = chan.get('label') - path = self._kodi.url_for('tvguide', date=date, channel=chan.get('name')) - plot = '%s\n%s' % (self._kodi.localize(30302, **chan), datelong) + path = url_for('tvguide', date=date, channel=chan.get('name')) + plot = '%s\n%s' % (localize(30302, **chan), datelong) else: - title = '[B]%s[/B]' % self._kodi.localize(30303, **chan) - path = self._kodi.url_for('tvguide_channel', channel=chan.get('name')) - plot = '%s\n\n%s' % (self._kodi.localize(30302, **chan), self.live_description(chan.get('name'))) + title = '[B]%s[/B]' % localize(30303, **chan) + path = url_for('tvguide_channel', channel=chan.get('name')) + plot = '%s\n\n%s' % (localize(30302, **chan), self.live_description(chan.get('name'))) channel_items.append(TitleItem( title=title, @@ -155,14 +154,16 @@ def get_episode_items(self, date, channel): cache_file = 'schedule.%s.json' % date if date in ('today', 'yesterday', 'tomorrow'): # Try the cache if it is fresh - schedule = self._kodi.get_cache(cache_file, ttl=60 * 60) + schedule = get_cache(cache_file, ttl=60 * 60) if not schedule: - self._kodi.log(2, 'URL get: {url}', url=epg_url) - schedule = json.load(urlopen(epg_url)) - self._kodi.update_cache(cache_file, schedule) + from json import load + log(2, 'URL get: {url}', url=epg_url) + schedule = load(urlopen(epg_url)) + update_cache(cache_file, schedule) else: - self._kodi.log(2, 'URL get: {url}', url=epg_url) - schedule = json.load(urlopen(epg_url)) + from json import load + log(2, 'URL get: {url}', url=epg_url) + schedule = load(urlopen(epg_url)) entry = find_entry(CHANNELS, 'name', channel) if entry: @@ -179,7 +180,7 @@ def get_episode_items(self, date, channel): if episode.get('url'): from statichelper import add_https_method, url_to_program video_url = add_https_method(episode.get('url')) - path = self._kodi.url_for('play_url', video_url=video_url) + path = url_for('play_url', video_url=video_url) program = url_to_program(episode.get('url')) context_menu, favorite_marker, watchlater_marker = self._metadata.get_context_menu(episode, program, cache_file) label += favorite_marker + watchlater_marker @@ -205,12 +206,13 @@ def playing_now(self, channel): if epg.hour < 6: epg += timedelta(days=-1) # Try the cache if it is fresh - schedule = self._kodi.get_cache('schedule.today.json', ttl=60 * 60) + schedule = get_cache('schedule.today.json', ttl=60 * 60) if not schedule: + from json import load epg_url = epg.strftime(self.VRT_TVGUIDE) - self._kodi.log(2, 'URL get: {url}', url=epg_url) - schedule = json.load(urlopen(epg_url)) - self._kodi.update_cache('schedule.today.json', schedule) + log(2, 'URL get: {url}', url=epg_url) + schedule = load(urlopen(epg_url)) + update_cache('schedule.today.json', schedule) entry = find_entry(CHANNELS, 'name', channel) if not entry: @@ -242,12 +244,13 @@ def live_description(self, channel): if epg.hour < 6: epg += timedelta(days=-1) # Try the cache if it is fresh - schedule = self._kodi.get_cache('schedule.today.json', ttl=60 * 60) + schedule = get_cache('schedule.today.json', ttl=60 * 60) if not schedule: + from json import load epg_url = epg.strftime(self.VRT_TVGUIDE) - self._kodi.log(2, 'URL get: {url}', url=epg_url) - schedule = json.load(urlopen(epg_url)) - self._kodi.update_cache('schedule.today.json', schedule) + log(2, 'URL get: {url}', url=epg_url) + schedule = load(urlopen(epg_url)) + update_cache('schedule.today.json', schedule) entry = find_entry(CHANNELS, 'name', channel) if not entry: @@ -264,22 +267,22 @@ def live_description(self, channel): start_date = dateutil.parser.parse(episode.get('startTime')) end_date = dateutil.parser.parse(episode.get('endTime')) if start_date <= now <= end_date: # Now playing - description = '[COLOR yellow][B]%s[/B] %s[/COLOR]\n' % (self._kodi.localize(30421), self.episode_description(episode)) + description = '[COLOR yellow][B]%s[/B] %s[/COLOR]\n' % (localize(30421), self.episode_description(episode)) try: - description += '[B]%s[/B] %s' % (self._kodi.localize(30422), self.episode_description(next(episodes))) + description += '[B]%s[/B] %s' % (localize(30422), self.episode_description(next(episodes))) except StopIteration: break break if now < start_date: # Nothing playing now, but this may be next - description = '[B]%s[/B] %s\n' % (self._kodi.localize(30422), self.episode_description(episode)) + description = '[B]%s[/B] %s\n' % (localize(30422), self.episode_description(episode)) try: - description += '[B]%s[/B] %s' % (self._kodi.localize(30422), self.episode_description(next(episodes))) + description += '[B]%s[/B] %s' % (localize(30422), self.episode_description(next(episodes))) except StopIteration: break break if not description: # Add a final 'No transmission' program - description = '[COLOR yellow][B]%s[/B] %s - 06:00\n» %s[/COLOR]' % (self._kodi.localize(30421), episode.get('end'), self._kodi.localize(30423)) + description = '[COLOR yellow][B]%s[/B] %s - 06:00\n» %s[/COLOR]' % (localize(30421), episode.get('end'), localize(30423)) return description @staticmethod diff --git a/resources/lib/vrtplayer.py b/resources/lib/vrtplayer.py index 4e1785e5..1881eaca 100644 --- a/resources/lib/vrtplayer.py +++ b/resources/lib/vrtplayer.py @@ -8,17 +8,19 @@ from helperobjects import TitleItem from resumepoints import ResumePoints from statichelper import find_entry +from kodiutils import (addon_id, current_container_url, delete_cached_thumbnail, end_of_directory, + get_addon_info, get_setting, has_credentials, localize, log_error, notify, + ok_dialog, play, set_setting, show_listing, url_for) class VRTPlayer: ''' An object providing all methods for Kodi menu generation ''' - def __init__(self, _kodi): + def __init__(self): ''' Initialise object ''' - self._kodi = _kodi - self._favorites = Favorites(_kodi) - self._resumepoints = ResumePoints(_kodi) - self._apihelper = ApiHelper(_kodi, self._favorites, self._resumepoints) + self._favorites = Favorites() + self._resumepoints = ResumePoints() + self._apihelper = ApiHelper(self._favorites, self._resumepoints) def show_main_menu(self): ''' The VRT NU add-on main menu ''' @@ -28,51 +30,51 @@ def show_main_menu(self): # Only add 'My favorites' when it has been activated if self._favorites.is_activated(): main_items.append(TitleItem( - title=self._kodi.localize(30010), # My favorites - path=self._kodi.url_for('favorites_menu'), + title=localize(30010), # My favorites + path=url_for('favorites_menu'), art_dict=dict(thumb='DefaultFavourites.png'), - info_dict=dict(plot=self._kodi.localize(30011)), + info_dict=dict(plot=localize(30011)), )) main_items.extend([ - TitleItem(title=self._kodi.localize(30012), # All programs - path=self._kodi.url_for('programs'), + TitleItem(title=localize(30012), # All programs + path=url_for('programs'), art_dict=dict(thumb='DefaultMovieTitle.png'), - info_dict=dict(plot=self._kodi.localize(30013))), - TitleItem(title=self._kodi.localize(30014), # Categories - path=self._kodi.url_for('categories'), + info_dict=dict(plot=localize(30013))), + TitleItem(title=localize(30014), # Categories + path=url_for('categories'), art_dict=dict(thumb='DefaultGenre.png'), - info_dict=dict(plot=self._kodi.localize(30015))), - TitleItem(title=self._kodi.localize(30016), # Channels - path=self._kodi.url_for('channels'), + info_dict=dict(plot=localize(30015))), + TitleItem(title=localize(30016), # Channels + path=url_for('channels'), art_dict=dict(thumb='DefaultTags.png'), - info_dict=dict(plot=self._kodi.localize(30017))), - TitleItem(title=self._kodi.localize(30018), # Live TV - path=self._kodi.url_for('livetv'), + info_dict=dict(plot=localize(30017))), + TitleItem(title=localize(30018), # Live TV + path=url_for('livetv'), art_dict=dict(thumb='DefaultTVShows.png'), - info_dict=dict(plot=self._kodi.localize(30019))), - TitleItem(title=self._kodi.localize(30020), # Recent items - path=self._kodi.url_for('recent'), + info_dict=dict(plot=localize(30019))), + TitleItem(title=localize(30020), # Recent items + path=url_for('recent'), art_dict=dict(thumb='DefaultRecentlyAddedEpisodes.png'), - info_dict=dict(plot=self._kodi.localize(30021))), - TitleItem(title=self._kodi.localize(30022), # Soon offline - path=self._kodi.url_for('offline'), + info_dict=dict(plot=localize(30021))), + TitleItem(title=localize(30022), # Soon offline + path=url_for('offline'), art_dict=dict(thumb='DefaultYear.png'), - info_dict=dict(plot=self._kodi.localize(30023))), - TitleItem(title=self._kodi.localize(30024), # Featured content - path=self._kodi.url_for('featured'), + info_dict=dict(plot=localize(30023))), + TitleItem(title=localize(30024), # Featured content + path=url_for('featured'), art_dict=dict(thumb='DefaultCountry.png'), - info_dict=dict(plot=self._kodi.localize(30025))), - TitleItem(title=self._kodi.localize(30026), # TV guide - path=self._kodi.url_for('tvguide'), + info_dict=dict(plot=localize(30025))), + TitleItem(title=localize(30026), # TV guide + path=url_for('tvguide'), art_dict=dict(thumb='DefaultAddonTvInfo.png'), - info_dict=dict(plot=self._kodi.localize(30027))), - TitleItem(title=self._kodi.localize(30028), # Search - path=self._kodi.url_for('search'), + info_dict=dict(plot=localize(30027))), + TitleItem(title=localize(30028), # Search + path=url_for('search'), art_dict=dict(thumb='DefaultAddonsSearch.png'), - info_dict=dict(plot=self._kodi.localize(30029))), + info_dict=dict(plot=localize(30029))), ]) - self._kodi.show_listing(main_items, cache=False) # No category + show_listing(main_items, cache=False) # No category self._version_check() def _version_check(self): @@ -80,30 +82,31 @@ def _version_check(self): if first_run: # 2.2.3 version: max_log_level to be an integer try: - int(self._kodi.get_setting('max_log_level', 0)) # May return string + int(get_setting('max_log_level', 0)) # May return string except ValueError: - self._kodi.set_setting('max_log_level', 0) + set_setting('max_log_level', 0) # 2.0.0 version: changed plugin:// url interface: show warning that Kodi favourites and what-was-watched will break - if settings_version == '' and self._kodi.credentials_filled_in(): - self._kodi.show_ok_dialog(self._kodi.localize(30978), self._kodi.localize(30979)) + if settings_version == '' and has_credentials(): + ok_dialog(localize(30978), localize(30979)) if addon_version == '2.2.1': # 2.2.1 version: changed artwork: delete old cached artwork - self._kodi.delete_cached_thumbnail(self._kodi.get_addon_info('fanart').replace('.png', '.jpg')) - self._kodi.delete_cached_thumbnail(self._kodi.get_addon_info('icon')) + delete_cached_thumbnail(get_addon_info('fanart').replace('.png', '.jpg')) + delete_cached_thumbnail(get_addon_info('icon')) # 2.2.1 version: moved tokens: delete old tokens from tokenresolver import TokenResolver - TokenResolver(self._kodi).delete_tokens() + TokenResolver().delete_tokens() - def _first_run(self): + @staticmethod + def _first_run(): '''Check if this add-on version is run for the first time''' # Get version from settings.xml - settings_version = self._kodi.get_setting('version', '') + settings_version = get_setting('version', '') # Get version from addon.xml - addon_version = self._kodi.get_addon_info('version') + addon_version = get_addon_info('version') # Compare versions (settings_version was not present in version 1.10.0 and older) settings_comp = tuple(map(int, settings_version.split('.'))) if settings_version != '' else (1, 10, 0) @@ -111,7 +114,7 @@ def _first_run(self): if addon_comp > settings_comp: # New version found, save addon version to settings - self._kodi.set_setting('version', addon_version) + set_setting('version', addon_version) return True, settings_version, addon_version return False, settings_version, addon_version @@ -120,63 +123,63 @@ def show_favorites_menu(self): ''' The VRT NU addon 'My programs' menu ''' self._favorites.refresh(ttl=60 * 60) favorites_items = [ - TitleItem(title=self._kodi.localize(30040), # My programs - path=self._kodi.url_for('favorites_programs'), + TitleItem(title=localize(30040), # My programs + path=url_for('favorites_programs'), art_dict=dict(thumb='DefaultMovieTitle.png'), - info_dict=dict(plot=self._kodi.localize(30041))), - TitleItem(title=self._kodi.localize(30046), # My recent items - path=self._kodi.url_for('favorites_recent'), + info_dict=dict(plot=localize(30041))), + TitleItem(title=localize(30046), # My recent items + path=url_for('favorites_recent'), art_dict=dict(thumb='DefaultRecentlyAddedEpisodes.png'), - info_dict=dict(plot=self._kodi.localize(30047))), - TitleItem(title=self._kodi.localize(30048), # My soon offline - path=self._kodi.url_for('favorites_offline'), + info_dict=dict(plot=localize(30047))), + TitleItem(title=localize(30048), # My soon offline + path=url_for('favorites_offline'), art_dict=dict(thumb='DefaultYear.png'), - info_dict=dict(plot=self._kodi.localize(30049))), + info_dict=dict(plot=localize(30049))), ] # Only add 'My watch later' and 'Continue watching' when it has been activated if self._resumepoints.is_activated(): favorites_items.append(TitleItem( - title=self._kodi.localize(30050), # My watch later - path=self._kodi.url_for('resumepoints_watchlater'), + title=localize(30050), # My watch later + path=url_for('resumepoints_watchlater'), art_dict=dict(thumb='DefaultVideoPlaylists.png'), - info_dict=dict(plot=self._kodi.localize(30051)), + info_dict=dict(plot=localize(30051)), )) favorites_items.append(TitleItem( - title=self._kodi.localize(30052), # Continue Watching - path=self._kodi.url_for('resumepoints_continue'), + title=localize(30052), # Continue Watching + path=url_for('resumepoints_continue'), art_dict=dict(thumb='DefaultInProgressShows.png'), - info_dict=dict(plot=self._kodi.localize(30053)), + info_dict=dict(plot=localize(30053)), )) - if self._kodi.get_setting('addmymovies', 'true') == 'true': + if get_setting('addmymovies', 'true') == 'true': favorites_items.append( - TitleItem(title=self._kodi.localize(30042), # My movies - path=self._kodi.url_for('categories', category='films'), + TitleItem(title=localize(30042), # My movies + path=url_for('categories', category='films'), art_dict=dict(thumb='DefaultAddonVideo.png'), - info_dict=dict(plot=self._kodi.localize(30043))), + info_dict=dict(plot=localize(30043))), ) - if self._kodi.get_setting('addmydocu', 'true') == 'true': + if get_setting('addmydocu', 'true') == 'true': favorites_items.append( - TitleItem(title=self._kodi.localize(30044), # My documentaries - path=self._kodi.url_for('favorites_docu'), + TitleItem(title=localize(30044), # My documentaries + path=url_for('favorites_docu'), art_dict=dict(thumb='DefaultMovies.png'), - info_dict=dict(plot=self._kodi.localize(30045))), + info_dict=dict(plot=localize(30045))), ) - self._kodi.show_listing(favorites_items, category=30010, cache=False) # My favorites + show_listing(favorites_items, category=30010, cache=False) # My favorites # Show dialog when no favorites were found if not self._favorites.titles(): - self._kodi.show_ok_dialog(heading=self._kodi.localize(30415), message=self._kodi.localize(30416)) + ok_dialog(heading=localize(30415), message=localize(30416)) def show_favorites_docu_menu(self): ''' The VRT NU add-on 'My documentaries' listing menu ''' self._favorites.refresh(ttl=60 * 60) self._resumepoints.refresh(ttl=60 * 60) episode_items, sort, ascending, content = self._apihelper.list_episodes(category='docu', season='allseasons', programtype='oneoff') - self._kodi.show_listing(episode_items, category=30044, sort=sort, ascending=ascending, content=content, cache=False) + show_listing(episode_items, category=30044, sort=sort, ascending=ascending, content=content, cache=False) def show_tvshow_menu(self, use_favorites=False): ''' The VRT NU add-on 'All programs' listing menu ''' @@ -184,7 +187,7 @@ def show_tvshow_menu(self, use_favorites=False): self._favorites.refresh(ttl=5 * 60 if use_favorites else 60 * 60) self._resumepoints.refresh(ttl=5 * 60 if use_favorites else 60 * 60) tvshow_items = self._apihelper.list_tvshows(use_favorites=use_favorites) - self._kodi.show_listing(tvshow_items, category=30440, sort='label', content='tvshows') # A-Z + show_listing(tvshow_items, category=30440, sort='label', content='tvshows') # A-Z def show_category_menu(self, category=None): ''' The VRT NU add-on 'Categories' listing menu ''' @@ -194,10 +197,10 @@ def show_category_menu(self, category=None): tvshow_items = self._apihelper.list_tvshows(category=category) from data import CATEGORIES category_msgctxt = find_entry(CATEGORIES, 'id', category).get('msgctxt') - self._kodi.show_listing(tvshow_items, category=category_msgctxt, sort='label', content='tvshows') + show_listing(tvshow_items, category=category_msgctxt, sort='label', content='tvshows') else: category_items = self._apihelper.list_categories() - self._kodi.show_listing(category_items, category=30014, sort='unsorted', content='files') # Categories + show_listing(category_items, category=30014, sort='unsorted', content='files') # Categories def show_channels_menu(self, channel=None): ''' The VRT NU add-on 'Channels' listing menu ''' @@ -206,15 +209,15 @@ def show_channels_menu(self, channel=None): self._favorites.refresh(ttl=60 * 60) self._resumepoints.refresh(ttl=60 * 60) channel_items = self._apihelper.list_channels(channels=[channel]) # Live TV - channel_items.extend(TVGuide(self._kodi).get_channel_items(channel=channel)) # TV guide + channel_items.extend(TVGuide().get_channel_items(channel=channel)) # TV guide channel_items.extend(self._apihelper.list_youtube(channels=[channel])) # YouTube channel_items.extend(self._apihelper.list_tvshows(channel=channel)) # TV shows from data import CHANNELS channel_name = find_entry(CHANNELS, 'name', channel).get('label') - self._kodi.show_listing(channel_items, category=channel_name, sort='unsorted', content='tvshows', cache=False) # Channel + show_listing(channel_items, category=channel_name, sort='unsorted', content='tvshows', cache=False) # Channel else: channel_items = self._apihelper.list_channels(live=False) - self._kodi.show_listing(channel_items, category=30016, cache=False) + show_listing(channel_items, category=30016, cache=False) def show_featured_menu(self, feature=None): ''' The VRT NU add-on 'Featured content' listing menu ''' @@ -224,15 +227,15 @@ def show_featured_menu(self, feature=None): tvshow_items = self._apihelper.list_tvshows(feature=feature) from data import FEATURED feature_msgctxt = find_entry(FEATURED, 'id', feature).get('msgctxt') - self._kodi.show_listing(tvshow_items, category=feature_msgctxt, sort='label', content='tvshows', cache=False) + show_listing(tvshow_items, category=feature_msgctxt, sort='label', content='tvshows', cache=False) else: featured_items = self._apihelper.list_featured() - self._kodi.show_listing(featured_items, category=30024, sort='label', content='files') + show_listing(featured_items, category=30024, sort='label', content='files') def show_livetv_menu(self): ''' The VRT NU add-on 'Live TV' listing menu ''' channel_items = self._apihelper.list_channels() - self._kodi.show_listing(channel_items, category=30018, cache=False) + show_listing(channel_items, category=30018, cache=False) def show_episodes_menu(self, program, season=None): ''' The VRT NU add-on episodes listing menu ''' @@ -240,7 +243,7 @@ def show_episodes_menu(self, program, season=None): self._resumepoints.refresh(ttl=60 * 60) episode_items, sort, ascending, content = self._apihelper.list_episodes(program=program, season=season) # FIXME: Translate program in Program Title - self._kodi.show_listing(episode_items, category=program.title(), sort=sort, ascending=ascending, content=content, cache=False) + show_listing(episode_items, category=program.title(), sort=sort, ascending=ascending, content=content, cache=False) def show_recent_menu(self, page=0, use_favorites=False): ''' The VRT NU add-on 'Most recent' and 'My most recent' listing menu ''' @@ -259,13 +262,13 @@ def show_recent_menu(self, page=0, use_favorites=False): else: recent = 'recent' episode_items.append(TitleItem( - title=self._kodi.localize(30300), - path=self._kodi.url_for(recent, page=page + 1), + title=localize(30300), + path=url_for(recent, page=page + 1), art_dict=dict(thumb='DefaultRecentlyAddedEpisodes.png'), info_dict=dict(), )) - self._kodi.show_listing(episode_items, category=30020, sort=sort, ascending=ascending, content=content, cache=False) + show_listing(episode_items, category=30020, sort=sort, ascending=ascending, content=content, cache=False) def show_offline_menu(self, page=0, use_favorites=False): ''' The VRT NU add-on 'Soon offline' and 'My soon offline' listing menu ''' @@ -284,13 +287,13 @@ def show_offline_menu(self, page=0, use_favorites=False): else: offline = 'offline' episode_items.append(TitleItem( - title=self._kodi.localize(30300), - path=self._kodi.url_for(offline, page=page + 1), + title=localize(30300), + path=url_for(offline, page=page + 1), art_dict=dict(thumb='DefaultYear.png'), info_dict=dict(), )) - self._kodi.show_listing(episode_items, category=30022, sort=sort, ascending=ascending, content=content, cache=False) + show_listing(episode_items, category=30022, sort=sort, ascending=ascending, content=content, cache=False) def show_watchlater_menu(self, page=0): ''' The VRT NU add-on 'My watch later' listing menu ''' @@ -301,7 +304,7 @@ def show_watchlater_menu(self, page=0): self._resumepoints.refresh(ttl=5 * 60) page = realpage(page) episode_items, sort, ascending, content = self._apihelper.list_episodes(page=page, variety='watchlater') - self._kodi.show_listing(episode_items, category=30050, sort=sort, ascending=ascending, content=content, cache=False) + show_listing(episode_items, category=30050, sort=sort, ascending=ascending, content=content, cache=False) def show_continue_menu(self, page=0): ''' The VRT NU add-on 'Continue waching' listing menu ''' @@ -312,15 +315,15 @@ def show_continue_menu(self, page=0): self._resumepoints.refresh(ttl=5 * 60) page = realpage(page) episode_items, sort, ascending, content = self._apihelper.list_episodes(page=page, variety='continue') - self._kodi.show_listing(episode_items, category=30052, sort=sort, ascending=ascending, content=content, cache=False) + show_listing(episode_items, category=30052, sort=sort, ascending=ascending, content=content, cache=False) def play_latest_episode(self, program): ''' A hidden feature in the VRT NU add-on to play the latest episode of a program ''' video = self._apihelper.get_latest_episode(program) if not video: - self._kodi.log_error('Play latest episode failed, program {program}', program=program) - self._kodi.show_ok_dialog(message=self._kodi.localize(30954)) - self._kodi.end_of_directory() + log_error('Play latest episode failed, program {program}', program=program) + ok_dialog(message=localize(30954)) + end_of_directory() return self.play(video) @@ -328,13 +331,13 @@ def play_episode_by_air_date(self, channel, start_date, end_date): ''' Play an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00) ''' video = self._apihelper.get_episode_by_air_date(channel, start_date, end_date) if video and video.get('errorlabel'): - self._kodi.show_ok_dialog(message=self._kodi.localize(30986, title=video.get('errorlabel'))) - self._kodi.end_of_directory() + ok_dialog(message=localize(30986, title=video.get('errorlabel'))) + end_of_directory() return if not video: - self._kodi.log_error('Play episode by air date failed, channel {channel}, start_date {start}', channel=channel, start=start_date) - self._kodi.show_ok_dialog(message=self._kodi.localize(30954)) - self._kodi.end_of_directory() + log_error('Play episode by air date failed, channel {channel}, start_date {start}', channel=channel, start=start_date) + ok_dialog(message=localize(30954)) + end_of_directory() return self.play(video) @@ -342,22 +345,23 @@ def play_whatson(self, whatson_id): ''' Play a video by whatson_id ''' video = self._apihelper.get_single_episode(whatson_id) if not video: - self._kodi.log_error('Play by whatson_id {id} failed', id=whatson_id) - self._kodi.show_ok_dialog(message=self._kodi.localize(30954)) - self._kodi.end_of_directory() + log_error('Play by whatson_id {id} failed', id=whatson_id) + ok_dialog(message=localize(30954)) + end_of_directory() return self.play(video) - def play(self, video): + @staticmethod + def play(video): ''' A wrapper for playing video items ''' from tokenresolver import TokenResolver from streamservice import StreamService - _tokenresolver = TokenResolver(self._kodi) - _streamservice = StreamService(self._kodi, _tokenresolver) + _tokenresolver = TokenResolver() + _streamservice = StreamService(_tokenresolver) stream = _streamservice.get_stream(video) if stream is None: return - self._kodi.play(stream, video.get('listitem')) - source_container = self._kodi.current_container_url() + play(stream, video.get('listitem')) + source_container = current_container_url() if source_container: - self._kodi.notify(sender=self._kodi.addon_id(), message='source_container', data=dict(container=self._kodi.current_container_url())) + notify(sender=addon_id(), message='source_container', data=dict(container=current_container_url())) diff --git a/test/test_apihelper.py b/test/test_apihelper.py index ba769c37..4405b828 100644 --- a/test/test_apihelper.py +++ b/test/test_apihelper.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -from addon import kodi from apihelper import ApiHelper from data import CHANNELS from favorites import Favorites @@ -21,9 +20,9 @@ class ApiHelperTests(unittest.TestCase): - _favorites = Favorites(kodi) - _resumepoints = ResumePoints(kodi) - _apihelper = ApiHelper(kodi, _favorites, _resumepoints) + _favorites = Favorites() + _resumepoints = ResumePoints() + _apihelper = ApiHelper(_favorites, _resumepoints) def test_get_api_data_single_season(self): title_items, sort, ascending, content = self._apihelper.list_episodes(program='het-journaal') diff --git a/test/test_favorites.py b/test/test_favorites.py index afd2f5e6..a2083724 100644 --- a/test/test_favorites.py +++ b/test/test_favorites.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest from random import shuffle -from addon import kodi from apihelper import ApiHelper from favorites import Favorites from resumepoints import ResumePoints @@ -24,9 +23,9 @@ class TestFavorites(unittest.TestCase): - _favorites = Favorites(kodi) - _resumepoints = ResumePoints(kodi) - _apihelper = ApiHelper(kodi, _favorites, _resumepoints) + _favorites = Favorites() + _resumepoints = ResumePoints() + _apihelper = ApiHelper(_favorites, _resumepoints) def test_get_recent_episodes(self): ''' Test items, sort and order ''' diff --git a/test/test_kodi.py b/test/test_kodi.py index 8adfa7bc..5ec80cfa 100644 --- a/test/test_kodi.py +++ b/test/test_kodi.py @@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -from kodiwrapper import KodiWrapper +from kodiutils import localize xbmc = __import__('xbmc') xbmcaddon = __import__('xbmcaddon') @@ -16,18 +16,16 @@ class KodiTests(unittest.TestCase): - _kodi = KodiWrapper(None) - def test_localize(self): - msg = self._kodi.localize(30958) + msg = localize(30958) #self.assertEqual(msg, "There is a problem with this VRT NU {protocol} stream. Try again with {component} {state} or try to play this program from the VRT NU website. Please report this problem at https://www.vrt.be/vrtnu/help/") # noqa self.assertEqual(msg, "Er is een probleem met deze VRT NU {protocol}-stream. Probeer het opnieuw met {component} {state} of probeer dit programma af te spelen vanaf de VRT NU-website. Meld dit probleem op https://www.vrt.be/vrtnu/help/") # noqa - msg = self._kodi.localize(30958, component='Widevine DRM', state='enabled') + msg = localize(30958, component='Widevine DRM', state='enabled') #self.assertEqual(msg, "There is a problem with this VRT NU {protocol} stream. Try again with Widevine DRM enabled or try to play this program from the VRT NU website. Please report this problem at https://www.vrt.be/vrtnu/help/") # noqa self.assertEqual(msg, "Er is een probleem met deze VRT NU {protocol}-stream. Probeer het opnieuw met Widevine DRM enabled of probeer dit programma af te spelen vanaf de VRT NU-website. Meld dit probleem op https://www.vrt.be/vrtnu/help/") # noqa - msg = self._kodi.localize(30958, protocol='MPEG-DASH', component='Widevine DRM', state='enabled') + msg = localize(30958, protocol='MPEG-DASH', component='Widevine DRM', state='enabled') #self.assertEqual(msg, "There is a problem with this VRT NU MPEG-DASH stream. Try again with Widevine DRM enabled or try to play this program from the VRT NU website. Please report this problem at https://www.vrt.be/vrtnu/help/") # noqa self.assertEqual(msg, "Er is een probleem met deze VRT NU MPEG-DASH-stream. Probeer het opnieuw met Widevine DRM enabled of probeer dit programma af te spelen vanaf de VRT NU-website. Meld dit probleem op https://www.vrt.be/vrtnu/help/") # noqa diff --git a/test/test_resumepoints.py b/test/test_resumepoints.py index b25c4bde..58edbc45 100644 --- a/test/test_resumepoints.py +++ b/test/test_resumepoints.py @@ -6,7 +6,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -from addon import kodi from apihelper import ApiHelper from favorites import Favorites from resumepoints import ResumePoints @@ -23,9 +22,9 @@ class TestResumePoints(unittest.TestCase): - _favorites = Favorites(kodi) - _resumepoints = ResumePoints(kodi) - _apihelper = ApiHelper(kodi, _favorites, _resumepoints) + _favorites = Favorites() + _resumepoints = ResumePoints() + _apihelper = ApiHelper(_favorites, _resumepoints) @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') diff --git a/test/test_search.py b/test/test_search.py index c98cc00d..ee71cd0d 100644 --- a/test/test_search.py +++ b/test/test_search.py @@ -6,7 +6,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -from addon import kodi from apihelper import ApiHelper from favorites import Favorites from resumepoints import ResumePoints @@ -20,9 +19,9 @@ class TestSearch(unittest.TestCase): - _favorites = Favorites(kodi) - _resumepoints = ResumePoints(kodi) - _apihelper = ApiHelper(kodi, _favorites, _resumepoints) + _favorites = Favorites() + _resumepoints = ResumePoints() + _apihelper = ApiHelper(_favorites, _resumepoints) def test_search_journaal(self): ''' Test for journaal ''' diff --git a/test/test_streamservice.py b/test/test_streamservice.py index 3fa0cd42..97dbcdf4 100644 --- a/test/test_streamservice.py +++ b/test/test_streamservice.py @@ -14,7 +14,6 @@ except ImportError: from urllib2 import HTTPError -from addon import kodi from data import CHANNELS from streamservice import StreamService from tokenresolver import TokenResolver @@ -34,8 +33,8 @@ class StreamServiceTests(unittest.TestCase): - _tokenresolver = TokenResolver(kodi) - _streamservice = StreamService(kodi, _tokenresolver) + _tokenresolver = TokenResolver() + _streamservice = StreamService(_tokenresolver) @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') diff --git a/test/test_tokenresolver.py b/test/test_tokenresolver.py index ea8eeba2..1816fea4 100644 --- a/test/test_tokenresolver.py +++ b/test/test_tokenresolver.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -from addon import kodi from tokenresolver import TokenResolver xbmc = __import__('xbmc') @@ -17,7 +16,7 @@ class TokenResolverTests(unittest.TestCase): - _tokenresolver = TokenResolver(kodi) + _tokenresolver = TokenResolver() def test_refresh_login(self): self._tokenresolver.refresh_login() diff --git a/test/test_tvguide.py b/test/test_tvguide.py index 4004cd69..f382d983 100644 --- a/test/test_tvguide.py +++ b/test/test_tvguide.py @@ -11,7 +11,6 @@ import dateutil.tz from xbmcextra import kodi_to_ansi, uri_to_path -from addon import kodi from tvguide import TVGuide xbmc = __import__('xbmc') @@ -25,7 +24,7 @@ class TestTVGuide(unittest.TestCase): - _tvguide = TVGuide(kodi) + _tvguide = TVGuide() def test_tvguide_date_menu(self): ''' Test TV guide main menu ''' diff --git a/test/test_vrtplayer.py b/test/test_vrtplayer.py index 3d97fe34..e1453cf3 100644 --- a/test/test_vrtplayer.py +++ b/test/test_vrtplayer.py @@ -6,7 +6,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals import random import unittest -from addon import kodi from apihelper import ApiHelper from data import CATEGORIES from favorites import Favorites @@ -22,10 +21,10 @@ class TestVRTPlayer(unittest.TestCase): - _favorites = Favorites(kodi) - _resumepoints = ResumePoints(kodi) - _apihelper = ApiHelper(kodi, _favorites, _resumepoints) - _vrtplayer = VRTPlayer(kodi) + _favorites = Favorites() + _resumepoints = ResumePoints() + _apihelper = ApiHelper(_favorites, _resumepoints) + _vrtplayer = VRTPlayer() def test_show_videos_single_episode_shows_videos(self): program = 'marathonradio'