From b9296e699ad0cf21c2e69d2afdf777e11b273e8a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:34:23 +1100 Subject: [PATCH 001/141] Fix error with subtitle selection prompt --- resources/lib/youtube_plugin/youtube/helper/subtitles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index b6e077985..2286d2c3d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -166,9 +166,9 @@ def _prompt(self): translations = [(track.get('languageCode'), self._get_language_name(track)) for track in self.translation_langs] languages = tracks + translations if languages: - choice = self.context.get_ui().on_select(self.context.localize(30560), [language_name for language, language_name in languages]) + choice = self.context.get_ui().on_select(self.context.localize(30560), [language for _, language in languages]) if choice != -1: - return self._get(language=languages[choice][0], language_name=languages[choice][1]) + return self._get(lang_code=languages[choice][0], language=languages[choice][1]) self.context.log_debug('Subtitle selection cancelled') return [] self.context.log_debug('No subtitles found for prompt') From 6acd239e4236381df07b749dc65ad66d62f5cf20 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:40:02 +1100 Subject: [PATCH 002/141] Misc minor fixes --- .../lib/youtube_plugin/kodion/impl/abstract_context.py | 2 +- .../youtube_plugin/kodion/impl/abstract_context_ui.py | 4 ++-- .../lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py | 1 - .../lib/youtube_plugin/kodion/json_store/__init__.py | 1 - .../youtube/helper/signature/__init__.py | 2 -- .../lib/youtube_plugin/youtube/helper/url_resolver.py | 2 +- resources/lib/youtube_plugin/youtube/helper/utils.py | 2 +- resources/lib/youtube_plugin/youtube/helper/v3.py | 8 ++++---- resources/lib/youtube_plugin/youtube/provider.py | 10 +++++----- resources/lib/youtube_requests.py | 2 +- 10 files changed, 15 insertions(+), 19 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py index 99d1cb702..6cc2541cf 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py @@ -54,7 +54,7 @@ def format_time(self, time_obj): def get_language(self): raise NotImplementedError() - def get_language_name(self): + def get_language_name(self, lang_id=None): raise NotImplementedError() def get_region(self): diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py index 7ed3feffe..eb7419174 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py @@ -25,7 +25,7 @@ def on_keyboard_input(self, title, default='', hidden=False): def on_numeric_input(self, title, default=''): raise NotImplementedError() - def on_yes_no_input(self, title, text): + def on_yes_no_input(self, title, text, nolabel='', yeslabel=''): raise NotImplementedError() def on_ok(self, title, text): @@ -40,7 +40,7 @@ def on_select(self, title, items=None): def open_settings(self): raise NotImplementedError() - def show_notification(self, message, header='', image_uri='', time_milliseconds=5000): + def show_notification(self, message, header='', image_uri='', time_milliseconds=5000, audible=True): raise NotImplementedError() @staticmethod diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py index c1341a023..5f9e601d2 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py @@ -35,7 +35,6 @@ def set_info(self, *args, **kwargs): def to_play_item(context, play_item): context.log_debug('Converting PlayItem |%s|' % play_item.get_uri()) - is_strm = str(context.get_param('strm', False)).lower() == 'true' thumb = play_item.get_image() if play_item.get_image() else u'DefaultVideo.png' diff --git a/resources/lib/youtube_plugin/kodion/json_store/__init__.py b/resources/lib/youtube_plugin/kodion/json_store/__init__.py index d4ff961a5..278ded74a 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/__init__.py +++ b/resources/lib/youtube_plugin/kodion/json_store/__init__.py @@ -12,4 +12,3 @@ from .login_tokens import LoginTokenStore __all__ = ['JSONStore', 'APIKeyStore', 'LoginTokenStore'] - diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py index a5d8eca2f..a1da71f71 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py @@ -11,5 +11,3 @@ from ....youtube.helper.signature.cipher import Cipher __all__ = ['Cipher'] - - diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index 2d09a4139..1b4ea4ff0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -144,7 +144,7 @@ def _loop(_url, tries=5): _next_query = parse_qs(_nc.query) # query string encoded inside next_url del _query['next_url'] # remove next_url from top level query string _next_query.update(_query) # add/overwrite all other params from top level query string - _next_query = dict(map(lambda kv : (kv[0], kv[1][0]), _next_query.items())) # flatten to only use first argument of each param + _next_query = dict(map(lambda kv: (kv[0], kv[1][0]), _next_query.items())) # flatten to only use first argument of each param _next_url = urlunsplit((_nc.scheme, _nc.netloc, _nc.path, urlencode(_next_query), _nc.fragment)) # build new URL from these components return _next_url diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 734e0a12f..386b4b7a0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -50,7 +50,7 @@ def make_comment_item(context, provider, snippet, uri, total_replies=0): label_props = None plot_props = None is_edited = (snippet['publishedAt'] != snippet['updatedAt']) - + str_likes = ('%.1fK' % (snippet['likeCount'] / 1000.0)) if snippet['likeCount'] > 1000 else str(snippet['likeCount']) str_replies = ('%.1fK' % (total_replies / 1000.0)) if total_replies > 1000 else str(total_replies) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index b0427088f..403cfc5b4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -157,7 +157,7 @@ def _process_list_response(provider, context, json_data): video_item.set_track_number(snippet['position'] + 1) result.append(video_item) video_id_dict[video_id] = video_item - + elif kind == 'activity': snippet = yt_item['snippet'] details = yt_item['contentDetails'] @@ -186,7 +186,7 @@ def _process_list_response(provider, context, json_data): video_item.set_fanart(provider.get_fanart(context)) result.append(video_item) video_id_dict[video_id] = video_item - + elif kind == 'commentthread': thread_snippet = yt_item['snippet'] total_replies = thread_snippet['totalReplyCount'] @@ -197,10 +197,10 @@ def _process_list_response(provider, context, json_data): else: item_uri = '' result.append(utils.make_comment_item(context, provider, snippet, item_uri, total_replies)) - + elif kind == 'comment': result.append(utils.make_comment_item(context, provider, yt_item['snippet'], uri='')) - + elif kind == 'searchresult': _, kind = _parse_kind(yt_item.get('id', {})) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index bdab66582..9fe12991d 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -689,9 +689,9 @@ def _on_my_location(self, context, re_match): path for playlist: '/play/?playlist_id=XXXXXXX&mode=[OPTION]' OPTION: [normal(default)|reverse|shuffle] - + path for channel live streams: '/play/?channel_id=UCXXXXXXX&live=X - OPTION: + OPTION: live parameter required, live=1 for first live stream live = index of live stream if channel has multiple live streams """ @@ -758,7 +758,7 @@ def on_play(self, context, re_match): context.log_debug('Redirecting playback, handle is -1') context.execute(builtin % context.create_uri(['play'], {'video_id': params['video_id']})) return - + if 'playlist_id' in params and (context.get_handle() != -1): builtin = 'RunPlugin(%s)' stream_url = context.create_uri(['play'], params) @@ -1375,12 +1375,12 @@ def on_root(self, context, re_match): if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions.show', True): # my subscription - + #clear cache cache = context.get_data_cache() cache_items_key = 'my-subscriptions-items' cache.set(cache_items_key, '[]') - + my_subscriptions_item = DirectoryItem( context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.my_subscriptions'])), context.create_uri(['special', 'new_uploaded_videos_tv']), diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index cf062d196..5f8c3831a 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -16,7 +16,7 @@ def __get_core_components(addon_id=None): """ :param addon_id: addon id associated with developer keys to use for requests - :return: addon provider, context and client + :return: addon provider, context and client """ provider = Provider() if addon_id is not None: From cd43d3271001fcb6ab98e585fa131906e338008c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:50:32 +1100 Subject: [PATCH 003/141] Updates to allow backports for Kodi 18 --- .../kodion/impl/xbmc/xbmc_items.py | 4 +-- .../lib/youtube_plugin/kodion/utils/ip_api.py | 4 +-- .../youtube_plugin/youtube/client/youtube.py | 2 +- .../youtube/helper/ratebypass/ratebypass.py | 6 ++-- .../youtube/helper/video_info.py | 29 ++++++++++--------- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py index 5f9e601d2..45beda6df 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py @@ -13,8 +13,8 @@ try: from infotagger.listitem import ListItemInfoTag except ImportError: - class ListItemInfoTag: - __slots__ = ('__li__', '__type__' ) + class ListItemInfoTag(object): + __slots__ = ('__li__', '__type__') def __init__(self, list_item, tag_type): self.__li__ = list_item diff --git a/resources/lib/youtube_plugin/kodion/utils/ip_api.py b/resources/lib/youtube_plugin/kodion/utils/ip_api.py index afb3a7595..dc371483c 100644 --- a/resources/lib/youtube_plugin/kodion/utils/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/utils/ip_api.py @@ -10,11 +10,11 @@ import requests -class Locator: +class Locator(object): def __init__(self, context): self._base_url = 'http://ip-api.com' - self._response = dict() + self._response = {} self._context = context def response(self): diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 3657cef02..41e7eb3ca 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1066,7 +1066,7 @@ def _request(self, url, method='GET', error_msg or 'Request failed', traceback.format_exc() )) if raise_error: - raise YouTubeException(error_msg) from error + raise YouTubeException(error_msg) return None return result diff --git a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py index baa8669be..b3632a734 100644 --- a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py +++ b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py @@ -14,7 +14,7 @@ try: from ....kodion import logger except: - class logger: + class logger(object): @staticmethod def log_debug(txt): print(txt) @@ -24,7 +24,7 @@ def throttling_reverse(arr): """Reverses the input list. Needs to do an in-place reversal so that the passed list gets changed. To accomplish this, we create a reversed copy, and then change each - indvidual element. + individual element. """ reverse_copy = arr[::-1] for i in range(len(reverse_copy)): @@ -221,7 +221,7 @@ def throttling_splice(d, e): js_splice(d, e, 1) -class CalculateN: +class CalculateN(object): # References: # https://github.com/ytdl-org/youtube-dl/issues/29326#issuecomment-894619419 # https://github.com/pytube/pytube/blob/fc9aec5c35829f2ebb4ef8dd599b14a666850d20/pytube/cipher.py diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 8bdbc6e63..e64701c07 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -873,28 +873,27 @@ def _generate_cpn(): cpn_alphabet = ('abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' '0123456789-_') - # Python 2 compatible method - # cpn = ''.join(cpn_alphabet[random.randint(0, 63)] for _ in range(16)) - # return cpn - return ''.join(random.choices(cpn_alphabet, k=16)) + return ''.join(random.choice(cpn_alphabet) for _ in range(16)) def load_stream_infos(self, video_id): self.video_id = video_id return self._get_video_info() def _build_client(self, client_name, auth_header=False): - def _merge_dicts(item1, item2): + def _merge_dicts(item1, item2, _=Ellipsis): if not isinstance(item1, dict) or not isinstance(item2, dict): - return item1 if item2 is ... else item2 + return item1 if item2 is _ else item2 new = {} - for key in (item1.keys() | item2.keys()): - value = _merge_dicts(item1.get(key, ...), item2.get(key, ...)) - if value is ...: + keys = set(item1) + keys.update(item2) + for key in keys: + value = _merge_dicts(item1.get(key, _), item2.get(key, _)) + if value is _: continue if isinstance(value, str) and '{' in value: _format['{0}.{1}'.format(id(new), key)] = (new, key, value) new[key] = value - return new or ... + return new or _ _format = {} client = (self.CLIENTS.get(client_name) or self.CLIENTS['web']).copy() @@ -934,7 +933,7 @@ def _request(self, url, method='GET', error_msg or 'Request failed', traceback.format_exc() )) if raise_error: - raise YouTubeException(error_msg) from error + raise YouTubeException(error_msg) return None return result @@ -1321,7 +1320,7 @@ def _get_video_info(self): client = self._build_client(client_name, auth_header) result = self._request( - video_info_url, 'POST', **client, + video_info_url, 'POST', error_msg=( 'Player response failed for video_id: {0},' ' using {1} client ({2})' @@ -1329,7 +1328,8 @@ def _get_video_info(self): client_name, 'logged in' if auth_header else 'logged out') ), - raise_error=True + raise_error=True, + **client ) response = result.json() @@ -1422,9 +1422,10 @@ def _get_video_info(self): captions['headers'] = client['headers'] elif client.get('_query_subtitles'): result = self._request( - video_info_url, 'POST', **self._build_client('smarttv', True), + video_info_url, 'POST', error_msg=('Caption request failed to get player response for' 'video_id: {0}'.format(self.video_id)), + **self._build_client('smarttv', True) ) response = result.json() From 995a4ee068f7b34010ce817e4d30b82bb18c170c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 15 Oct 2023 01:05:22 +1100 Subject: [PATCH 004/141] Update client details --- .../youtube/helper/video_info.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index e64701c07..f0158f909 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -576,6 +576,7 @@ class VideoInfo(object): '_id': 30, '_query_subtitles': True, 'json': { + 'params': '2AMBCgIQBg', 'context': { 'client': { 'clientName': 'ANDROID_TESTSUITE', @@ -603,7 +604,7 @@ class VideoInfo(object): 'android': { '_id': 3, 'json': { - 'params': 'CgIQBg==', + 'params': '2AMBCgIQBg', 'context': { 'client': { 'clientName': 'ANDROID', @@ -633,6 +634,7 @@ class VideoInfo(object): 'android_embedded': { '_id': 55, 'json': { + 'params': '2AMBCgIQBg', 'context': { 'client': { 'clientName': 'ANDROID_EMBEDDED_PLAYER', @@ -668,6 +670,7 @@ class VideoInfo(object): '_id': 29, '_query_subtitles': True, 'json': { + 'params': '2AMBCgIQBg', 'context': { 'client': { 'clientName': 'ANDROID_UNPLUGGED', @@ -722,15 +725,20 @@ class VideoInfo(object): }, # Used to requests captions for clients that don't provide them # Requires handling of nsig to overcome throttling (TODO) - 'smarttv': { - '_id': 75, + 'smarttv_embedded': { + '_id': 85, 'json': { + 'params': '2AMBCgIQBg', 'context': { 'client': { - 'clientName': 'TVHTML5_SIMPLY', - 'clientVersion': '1.0', + 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', + 'clientScreen': 'WATCH', + 'clientVersion': '2.0', }, }, + 'thirdParty': { + 'embedUrl': 'https://www.youtube.com', + }, }, # Headers from a 2022 Samsung Tizen 6.5 based Smart TV 'headers': { @@ -1234,8 +1242,7 @@ def _process_url_params(self, url): new_query = {} update_url = False - if (self._calculate_n and 'n' in query - and query.get('ratebypass', [None])[0] != 'yes'): + if self._calculate_n and 'n' in query: self._player_js = self._player_js or self._get_player_js() if self._calculate_n is True: self._context.log_debug('nsig detected') @@ -1397,8 +1404,7 @@ def _get_video_info(self): self._context.log_debug( 'Retrieved video info for video_id: {0}, using {1} client ({2})' - .format(self.video_id, - client['json']['context']['client']['clientName'], + .format(self.video_id, client_name, 'logged in' if auth_header else 'logged out') ) self._selected_client = client.copy() @@ -1425,7 +1431,7 @@ def _get_video_info(self): video_info_url, 'POST', error_msg=('Caption request failed to get player response for' 'video_id: {0}'.format(self.video_id)), - **self._build_client('smarttv', True) + **self._build_client('smarttv_embedded', True) ) response = result.json() From 0d8e1abfb5a8f5f45dc0e48cf4b566cb09539d90 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 17 Oct 2023 01:08:24 +1100 Subject: [PATCH 005/141] Fix stream quality comparison to quality selections Iterating over dict in insertion order only available in Python 3.7+ --- .../lib/youtube_plugin/kodion/impl/abstract_settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index 47ec7ed2e..f9594e1ca 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -242,7 +242,10 @@ def get_mpd_video_qualities(self): if not self.use_mpd_videos(): return [] selected = self.get_int(SETTINGS.MPD_QUALITY_SELECTION, 4) - return [quality for key, quality in self._QUALITY_SELECTIONS.items() + return [quality + for key, quality in sorted( + self._QUALITY_SELECTIONS.items(), reverse=True + ) if selected >= key] def stream_features(self): From 2b82ca1637fbaac7661a066f910b9f17174dd3e0 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 21 Oct 2023 07:46:07 +1100 Subject: [PATCH 006/141] Fix inputstream.adaptive dependency for matrix.unofficial --- .github/workflows/make-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 4f515947b..13b2dcc29 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -115,6 +115,7 @@ jobs: version=$(xmlstarlet sel -t -v 'string(/addon/@version)' addon.xml) xmlstarlet ed -L -u '/addon/@version' -v "${version}+matrix.unofficial.1" addon.xml xmlstarlet ed -L -u '/addon/requires/import[@addon="xbmc.python"]/@version' -v '3.0.0' addon.xml + xmlstarlet ed -L -u '/addon/requires/import[@addon="inputstream.adaptive"]/@version' -v '19.0.7' addon.xml xmlstarlet ed -L -d '/addon/requires/import[@addon="script.module.infotagger"]' addon.xml filename=${{ github.event.repository.name }}-${version}.matrix.unofficial.1.zip cd .. From a565b44513059c88f63dc7994b1dfbde94b9989a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 21 Oct 2023 07:52:41 +1100 Subject: [PATCH 007/141] Test workaround for some MPD live stream manifests --- resources/lib/youtube_plugin/youtube/helper/video_info.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index f0158f909..e3ac3d70c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1577,8 +1577,12 @@ def _get_video_info(self): # MPD structure has segments with additional attributes # and url has changed from using a query string to using url params # This breaks the InputStream.Adaptive partial manifest update - video_stream['url'] = ('{0}?start_seq=$START_NUMBER$' - .format(video_stream['url'])) + if '?' in manifest_url: + video_stream['url'] = manifest_url + '&mpd_version=5' + elif manifest_url.endswith('/'): + video_stream['url'] = manifest_url + 'mpd_version/5' + else: + video_stream['url'] = manifest_url + '/mpd_version/5' details = self.FORMAT.get('9998') else: details = self.FORMAT.get('9999').copy() From e5b69b430438d23bc2ff0cc24632b92f27598011 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 27 Oct 2023 02:58:11 +1100 Subject: [PATCH 008/141] Debug test --- resources/lib/default.py | 8 +- resources/lib/youtube_plugin/kodion/debug.py | 169 +++++++++++++++++- resources/lib/youtube_plugin/kodion/logger.py | 2 +- 3 files changed, 172 insertions(+), 7 deletions(-) diff --git a/resources/lib/default.py b/resources/lib/default.py index 10385f513..bbc54e2c9 100644 --- a/resources/lib/default.py +++ b/resources/lib/default.py @@ -8,8 +8,14 @@ See LICENSES/GPL-2.0-only for more information. """ -from youtube_plugin.kodion import runner +from xbmc import log + from youtube_plugin import youtube +from youtube_plugin.kodion import runner +from youtube_plugin.kodion.debug import Profiler + +profiler = Profiler(enabled=True, lazy=False) __provider__ = youtube.Provider() runner.run(__provider__) +log(profiler.get_stats(), 1) diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index 1fff38661..cf12d73b4 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-present plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -11,6 +11,8 @@ import os import json +from .logger import log_debug + def debug_here(host='localhost'): import sys @@ -43,10 +45,7 @@ def runtime(context, addon_version, elapsed, single_file=True): contents = f.read() with open(debug_file, 'w') as f: - if not contents: - contents = default_contents - else: - contents = json.loads(contents) + contents = json.loads(contents) if contents else default_contents if not single_file: items = contents.get('runtimes', []) items.append({"path": context.get_path(), "parameters": context.get_params(), "runtime": round(elapsed, 4)}) @@ -56,3 +55,163 @@ def runtime(context, addon_version, elapsed, single_file=True): items.append({"parameters": context.get_params(), "runtime": round(elapsed, 4)}) contents['runtimes'][context.get_path()] = items f.write(json.dumps(contents, indent=4)) + + +class Profiler(object): + """Class used to profile a block of code""" + + __slots__ = ('__weakref__', '_enabled', '_profiler', '_reuse', 'name', ) + + from cProfile import Profile as _Profile + from pstats import Stats as _Stats + try: + from StringIO import StringIO as _StringIO + except ImportError: + from io import StringIO as _StringIO + from functools import wraps as _wraps + _wraps = staticmethod(_wraps) + from weakref import ref as _ref + + class Proxy(_ref): + def __call__(self, *args, **kwargs): + return super(Profiler.Proxy, self).__call__().__call__( + *args, **kwargs + ) + + def __enter__(self, *args, **kwargs): + return super(Profiler.Proxy, self).__call__().__enter__( + *args, **kwargs + ) + + def __exit__(self, *args, **kwargs): + return super(Profiler.Proxy, self).__call__().__exit__( + *args, **kwargs + ) + + _instances = set() + + def __new__(cls, *args, **kwargs): + self = super(Profiler, cls).__new__(cls) + cls._instances.add(self) + if not kwargs.get('enabled') or kwargs.get('lazy'): + self.__init__(*args, **kwargs) + return cls.Proxy(self) + return self + + def __init__(self, enabled=True, lazy=True, name=__name__, reuse=False): + self._enabled = enabled + self._profiler = None + self._reuse = reuse + self.name = name + + if enabled and not lazy: + self._create_profiler() + + def __del__(self): + self.__class__._instances.discard(self) # pylint: disable=protected-access + + def __enter__(self): + if not self._enabled: + return + + if not self._profiler: + self._create_profiler() + + def __exit__(self, exc_type=None, exc_value=None, traceback=None): + if not self._enabled: + return + + log_debug('Profiling stats: {0}'.format(self.get_stats( + reuse=self._reuse + ))) + if not self._reuse: + self.__del__() + + def __call__(self, func=None, name=__name__, reuse=False): + """Decorator used to profile function calls""" + + if not func: + self._reuse = reuse + self.name = name + return self + + @self.__class__._wraps(func) # pylint: disable=protected-access + def wrapper(*args, **kwargs): + """Wrapper to: + 1) create a new Profiler instance; + 2) run the function being profiled; + 3) print out profiler result to the log; and + 4) return result of function call""" + + name = getattr(func, '__qualname__', None) + if name: + # If __qualname__ is available (Python 3.3+) then use it + pass + + elif args and getattr(args[0], func.__name__, None): + if isinstance(args[0], type): + class_name = args[0].__name__ + else: + class_name = args[0].__class__.__name__ + name = '{0}.{1}'.format(class_name, func.__name__) + + elif (func.__class__ + and not isinstance(func.__class__, type) + and func.__class__.__name__ != 'function'): + name = '{0}.{1}'.format(func.__class__.__name__, func.__name__) + + elif func.__module__: + name = '{0}.{1}'.format(func.__module__, func.__name__) + + else: + name = func.__name__ + + self.name = name + with self: + result = func(*args, **kwargs) + + return result + + if not self._enabled: + self.__del__() + return func + return wrapper + + def _create_profiler(self): + self._profiler = self._Profile() + self._profiler.enable() + + def disable(self): + if self._profiler: + self._profiler.disable() + + def enable(self, flush=False): + self._enabled = True + if flush or not self._profiler: + self._create_profiler() + else: + self._profiler.enable() + + def get_stats(self, flush=True, reuse=False): + if not (self._enabled and self._profiler): + return None + + self.disable() + + output_stream = self._StringIO() + try: + self._Stats( + self._profiler, + stream=output_stream + ).strip_dirs().sort_stats('cumulative', 'time').print_stats(20) + # Occurs when no stats were able to be generated from profiler + except TypeError: + pass + output = output_stream.getvalue() + output_stream.close() + + if reuse: + # If stats are accumulating then enable existing/new profiler + self.enable(flush) + + return output diff --git a/resources/lib/youtube_plugin/kodion/logger.py b/resources/lib/youtube_plugin/kodion/logger.py index df709b3bb..c97587d21 100644 --- a/resources/lib/youtube_plugin/kodion/logger.py +++ b/resources/lib/youtube_plugin/kodion/logger.py @@ -11,7 +11,7 @@ import xbmc import xbmcaddon -DEBUG = xbmc.LOGDEBUG +DEBUG = xbmc.LOGINFO INFO = xbmc.LOGINFO NOTICE = INFO WARNING = xbmc.LOGWARNING From 8a1d739e9e489536cce9a5c6782ef2fa0c056b39 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:13:08 +1100 Subject: [PATCH 009/141] Decrease inputstream.adaptive dependency version --- .github/workflows/make-release.yml | 4 ++-- addon.xml | 2 +- resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 13b2dcc29..76e1c82ef 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -93,7 +93,7 @@ jobs: version=$(xmlstarlet sel -t -v 'string(/addon/@version)' addon.xml) xmlstarlet ed -L -u '/addon/@version' -v "${version}+matrix.1" addon.xml xmlstarlet ed -L -u '/addon/requires/import[@addon="xbmc.python"]/@version' -v '3.0.0' addon.xml - xmlstarlet ed -L -u '/addon/requires/import[@addon="inputstream.adaptive"]/@version' -v '19.0.7' addon.xml + xmlstarlet ed -L -u '/addon/requires/import[@addon="inputstream.adaptive"]/@version' -v '19.0.0' addon.xml xmlstarlet ed -L -d '/addon/requires/import[@addon="script.module.infotagger"]' addon.xml filename=${{ github.event.repository.name }}-${version}.matrix.1.zip cd .. @@ -115,7 +115,7 @@ jobs: version=$(xmlstarlet sel -t -v 'string(/addon/@version)' addon.xml) xmlstarlet ed -L -u '/addon/@version' -v "${version}+matrix.unofficial.1" addon.xml xmlstarlet ed -L -u '/addon/requires/import[@addon="xbmc.python"]/@version' -v '3.0.0' addon.xml - xmlstarlet ed -L -u '/addon/requires/import[@addon="inputstream.adaptive"]/@version' -v '19.0.7' addon.xml + xmlstarlet ed -L -u '/addon/requires/import[@addon="inputstream.adaptive"]/@version' -v '19.0.0' addon.xml xmlstarlet ed -L -d '/addon/requires/import[@addon="script.module.infotagger"]' addon.xml filename=${{ github.event.repository.name }}-${version}.matrix.unofficial.1.zip cd .. diff --git a/addon.xml b/addon.xml index b2b9fd031..f2b401b55 100644 --- a/addon.xml +++ b/addon.xml @@ -3,7 +3,7 @@ - + diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 03b9cee76..9a3eb8e3d 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -307,7 +307,7 @@ def use_inputstream_adaptive(self): 'drm': '2.2.12', # audio codecs 'vorbis': '2.3.14', - 'opus': '19.0.7', + 'opus': '19.0.0', # unknown when Opus audio support was first implemented 'mp4a': True, 'ac-3': '2.1.15', 'ec-3': '2.1.15', From a77473bb3bf61b91bf2ba225714fc236a408e7ad Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 30 Oct 2023 23:07:11 +1100 Subject: [PATCH 010/141] Update sort methods - Remove use of eval - Remove incrementing dummy values for missing sort methods - Not sure why it was done this way originally (for old unit tests?) but the incrementing dummy values can be added back if needed --- .../kodion/constants/const_sort_methods.py | 120 ++++++++++-------- .../lib/youtube_plugin/youtube/provider.py | 4 +- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py b/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py index e33facda7..2838ed93b 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py @@ -8,62 +8,72 @@ See LICENSES/GPL-2.0-only for more information. """ -_xbmc = True +import sys +from xbmcplugin import __dict__ as xbmcplugin -try: - from xbmcplugin import * -except: - _xbmc = False - _count = 0 +namespace = sys.modules[__name__] +names = [ + # 'NONE', + 'LABEL', + 'LABEL_IGNORE_THE', + 'DATE', + 'SIZE', + 'FILE', + 'DRIVE_TYPE', + 'TRACKNUM', + 'DURATION', + 'TITLE', + 'TITLE_IGNORE_THE', + 'ARTIST', + # 'ARTIST_AND_YEAR', + 'ARTIST_IGNORE_THE', + 'ALBUM', + 'ALBUM_IGNORE_THE', + 'GENRE', + 'COUNTRY', + # 'YEAR', + 'VIDEO_YEAR', + 'VIDEO_RATING', + 'VIDEO_USER_RATING', + 'DATEADDED', + 'PROGRAM_COUNT', + 'PLAYLIST_ORDER', + 'EPISODE', + 'VIDEO_TITLE', + 'VIDEO_SORT_TITLE', + 'VIDEO_SORT_TITLE_IGNORE_THE', + 'PRODUCTIONCODE', + 'SONG_RATING', + 'SONG_USER_RATING', + 'MPAA_RATING', + 'VIDEO_RUNTIME', + 'STUDIO', + 'STUDIO_IGNORE_THE', + 'FULLPATH', + 'LABEL_IGNORE_FOLDERS', + 'LASTPLAYED', + 'PLAYCOUNT', + 'LISTENERS', + 'UNSORTED', + 'CHANNEL', + 'CHANNEL_NUMBER', + 'BITRATE', + 'DATE_TAKEN', + 'CLIENT_CHANNEL_ORDER', + 'TOTAL_DISCS', + 'ORIG_DATE', + 'BPM', + 'VIDEO_ORIGINAL_TITLE', + 'VIDEO_ORIGINAL_TITLE_IGNORE_THE', + 'PROVIDER', + 'USER_PREFERENCE', + # 'MAX', +] -def _const(name): - if _xbmc: - return eval(name) - else: - global _count - _count += 1 - return _count +for name in names: + fullname = 'SORT_METHOD_' + name + setattr(namespace, name, + xbmcplugin[fullname] if fullname in xbmcplugin else -1) - -ALBUM = _const('SORT_METHOD_ALBUM') -ALBUM_IGNORE_THE = _const('SORT_METHOD_ALBUM_IGNORE_THE') -ARTIST = _const('SORT_METHOD_ARTIST') -ARTIST_IGNORE_THE = _const('SORT_METHOD_ARTIST_IGNORE_THE') -BIT_RATE = _const('SORT_METHOD_BITRATE') -# CHANNEL = _const('SORT_METHOD_CHANNEL') -# COUNTRY = _const('SORT_METHOD_COUNTRY') -DATE = _const('SORT_METHOD_DATE') -DATE_ADDED = _const('SORT_METHOD_DATEADDED') -# DATE_TAKEN = _const('SORT_METHOD_DATE_TAKEN') -DRIVE_TYPE = _const('SORT_METHOD_DRIVE_TYPE') -DURATION = _const('SORT_METHOD_DURATION') -EPISODE = _const('SORT_METHOD_EPISODE') -FILE = _const('SORT_METHOD_FILE') -# FULL_PATH = _const('SORT_METHOD_FULLPATH') -GENRE = _const('SORT_METHOD_GENRE') -LABEL = _const('SORT_METHOD_LABEL') -# LABEL_IGNORE_FOLDERS = _const('SORT_METHOD_LABEL_IGNORE_FOLDERS') -LABEL_IGNORE_THE = _const('SORT_METHOD_LABEL_IGNORE_THE') -# LAST_PLAYED = _const('SORT_METHOD_LASTPLAYED') -LISTENERS = _const('SORT_METHOD_LISTENERS') -MPAA_RATING = _const('SORT_METHOD_MPAA_RATING') -NONE = _const('SORT_METHOD_NONE') -# PLAY_COUNT = _const('SORT_METHOD_PLAYCOUNT') -PLAYLIST_ORDER = _const('SORT_METHOD_PLAYLIST_ORDER') -PRODUCTION_CODE = _const('SORT_METHOD_PRODUCTIONCODE') -PROGRAM_COUNT = _const('SORT_METHOD_PROGRAM_COUNT') -SIZE = _const('SORT_METHOD_SIZE') -SONG_RATING = _const('SORT_METHOD_SONG_RATING') -STUDIO = _const('SORT_METHOD_STUDIO') -STUDIO_IGNORE_THE = _const('SORT_METHOD_STUDIO_IGNORE_THE') -TITLE = _const('SORT_METHOD_TITLE') -TITLE_IGNORE_THE = _const('SORT_METHOD_TITLE_IGNORE_THE') -TRACK_NUMBER = _const('SORT_METHOD_TRACKNUM') -UNSORTED = _const('SORT_METHOD_UNSORTED') -VIDEO_RATING = _const('SORT_METHOD_VIDEO_RATING') -VIDEO_RUNTIME = _const('SORT_METHOD_VIDEO_RUNTIME') -VIDEO_SORT_TITLE = _const('SORT_METHOD_VIDEO_SORT_TITLE') -VIDEO_SORT_TITLE_IGNORE_THE = _const('SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE') -VIDEO_TITLE = _const('SORT_METHOD_VIDEO_TITLE') -VIDEO_YEAR = _const('SORT_METHOD_VIDEO_YEAR') +del sys, xbmcplugin, namespace, names, name, fullname diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 9fe12991d..ab284c4e3 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1598,8 +1598,8 @@ def set_content_type(context, content_type): if content_type == kodion.constants.content_type.VIDEOS: context.add_sort_method(kodion.constants.sort_method.UNSORTED, kodion.constants.sort_method.VIDEO_RUNTIME, - kodion.constants.sort_method.DATE_ADDED, - kodion.constants.sort_method.TRACK_NUMBER, + kodion.constants.sort_method.DATEADDED, + kodion.constants.sort_method.TRACKNUM, kodion.constants.sort_method.VIDEO_TITLE, kodion.constants.sort_method.DATE) From 533fc96c54429dab376284b0d43828ca461d56d2 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 09:57:28 +1100 Subject: [PATCH 011/141] Misc tidy up No (intended) functional changes: - extraneous indentation/whitespace - missing explicit return values - unnecessary return values - combine nested if statements - else/elif after return - change else if to elif - change if X: to if not X: return - use conditional expressions - simplify redundant conditions in if statements - change bool if condition else not bool to just condition - remove u-strings - remove unused variables - reduce line lengths --- .../kodion/abstract_provider.py | 66 +-- .../kodion/impl/abstract_context.py | 6 +- .../kodion/impl/abstract_context_ui.py | 3 +- .../kodion/impl/abstract_settings.py | 3 +- .../kodion/impl/xbmc/xbmc_context.py | 2 +- .../kodion/impl/xbmc/xbmc_context_ui.py | 5 +- .../kodion/impl/xbmc/xbmc_items.py | 6 +- .../kodion/impl/xbmc/xbmc_playlist.py | 16 +- .../youtube_plugin/kodion/items/audio_item.py | 2 +- .../youtube_plugin/kodion/items/base_item.py | 9 +- .../kodion/items/directory_item.py | 2 +- .../youtube_plugin/kodion/items/image_item.py | 2 +- .../youtube_plugin/kodion/items/uri_item.py | 2 +- .../lib/youtube_plugin/kodion/items/utils.py | 8 +- .../youtube_plugin/kodion/items/video_item.py | 18 +- .../kodion/json_store/json_store.py | 1 + .../kodion/utils/access_manager.py | 3 +- .../kodion/utils/http_server.py | 475 +++++++++--------- .../lib/youtube_plugin/kodion/utils/ip_api.py | 5 +- .../youtube_plugin/kodion/utils/methods.py | 6 +- .../youtube_plugin/kodion/utils/monitor.py | 33 +- .../lib/youtube_plugin/kodion/utils/player.py | 148 +++--- .../youtube_plugin/kodion/utils/storage.py | 12 +- .../kodion/utils/system_version.py | 24 +- .../youtube/client/__config__.py | 68 ++- .../youtube/client/login_client.py | 20 +- .../youtube/helper/resource_manager.py | 2 + .../youtube/helper/signature/cipher.py | 2 +- .../youtube/helper/subtitles.py | 3 +- .../youtube_plugin/youtube/helper/utils.py | 53 +- .../youtube/helper/video_info.py | 5 +- .../youtube/helper/yt_context_menu.py | 7 +- .../youtube_plugin/youtube/helper/yt_login.py | 19 +- .../youtube_plugin/youtube/helper/yt_play.py | 12 +- .../youtube/helper/yt_playlist.py | 39 +- .../youtube/helper/yt_setup_wizard.py | 88 ++-- .../youtube/helper/yt_specials.py | 29 +- .../youtube/helper/yt_subscriptions.py | 6 +- .../youtube_plugin/youtube/helper/yt_video.py | 14 +- .../lib/youtube_plugin/youtube/provider.py | 154 +++--- resources/lib/youtube_requests.py | 63 ++- 41 files changed, 721 insertions(+), 720 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index ff8ffd444..925d91ae3 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -117,7 +117,7 @@ def on_extra_fanart(context, re_match): :param re_match: :return: """ - return None + return def _internal_on_extra_fanart(self, context, re_match): path = re_match.group('path') @@ -146,12 +146,13 @@ def _internal_favorite(context, re_match): if command == 'add': fav_item = items.from_json(params['item']) context.get_favorite_list().add(fav_item) - elif command == 'remove': + return None + if command == 'remove': fav_item = items.from_json(params['item']) context.get_favorite_list().remove(fav_item) context.get_ui().refresh_container() - elif command == 'list': - + return None + if command == 'list': directory_items = context.get_favorite_list().list() for directory_item in directory_items: @@ -161,8 +162,7 @@ def _internal_favorite(context, re_match): directory_item.set_context_menu(context_menu) return directory_items - else: - pass + return None def _internal_watch_later(self, context, re_match): self.on_watch_later(context, re_match) @@ -173,11 +173,13 @@ def _internal_watch_later(self, context, re_match): if command == 'add': item = items.from_json(params['item']) context.get_watch_later_list().add(item) - elif command == 'remove': + return None + if command == 'remove': item = items.from_json(params['item']) context.get_watch_later_list().remove(item) context.get_ui().refresh_container() - elif command == 'list': + return None + if command == 'list': video_items = context.get_watch_later_list().list() for video_item in video_items: @@ -187,9 +189,7 @@ def _internal_watch_later(self, context, re_match): video_item.set_context_menu(context_menu) return video_items - else: - # do something - pass + return None @property def data_cache(self): @@ -210,7 +210,7 @@ def _internal_search(self, context, re_match): search_history.remove(query) context.get_ui().refresh_container() return True - elif command == 'rename': + if command == 'rename': query = params['q'] result, new_query = context.get_ui().on_keyboard_input(context.localize(constants.localize.SEARCH_RENAME), query) @@ -218,11 +218,11 @@ def _internal_search(self, context, re_match): search_history.rename(query, new_query) context.get_ui().refresh_container() return True - elif command == 'clear': + if command == 'clear': search_history.clear() context.get_ui().refresh_container() return True - elif command == 'input': + if command == 'input': self.data_cache = context folder_path = context.get_ui().get_info_label('Container.FolderPath') @@ -263,7 +263,7 @@ def _internal_search(self, context, re_match): query = query.decode('utf-8') return self.on_search(query, context, re_match) - elif command == 'query': + if command == 'query': incognito = str(context.get_param('incognito', False)).lower() == 'true' channel_id = context.get_param('channel_id', '') query = params['q'] @@ -277,30 +277,30 @@ def _internal_search(self, context, re_match): if isinstance(query, bytes): query = query.decode('utf-8') return self.on_search(query, context, re_match) - else: - context.set_content_type(constants.content_type.FILES) - result = [] - location = str(context.get_param('location', False)).lower() == 'true' + context.set_content_type(constants.content_type.FILES) + result = [] - # 'New Search...' - new_search_item = items.NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) - result.append(new_search_item) + location = str(context.get_param('location', False)).lower() == 'true' - for search in search_history.list(): - # little fallback for old history entries - if isinstance(search, items.DirectoryItem): - search = search.get_name() + # 'New Search...' + new_search_item = items.NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) + result.append(new_search_item) - # we create a new instance of the SearchItem - search_history_item = items.SearchHistoryItem(context, search, fanart=self.get_alternative_fanart(context), location=location) - result.append(search_history_item) + for search in search_history.list(): + # little fallback for old history entries + if isinstance(search, items.DirectoryItem): + search = search.get_name() - if search_history.is_empty(): - # context.execute('RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'input'])) - pass + # we create a new instance of the SearchItem + search_history_item = items.SearchHistoryItem(context, search, fanart=self.get_alternative_fanart(context), location=location) + result.append(search_history_item) + + if search_history.is_empty(): + # context.execute('RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'input'])) + pass - return result, {self.RESULT_CACHE_TO_DISC: False} + return result, {self.RESULT_CACHE_TO_DISC: False} def handle_exception(self, context, exception_to_handle): return True diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py index 6cc2541cf..078314c5c 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py @@ -17,7 +17,7 @@ class AbstractContext(object): - def __init__(self, path=u'/', params=None, plugin_name=u'', plugin_id=u''): + def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): if not params: params = {} @@ -137,7 +137,7 @@ def get_system_version(self): return self._system_version - def create_uri(self, path=u'/', params=None): + def create_uri(self, path='/', params=None): if not params: params = {} @@ -218,7 +218,7 @@ def get_handle(self): def get_settings(self): raise NotImplementedError() - def localize(self, text_id, default_text=u''): + def localize(self, text_id, default_text=''): raise NotImplementedError() def set_content_type(self, content_type): diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py index eb7419174..efca1c1b2 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py @@ -40,7 +40,8 @@ def on_select(self, title, items=None): def open_settings(self): raise NotImplementedError() - def show_notification(self, message, header='', image_uri='', time_milliseconds=5000, audible=True): + def show_notification(self, message, header='', image_uri='', + time_milliseconds=5000, audible=True): raise NotImplementedError() @staticmethod diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index f9594e1ca..14868fab6 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -205,8 +205,7 @@ def get_location(self): latitude = longitude = None if latitude and longitude: return '{lat},{long}'.format(lat=latitude, long=longitude) - else: - return '' + return '' def set_location(self, value): self.set_string(SETTINGS.LOCATION, value) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 9a3eb8e3d..f36f60e86 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -186,7 +186,7 @@ def get_native_path(self): def get_settings(self): return self._settings - def localize(self, text_id, default_text=u''): + def localize(self, text_id, default_text=''): result = None if isinstance(text_id, int): """ diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py index 147d4e5eb..4f1e50aee 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py @@ -44,8 +44,7 @@ def on_keyboard_input(self, title, default='', hidden=False): if keyboard.isConfirmed() and keyboard.getText(): text = utils.to_unicode(keyboard.getText()) return True, text - else: - return False, u'' + return False, '' # Starting with Gotham (13.X > ...) dialog = xbmcgui.Dialog() @@ -54,7 +53,7 @@ def on_keyboard_input(self, title, default='', hidden=False): text = utils.to_unicode(result) return True, text - return False, u'' + return False, '' def on_numeric_input(self, title, default=''): dialog = xbmcgui.Dialog() diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py index 45beda6df..9c43c5b49 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py @@ -37,7 +37,7 @@ def to_play_item(context, play_item): is_strm = str(context.get_param('strm', False)).lower() == 'true' - thumb = play_item.get_image() if play_item.get_image() else u'DefaultVideo.png' + thumb = play_item.get_image() if play_item.get_image() else 'DefaultVideo.png' title = play_item.get_title() if play_item.get_title() else play_item.get_name() fanart = '' settings = context.get_settings() @@ -138,7 +138,7 @@ def to_play_item(context, play_item): def to_video_item(context, video_item): context.log_debug('Converting VideoItem |%s|' % video_item.get_uri()) - thumb = video_item.get_image() if video_item.get_image() else u'DefaultVideo.png' + thumb = video_item.get_image() if video_item.get_image() else 'DefaultVideo.png' title = video_item.get_title() if video_item.get_title() else video_item.get_name() fanart = '' settings = context.get_settings() @@ -201,7 +201,7 @@ def to_video_item(context, video_item): def to_audio_item(context, audio_item): context.log_debug('Converting AudioItem |%s|' % audio_item.get_uri()) - thumb = audio_item.get_image() if audio_item.get_image() else u'DefaultAudio.png' + thumb = audio_item.get_image() if audio_item.get_image() else 'DefaultAudio.png' title = audio_item.get_name() fanart = '' settings = context.get_settings() diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py index e95bfb436..b21130c86 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py @@ -61,12 +61,12 @@ def get_items(self): if 'items' in response['result']: return response['result']['items'] return [] + + if 'error' in response: + message = response['error']['message'] + code = response['error']['code'] + error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code) else: - if 'error' in response: - message = response['error']['message'] - code = response['error']['code'] - error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code) - else: - error = 'Requested |%s| received error |%s|' % (rpc_request, str(response)) - self._context.log_debug(error) - return [] + error = 'Requested |%s| received error |%s|' % (rpc_request, str(response)) + self._context.log_debug(error) + return [] diff --git a/resources/lib/youtube_plugin/kodion/items/audio_item.py b/resources/lib/youtube_plugin/kodion/items/audio_item.py index 33e5c530d..0d4b012ff 100644 --- a/resources/lib/youtube_plugin/kodion/items/audio_item.py +++ b/resources/lib/youtube_plugin/kodion/items/audio_item.py @@ -14,7 +14,7 @@ class AudioItem(BaseItem): - def __init__(self, name, uri, image=u'', fanart=u''): + def __init__(self, name, uri, image='', fanart=''): BaseItem.__init__(self, name, uri, image, fanart) self._duration = None self._track_number = None diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 0385052ab..d3b8572e2 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -19,7 +19,7 @@ class BaseItem(object): VERSION = 3 INFO_DATE = 'date' # (string) iso 8601 - def __init__(self, name, uri, image=u'', fanart=u''): + def __init__(self, name, uri, image='', fanart=''): self._version = BaseItem.VERSION try: @@ -29,7 +29,7 @@ def __init__(self, name, uri, image=u'', fanart=u''): self._uri = uri - self._image = u'' + self._image = '' self.set_image(image) self._fanart = fanart @@ -64,10 +64,7 @@ def get_name(self): return self._name def set_uri(self, uri): - if isinstance(uri, str): - self._uri = uri - else: - self._uri = '' + self._uri = uri if uri and isinstance(uri, str) else '' def get_uri(self): """ diff --git a/resources/lib/youtube_plugin/kodion/items/directory_item.py b/resources/lib/youtube_plugin/kodion/items/directory_item.py index 9c3749ebd..96072485b 100644 --- a/resources/lib/youtube_plugin/kodion/items/directory_item.py +++ b/resources/lib/youtube_plugin/kodion/items/directory_item.py @@ -12,7 +12,7 @@ class DirectoryItem(BaseItem): - def __init__(self, name, uri, image=u'', fanart=u''): + def __init__(self, name, uri, image='', fanart=''): BaseItem.__init__(self, name, uri, image, fanart) self._plot = self.get_name() self._is_action = False diff --git a/resources/lib/youtube_plugin/kodion/items/image_item.py b/resources/lib/youtube_plugin/kodion/items/image_item.py index b0f52b252..3909a48d6 100644 --- a/resources/lib/youtube_plugin/kodion/items/image_item.py +++ b/resources/lib/youtube_plugin/kodion/items/image_item.py @@ -12,7 +12,7 @@ class ImageItem(BaseItem): - def __init__(self, name, uri, image=u'', fanart=u''): + def __init__(self, name, uri, image='', fanart=''): BaseItem.__init__(self, name, uri, image, fanart) self._title = None diff --git a/resources/lib/youtube_plugin/kodion/items/uri_item.py b/resources/lib/youtube_plugin/kodion/items/uri_item.py index d12a3f662..9ba74cc9b 100644 --- a/resources/lib/youtube_plugin/kodion/items/uri_item.py +++ b/resources/lib/youtube_plugin/kodion/items/uri_item.py @@ -13,4 +13,4 @@ class UriItem(BaseItem): def __init__(self, uri): - BaseItem.__init__(self, name=u'', uri=uri) + BaseItem.__init__(self, name='', uri=uri) diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index db7f235a4..9cdd6b6a4 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -24,10 +24,10 @@ def from_json(json_data): """ def _from_json(_json_data): - mapping = {'VideoItem': lambda: VideoItem(u'', u''), - 'DirectoryItem': lambda: DirectoryItem(u'', u''), - 'AudioItem': lambda: AudioItem(u'', u''), - 'ImageItem': lambda: ImageItem(u'', u'')} + mapping = {'VideoItem': lambda: VideoItem('', ''), + 'DirectoryItem': lambda: DirectoryItem('', ''), + 'AudioItem': lambda: AudioItem('', ''), + 'ImageItem': lambda: ImageItem('', '')} item = None item_type = _json_data.get('type', None) diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 7d19fd4d9..acb0620f9 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -19,7 +19,7 @@ class VideoItem(BaseItem): - def __init__(self, name, uri, image=u'', fanart=u''): + def __init__(self, name, uri, image='', fanart=''): BaseItem.__init__(self, name, uri, image, fanart) self._genre = None self._aired = None @@ -107,7 +107,9 @@ def set_premiered(self, year, month, day): self._premiered = date.isoformat() def set_premiered_from_datetime(self, date_time): - self.set_premiered(year=date_time.year, month=date_time.month, day=date_time.day) + self.set_premiered(year=date_time.year, + month=date_time.month, + day=date_time.day) def get_premiered(self): return self._premiered @@ -191,7 +193,9 @@ def get_aired_utc(self): return self._aired_utc def set_aired_from_datetime(self, date_time): - self.set_aired(year=date_time.year, month=date_time.month, day=date_time.day) + self.set_aired(year=date_time.year, + month=date_time.month, + day=date_time.day) def set_scheduled_start_utc(self, dt): self._scheduled_start_utc = dt @@ -221,8 +225,12 @@ def set_date(self, year, month, day, hour=0, minute=0, second=0): self._date = date.isoformat(sep=' ') def set_date_from_datetime(self, date_time): - self.set_date(year=date_time.year, month=date_time.month, day=date_time.day, hour=date_time.hour, - minute=date_time.minute, second=date_time.second) + self.set_date(year=date_time.year, + month=date_time.month, + day=date_time.day, + hour=date_time.hour, + minute=date_time.minute, + second=date_time.second) def get_date(self): return self._date diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 3d071a7b9..52b1e06f3 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -17,6 +17,7 @@ from .. import logger + try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: diff --git a/resources/lib/youtube_plugin/kodion/utils/access_manager.py b/resources/lib/youtube_plugin/kodion/utils/access_manager.py index 2e6405bb3..f3d3a0464 100644 --- a/resources/lib/youtube_plugin/kodion/utils/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/utils/access_manager.py @@ -344,8 +344,7 @@ def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): if last_hash != current_hash: self.set_dev_last_key_hash(addon_id, current_hash) return True - else: - return False + return False @staticmethod def __calc_key_hash(api_key, client_id, client_secret): diff --git a/resources/lib/youtube_plugin/kodion/utils/http_server.py b/resources/lib/youtube_plugin/kodion/utils/http_server.py index 522e5a830..8e3202719 100644 --- a/resources/lib/youtube_plugin/kodion/utils/http_server.py +++ b/resources/lib/youtube_plugin/kodion/utils/http_server.py @@ -55,9 +55,8 @@ def connection_allowed(self): if not conn_allowed: logger.log_debug('HTTPServer: Connection from |%s| not allowed' % client_ip) - else: - if self.path != '/ping': - logger.log_debug(' '.join(log_lines)) + elif self.path != '/ping': + logger.log_debug(' '.join(log_lines)) return conn_allowed # noinspection PyPep8Naming @@ -82,86 +81,82 @@ def do_GET(self): if not self.connection_allowed(): self.send_error(403) - else: - if mpd_proxy_enabled and self.path.endswith('.mpd'): - file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) - file_chunk = True - logger.log_debug('HTTPServer: Request file path |{file_path}|'.format(file_path=file_path.encode('utf-8'))) - try: - with open(file_path, 'rb') as f: - self.send_response(200) - self.send_header('Content-Type', 'application/xml+dash') - self.send_header('Content-Length', os.path.getsize(file_path)) - self.end_headers() - while file_chunk: - file_chunk = f.read(self.chunk_size) - if file_chunk: - self.wfile.write(file_chunk) - except IOError: - response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8')) - self.send_error(404, response) - elif api_config_enabled and stripped_path.lower() == '/api': - html = self.api_config_page() - html = html.encode('utf-8') - self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Content-Length', len(html)) - self.end_headers() - for chunk in self.get_chunks(html): - self.wfile.write(chunk) - elif api_config_enabled and stripped_path.startswith('/api_submit'): - addon = xbmcaddon.Addon('plugin.video.youtube') - i18n = addon.getLocalizedString - xbmc.executebuiltin('Dialog.Close(addonsettings,true)') - old_api_key = addon.getSetting('youtube.api.key') - old_api_id = addon.getSetting('youtube.api.id') - old_api_secret = addon.getSetting('youtube.api.secret') - query = urlparse(self.path).query - params = parse_qs(query) - api_key = params.get('api_key', [None])[0] - api_id = params.get('api_id', [None])[0] - api_secret = params.get('api_secret', [None])[0] - if api_key and api_id and api_secret: - footer = i18n(30638) - else: - footer = u'' - if re.search(r'api_key=(?:&|$)', query): - api_key = '' - if re.search(r'api_id=(?:&|$)', query): - api_id = '' - if re.search(r'api_secret=(?:&|$)', query): - api_secret = '' - updated = [] - if api_key is not None and api_key != old_api_key: - addon.setSetting('youtube.api.key', api_key) - updated.append(i18n(30201)) - if api_id is not None and api_id != old_api_id: - addon.setSetting('youtube.api.id', api_id) - updated.append(i18n(30202)) - if api_secret is not None and api_secret != old_api_secret: - updated.append(i18n(30203)) - addon.setSetting('youtube.api.secret', api_secret) - if addon.getSetting('youtube.api.key') and addon.getSetting('youtube.api.id') and \ - addon.getSetting('youtube.api.secret'): - enabled = i18n(30636) - else: - enabled = i18n(30637) - if not updated: - updated = i18n(30635) - else: - updated = i18n(30631) % u', '.join(updated) - html = self.api_submit_page(updated, enabled, footer) - html = html.encode('utf-8') - self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Content-Length', len(html)) - self.end_headers() - for chunk in self.get_chunks(html): - self.wfile.write(chunk) - elif stripped_path == '/ping': - self.send_error(204) + elif mpd_proxy_enabled and self.path.endswith('.mpd'): + file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) + file_chunk = True + logger.log_debug('HTTPServer: Request file path |{file_path}|'.format(file_path=file_path.encode('utf-8'))) + try: + with open(file_path, 'rb') as f: + self.send_response(200) + self.send_header('Content-Type', 'application/xml+dash') + self.send_header('Content-Length', os.path.getsize(file_path)) + self.end_headers() + while file_chunk: + file_chunk = f.read(self.chunk_size) + if file_chunk: + self.wfile.write(file_chunk) + except IOError: + response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8')) + self.send_error(404, response) + elif api_config_enabled and stripped_path.lower() == '/api': + html = self.api_config_page() + html = html.encode('utf-8') + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Content-Length', len(html)) + self.end_headers() + for chunk in self.get_chunks(html): + self.wfile.write(chunk) + elif api_config_enabled and stripped_path.startswith('/api_submit'): + addon = xbmcaddon.Addon('plugin.video.youtube') + i18n = addon.getLocalizedString + xbmc.executebuiltin('Dialog.Close(addonsettings,true)') + old_api_key = addon.getSetting('youtube.api.key') + old_api_id = addon.getSetting('youtube.api.id') + old_api_secret = addon.getSetting('youtube.api.secret') + query = urlparse(self.path).query + params = parse_qs(query) + api_key = params.get('api_key', [None])[0] + api_id = params.get('api_id', [None])[0] + api_secret = params.get('api_secret', [None])[0] + footer = i18n(30638) if api_key and api_id and api_secret else '' + if re.search(r'api_key=(?:&|$)', query): + api_key = '' + if re.search(r'api_id=(?:&|$)', query): + api_id = '' + if re.search(r'api_secret=(?:&|$)', query): + api_secret = '' + updated = [] + if api_key is not None and api_key != old_api_key: + addon.setSetting('youtube.api.key', api_key) + updated.append(i18n(30201)) + if api_id is not None and api_id != old_api_id: + addon.setSetting('youtube.api.id', api_id) + updated.append(i18n(30202)) + if api_secret is not None and api_secret != old_api_secret: + updated.append(i18n(30203)) + addon.setSetting('youtube.api.secret', api_secret) + if addon.getSetting('youtube.api.key') and addon.getSetting('youtube.api.id') and \ + addon.getSetting('youtube.api.secret'): + enabled = i18n(30636) else: - self.send_error(501) + enabled = i18n(30637) + if not updated: + updated = i18n(30635) + else: + updated = i18n(30631) % ', '.join(updated) + html = self.api_submit_page(updated, enabled, footer) + html = html.encode('utf-8') + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Content-Length', len(html)) + self.end_headers() + for chunk in self.get_chunks(html): + self.wfile.write(chunk) + elif stripped_path == '/ping': + self.send_error(204) + else: + self.send_error(501) # noinspection PyPep8Naming def do_HEAD(self): @@ -289,172 +284,172 @@ def api_submit_page(updated_keys, enabled, footer): class Pages(object): api_configuration = { 'html': - u'\n\n' - u'\n\t\n' - u'\t{title}\n' - u'\t\n' - u'\n\n' - u'\t
\n' - u'\t
{header}
\n' - u'\t
\n' - u'\t\t\n' - u'\t\t\n' - u'\t\t\n' - u'\t\t\n' - u'\t
\n' - u'\t
\n' - u'\n', + '\n\n' + '\n\t\n' + '\t{title}\n' + '\t\n' + '\n\n' + '\t
\n' + '\t
{header}
\n' + '\t
\n' + '\t\t\n' + '\t\t\n' + '\t\t\n' + '\t\t\n' + '\t
\n' + '\t
\n' + '\n', 'css': - u'body {\n' - u' background: #141718;\n' - u'}\n' - u'.center {\n' - u' margin: auto;\n' - u' width: 600px;\n' - u' padding: 10px;\n' - u'}\n' - u'.config_form {\n' - u' width: 575px;\n' - u' height: 145px;\n' - u' font-size: 16px;\n' - u' background: #1a2123;\n' - u' padding: 30px 30px 15px 30px;\n' - u' border: 5px solid #1a2123;\n' - u'}\n' - u'h5 {\n' - u' font-family: Arial, Helvetica, sans-serif;\n' - u' font-size: 16px;\n' - u' color: #fff;\n' - u' font-weight: 600;\n' - u' width: 575px;\n' - u' height: 20px;\n' - u' background: #0f84a5;\n' - u' padding: 5px 30px 5px 30px;\n' - u' border: 5px solid #0f84a5;\n' - u' margin: 0px;\n' - u'}\n' - u'.config_form input[type=submit],\n' - u'.config_form input[type=button],\n' - u'.config_form input[type=text],\n' - u'.config_form textarea,\n' - u'.config_form label {\n' - u' font-family: Arial, Helvetica, sans-serif;\n' - u' font-size: 16px;\n' - u' color: #fff;\n' - u'}\n' - u'.config_form label {\n' - u' display:block;\n' - u' margin-bottom: 10px;\n' - u'}\n' - u'.config_form label > span {\n' - u' display: inline-block;\n' - u' float: left;\n' - u' width: 150px;\n' - u'}\n' - u'.config_form input[type=text] {\n' - u' background: transparent;\n' - u' border: none;\n' - u' border-bottom: 1px solid #147a96;\n' - u' width: 400px;\n' - u' outline: none;\n' - u' padding: 0px 0px 0px 0px;\n' - u'}\n' - u'.config_form input[type=text]:focus {\n' - u' border-bottom: 1px dashed #0f84a5;\n' - u'}\n' - u'.config_form input[type=submit],\n' - u'.config_form input[type=button] {\n' - u' width: 150px;\n' - u' background: #141718;\n' - u' border: none;\n' - u' padding: 8px 0px 8px 10px;\n' - u' border-radius: 5px;\n' - u' color: #fff;\n' - u' margin-top: 10px\n' - u'}\n' - u'.config_form input[type=submit]:hover,\n' - u'.config_form input[type=button]:hover {\n' - u' background: #0f84a5;\n' - u'}\n' + 'body {\n' + ' background: #141718;\n' + '}\n' + '.center {\n' + ' margin: auto;\n' + ' width: 600px;\n' + ' padding: 10px;\n' + '}\n' + '.config_form {\n' + ' width: 575px;\n' + ' height: 145px;\n' + ' font-size: 16px;\n' + ' background: #1a2123;\n' + ' padding: 30px 30px 15px 30px;\n' + ' border: 5px solid #1a2123;\n' + '}\n' + 'h5 {\n' + ' font-family: Arial, Helvetica, sans-serif;\n' + ' font-size: 16px;\n' + ' color: #fff;\n' + ' font-weight: 600;\n' + ' width: 575px;\n' + ' height: 20px;\n' + ' background: #0f84a5;\n' + ' padding: 5px 30px 5px 30px;\n' + ' border: 5px solid #0f84a5;\n' + ' margin: 0px;\n' + '}\n' + '.config_form input[type=submit],\n' + '.config_form input[type=button],\n' + '.config_form input[type=text],\n' + '.config_form textarea,\n' + '.config_form label {\n' + ' font-family: Arial, Helvetica, sans-serif;\n' + ' font-size: 16px;\n' + ' color: #fff;\n' + '}\n' + '.config_form label {\n' + ' display:block;\n' + ' margin-bottom: 10px;\n' + '}\n' + '.config_form label > span {\n' + ' display: inline-block;\n' + ' float: left;\n' + ' width: 150px;\n' + '}\n' + '.config_form input[type=text] {\n' + ' background: transparent;\n' + ' border: none;\n' + ' border-bottom: 1px solid #147a96;\n' + ' width: 400px;\n' + ' outline: none;\n' + ' padding: 0px 0px 0px 0px;\n' + '}\n' + '.config_form input[type=text]:focus {\n' + ' border-bottom: 1px dashed #0f84a5;\n' + '}\n' + '.config_form input[type=submit],\n' + '.config_form input[type=button] {\n' + ' width: 150px;\n' + ' background: #141718;\n' + ' border: none;\n' + ' padding: 8px 0px 8px 10px;\n' + ' border-radius: 5px;\n' + ' color: #fff;\n' + ' margin-top: 10px\n' + '}\n' + '.config_form input[type=submit]:hover,\n' + '.config_form input[type=button]:hover {\n' + ' background: #0f84a5;\n' + '}\n' } api_submit = { 'html': - u'\n\n' - u'\n\t\n' - u'\t{title}\n' - u'\t\n' - u'\n\n' - u'\t
\n' - u'\t
{header}
\n' - u'\t
\n' - u'\t\t{updated}\n' - u'\t\t{enabled}\n' - u'\t\t \n' - u'\t\t \n' - u'\t\t \n' - u'\t\t \n' - u'\t\t
\n' - u'\t\t\t{footer}\n' - u'\t\t
\n' - u'\t
\n' - u'\t
\n' - u'\n', + '\n\n' + '\n\t\n' + '\t{title}\n' + '\t\n' + '\n\n' + '\t
\n' + '\t
{header}
\n' + '\t
\n' + '\t\t{updated}\n' + '\t\t{enabled}\n' + '\t\t \n' + '\t\t \n' + '\t\t \n' + '\t\t \n' + '\t\t
\n' + '\t\t\t{footer}\n' + '\t\t
\n' + '\t
\n' + '\t
\n' + '\n', 'css': - u'body {\n' - u' background: #141718;\n' - u'}\n' - u'.center {\n' - u' margin: auto;\n' - u' width: 600px;\n' - u' padding: 10px;\n' - u'}\n' - u'.textcenter {\n' - u' margin: auto;\n' - u' width: 600px;\n' - u' padding: 10px;\n' - u' text-align: center;\n' - u'}\n' - u'.content {\n' - u' width: 575px;\n' - u' height: 145px;\n' - u' background: #1a2123;\n' - u' padding: 30px 30px 15px 30px;\n' - u' border: 5px solid #1a2123;\n' - u'}\n' - u'h5 {\n' - u' font-family: Arial, Helvetica, sans-serif;\n' - u' font-size: 16px;\n' - u' color: #fff;\n' - u' font-weight: 600;\n' - u' width: 575px;\n' - u' height: 20px;\n' - u' background: #0f84a5;\n' - u' padding: 5px 30px 5px 30px;\n' - u' border: 5px solid #0f84a5;\n' - u' margin: 0px;\n' - u'}\n' - u'span {\n' - u' font-family: Arial, Helvetica, sans-serif;\n' - u' font-size: 16px;\n' - u' color: #fff;\n' - u' display: block;\n' - u' float: left;\n' - u' width: 575px;\n' - u'}\n' - u'small {\n' - u' font-family: Arial, Helvetica, sans-serif;\n' - u' font-size: 12px;\n' - u' color: #fff;\n' - u'}\n' + 'body {\n' + ' background: #141718;\n' + '}\n' + '.center {\n' + ' margin: auto;\n' + ' width: 600px;\n' + ' padding: 10px;\n' + '}\n' + '.textcenter {\n' + ' margin: auto;\n' + ' width: 600px;\n' + ' padding: 10px;\n' + ' text-align: center;\n' + '}\n' + '.content {\n' + ' width: 575px;\n' + ' height: 145px;\n' + ' background: #1a2123;\n' + ' padding: 30px 30px 15px 30px;\n' + ' border: 5px solid #1a2123;\n' + '}\n' + 'h5 {\n' + ' font-family: Arial, Helvetica, sans-serif;\n' + ' font-size: 16px;\n' + ' color: #fff;\n' + ' font-weight: 600;\n' + ' width: 575px;\n' + ' height: 20px;\n' + ' background: #0f84a5;\n' + ' padding: 5px 30px 5px 30px;\n' + ' border: 5px solid #0f84a5;\n' + ' margin: 0px;\n' + '}\n' + 'span {\n' + ' font-family: Arial, Helvetica, sans-serif;\n' + ' font-size: 16px;\n' + ' color: #fff;\n' + ' display: block;\n' + ' float: left;\n' + ' width: 575px;\n' + '}\n' + 'small {\n' + ' font-family: Arial, Helvetica, sans-serif;\n' + ' font-size: 12px;\n' + ' color: #fff;\n' + '}\n' } diff --git a/resources/lib/youtube_plugin/kodion/utils/ip_api.py b/resources/lib/youtube_plugin/kodion/utils/ip_api.py index dc371483c..f6873e253 100644 --- a/resources/lib/youtube_plugin/kodion/utils/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/utils/ip_api.py @@ -42,6 +42,5 @@ def coordinates(self): if lat is None or lon is None: self._context.log_error('No coordinates returned') return None - else: - self._context.log_debug('Coordinates found') - return lat, lon + self._context.log_debug('Coordinates found') + return lat, lon diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 5a0738c63..490759552 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -22,6 +22,7 @@ __all__ = ['create_path', 'create_uri_path', 'strip_html_from_text', 'print_items', 'find_best_fit', 'to_utf8', 'to_str', 'to_unicode', 'select_stream', 'make_dirs', 'loose_version', 'find_video_id'] + try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: @@ -138,8 +139,7 @@ def _sort_stream_data(_stream_data): def _find_best_fit_video(_stream_data): if audio_only: return video_quality - _stream_data.get('sort', (0, 0))[0] - else: - return video_quality - _stream_data.get('video', {}).get('height', 0) + return video_quality - _stream_data.get('video', {}).get('height', 0) sorted_stream_data_list = sorted(stream_data_list, key=_sort_stream_data) @@ -187,7 +187,7 @@ def create_path(*args): uri_path = '/'.join(comps) if uri_path: - return u'/%s/' % uri_path + return '/%s/' % uri_path return '/' diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index 9ca023e79..4105b4fd0 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -106,18 +106,26 @@ def httpd_port_sync(self): self._old_httpd_port = self._httpd_port def start_httpd(self): + if self.httpd: + return + + logger.log_debug('HTTPServer: Starting |{ip}:{port}|'.format( + ip=self.httpd_address(), + port=str(self.httpd_port()) + )) + self.httpd_port_sync() + self.httpd = get_http_server(address=self.httpd_address(), port=self.httpd_port()) if not self.httpd: - logger.log_debug('HTTPServer: Starting |{ip}:{port}|'.format(ip=self.httpd_address(), - port=str(self.httpd_port()))) - self.httpd_port_sync() - self.httpd = get_http_server(address=self.httpd_address(), port=self.httpd_port()) - if self.httpd: - self.httpd_thread = threading.Thread(target=self.httpd.serve_forever) - self.httpd_thread.daemon = True - self.httpd_thread.start() - sock_name = self.httpd.socket.getsockname() - logger.log_debug('HTTPServer: Serving on |{ip}:{port}|'.format(ip=str(sock_name[0]), - port=str(sock_name[1]))) + return + + self.httpd_thread = threading.Thread(target=self.httpd.serve_forever) + self.httpd_thread.daemon = True + self.httpd_thread.start() + sock_name = self.httpd.socket.getsockname() + logger.log_debug('HTTPServer: Serving on |{ip}:{port}|'.format( + ip=str(sock_name[0]), + port=str(sock_name[1]) + )) def shutdown_httpd(self): if self.httpd: @@ -160,5 +168,4 @@ def remove_temp_dir(self): if os.path.isdir(path): logger.log_debug('Failed to remove directory: {dir}'.format(dir=path.encode('utf-8'))) return False - else: - return True + return True diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index c8ce05226..0bdccf1c1 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -146,7 +146,7 @@ def run(self): except RuntimeError: pass - if self.current_time < 0.0: + if self.current_time < 0: self.current_time = 0.0 if self.abort_now(): @@ -162,7 +162,7 @@ def run(self): self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) break - if seek_time and seek_time != 0.0: + if seek_time: player.seekTime(seek_time) try: self.current_time = float(player.getTime()) @@ -194,43 +194,43 @@ def run(self): self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) break - if is_logged_in and report_url and use_remote_history: - if first_report or (p_waited >= report_interval): - if first_report: - first_report = False - self.segment_start = 0.0 - self.current_time = 0.0 - self.percent_complete = 0 - - p_waited = 0.0 - - if self.segment_start < 0.0: - self.segment_start = 0.0 - - if state == 'playing': - segment_end = self.current_time - else: - segment_end = self.segment_start - - if segment_end > float(self.total_time): - segment_end = float(self.total_time) - - if self.segment_start > segment_end: - segment_end = self.segment_start + 10.0 - - if state == 'playing' or last_state == 'playing': # only report state='paused' once - client.update_watch_history(self.video_id, report_url - .format(st=format(self.segment_start, '.3f'), - et=format(segment_end, '.3f'), - state=state)) - self.context.log_debug( - 'Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' % - (self.video_id, - format(self.segment_start, '.3f'), - format(segment_end, '.3f'), - self.percent_complete, state)) - - self.segment_start = segment_end + if (is_logged_in and report_url and use_remote_history and ( + first_report or p_waited >= report_interval)): + if first_report: + first_report = False + self.segment_start = 0.0 + self.current_time = 0.0 + self.percent_complete = 0 + + p_waited = 0.0 + + if self.segment_start < 0: + self.segment_start = 0.0 + + if state == 'playing': + segment_end = self.current_time + else: + segment_end = self.segment_start + + if segment_end > float(self.total_time): + segment_end = float(self.total_time) + + if self.segment_start > segment_end: + segment_end = self.segment_start + 10.0 + + if state == 'playing' or last_state == 'playing': # only report state='paused' once + client.update_watch_history(self.video_id, report_url + .format(st=format(self.segment_start, '.3f'), + et=format(segment_end, '.3f'), + state=state)) + self.context.log_debug( + 'Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' % + (self.video_id, + format(self.segment_start, '.3f'), + format(segment_end, '.3f'), + self.percent_complete, state)) + + self.segment_start = segment_end if self.abort_now(): break @@ -292,42 +292,40 @@ def run(self): self.context.get_playback_history().update(self.video_id, play_count, self.total_time, self.current_time, self.percent_complete) - if not refresh_only: - if is_logged_in: - - if settings.get_bool('youtube.playlist.watchlater.autoremove', True): - watch_later_id = access_manager.get_watch_later_id() - - if watch_later_id: - playlist_item_id = \ - client.get_playlist_item_id_of_video_id(playlist_id=watch_later_id, video_id=self.video_id) - if playlist_item_id: - json_data = client.remove_video_from_playlist(watch_later_id, playlist_item_id) - _ = self.provider.v3_handle_error(self.provider, self.context, json_data) - - history_playlist_id = access_manager.get_watch_history_id() - if history_playlist_id and history_playlist_id != 'HL': - json_data = client.add_video_to_playlist(history_playlist_id, self.video_id) - _ = self.provider.v3_handle_error(self.provider, self.context, json_data) - - # rate video - if settings.get_bool('youtube.post.play.rate', False): - do_rating = True - if not settings.get_bool('youtube.post.play.rate.playlists', False): - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - do_rating = int(playlist.size()) < 2 - - if do_rating: - json_data = client.get_video_rating(self.video_id) - success = self.provider.v3_handle_error(self.provider, self.context, json_data) - if success: - items = json_data.get('items', [{'rating': 'none'}]) - rating = items[0].get('rating', 'none') - if rating == 'none': - rating_match = \ - re.search('/(?P[^/]+)/(?P[^/]+)', '/%s/%s/' % - (self.video_id, rating)) - self.provider.yt_video.process('rate', self.provider, self.context, rating_match) + if not refresh_only and is_logged_in: + if settings.get_bool('youtube.playlist.watchlater.autoremove', True): + watch_later_id = access_manager.get_watch_later_id() + + if watch_later_id: + playlist_item_id = \ + client.get_playlist_item_id_of_video_id(playlist_id=watch_later_id, video_id=self.video_id) + if playlist_item_id: + json_data = client.remove_video_from_playlist(watch_later_id, playlist_item_id) + _ = self.provider.v3_handle_error(self.provider, self.context, json_data) + + history_playlist_id = access_manager.get_watch_history_id() + if history_playlist_id and history_playlist_id != 'HL': + json_data = client.add_video_to_playlist(history_playlist_id, self.video_id) + _ = self.provider.v3_handle_error(self.provider, self.context, json_data) + + # rate video + if settings.get_bool('youtube.post.play.rate', False): + do_rating = True + if not settings.get_bool('youtube.post.play.rate.playlists', False): + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + do_rating = int(playlist.size()) < 2 + + if do_rating: + json_data = client.get_video_rating(self.video_id) + success = self.provider.v3_handle_error(self.provider, self.context, json_data) + if success: + items = json_data.get('items', [{'rating': 'none'}]) + rating = items[0].get('rating', 'none') + if rating == 'none': + rating_match = \ + re.search('/(?P[^/]+)/(?P[^/]+)', '/%s/%s/' % + (self.video_id, rating)) + self.provider.yt_video.process('rate', self.provider, self.context, rating_match) playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) do_refresh = (int(playlist.size()) < 2) or (playlist.getposition() == -1) diff --git a/resources/lib/youtube_plugin/kodion/utils/storage.py b/resources/lib/youtube_plugin/kodion/utils/storage.py index 17ba64062..e764274bd 100644 --- a/resources/lib/youtube_plugin/kodion/utils/storage.py +++ b/resources/lib/youtube_plugin/kodion/utils/storage.py @@ -70,15 +70,14 @@ def _execute(self, needs_commit, query, values=None): Tests revealed that sqlite has problems to release the database in time. This happens no so often, but just to be sure, we try at least 3 times to execute out statement. """ - for tries in range(3): + for _ in range(3): try: return self._cursor.execute(query, values) except TypeError: return None except: time.sleep(0.1) - else: - return None + return None def _close(self): if self._file is not None: @@ -117,9 +116,10 @@ def _create_table(self): self._table_created = True def sync(self): - if self._cursor is not None and self._needs_commit: - self._needs_commit = False - return self._execute(False, 'COMMIT') + if not self._cursor or not self._needs_commit: + return None + self._needs_commit = False + return self._execute(False, 'COMMIT') def _set(self, item_id, item): def _encode(obj): diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py index 50e24b86a..09f0553fe 100644 --- a/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -15,20 +15,20 @@ class SystemVersion(object): def __init__(self, version, releasename, appname): - if not isinstance(version, tuple): - self._version = (0, 0, 0, 0) - else: - self._version = version + self._version = ( + version if version and isinstance(version, tuple) + else (0, 0, 0, 0) + ) - if not releasename or not isinstance(releasename, str): - self._releasename = 'UNKNOWN' - else: - self._releasename = releasename + self._releasename = ( + releasename if releasename and isinstance(releasename, str) + else 'UNKNOWN' + ) - if not appname or not isinstance(appname, str): - self._appname = 'UNKNOWN' - else: - self._appname = appname + self._appname = ( + appname if appname and isinstance(appname, str) + else 'UNKNOWN' + ) try: json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Application.GetProperties", ' diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index 7df58a97b..ac952299c 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -44,10 +44,10 @@ def _on_init(self): # users are now pasting keys into api_keys.json # try stripping whitespace and .apps.googleusercontent.com from keys and saving the result if they differ stripped_key, stripped_id, stripped_secret = self._strip_api_keys(j_key, j_id, j_secret) - if stripped_key and stripped_id and stripped_secret: - if (j_key != stripped_key) or (j_id != stripped_id) or (j_secret != stripped_secret): - self._json_api['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} - self._api_jstore.save(self._json_api) + if (stripped_key and stripped_id and stripped_secret + and (j_key != stripped_key or j_id != stripped_id or j_secret != stripped_secret)): + self._json_api['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} + self._api_jstore.save(self._json_api) original_key = self._settings.get_string('youtube.api.key') original_id = self._settings.get_string('youtube.api.id') @@ -81,18 +81,18 @@ def _on_init(self): refresh_token = self._settings.get_string('kodion.refresh_token', '') token_expires = self._settings.get_int('kodion.access_token.expires', -1) last_hash = self._settings.get_string('youtube.api.last.hash', '') - if not self._json_am['access_manager']['users'].get(user, {}).get('access_token') or \ - not self._json_am['access_manager']['users'].get(user, {}).get('refresh_token'): - if access_token and refresh_token: - self._json_am['access_manager']['users'][user]['access_token'] = access_token - self._json_am['access_manager']['users'][user]['refresh_token'] = refresh_token - self._json_am['access_manager']['users'][user]['token_expires'] = token_expires - if switch == 'own': - own_key_hash = self._get_key_set_hash('own') - if last_hash == self._get_key_set_hash('own', True) or \ - last_hash == own_key_hash: - self._json_am['access_manager']['users'][user]['last_key_hash'] = own_key_hash - self._am_jstore.save(self._json_am) + if ((not self._json_am['access_manager']['users'].get(user, {}).get('access_token') + or not self._json_am['access_manager']['users'].get(user, {}).get('refresh_token')) + and access_token and refresh_token): + self._json_am['access_manager']['users'][user]['access_token'] = access_token + self._json_am['access_manager']['users'][user]['refresh_token'] = refresh_token + self._json_am['access_manager']['users'][user]['token_expires'] = token_expires + if switch == 'own': + own_key_hash = self._get_key_set_hash('own') + if (last_hash == self._get_key_set_hash('own', True) + or last_hash == own_key_hash): + self._json_am['access_manager']['users'][user]['last_key_hash'] = own_key_hash + self._am_jstore.save(self._json_am) if access_token or refresh_token or last_hash: self._settings.set_string('kodion.access_token', '') self._settings.set_string('kodion.refresh_token', '') @@ -121,26 +121,23 @@ def has_own_api_keys(self): own_key = self._json_api['keys']['personal']['api_key'] own_id = self._json_api['keys']['personal']['client_id'] own_secret = self._json_api['keys']['personal']['client_secret'] - return False if not own_key or \ - not own_id or \ - not own_secret else True + return own_key and own_id and own_secret def get_api_keys(self, switch): self._json_api = self._api_jstore.get_data() + if switch == 'developer': + return self._json_api['keys'][switch] if switch == 'youtube-tv': - api_key = b64decode(key_sets['youtube-tv']['key']).decode('utf-8'), - client_id = u''.join([b64decode(key_sets['youtube-tv']['id']).decode('utf-8'), u'.apps.googleusercontent.com']) - client_secret = b64decode(key_sets['youtube-tv']['secret']).decode('utf-8') - elif switch == 'developer': - self._json_api = self._api_jstore.get_data() - return self._json_api['keys']['developer'] + api_key = b64decode(key_sets[switch]['key']).decode('utf-8'), + client_id = ''.join([b64decode(key_sets[switch]['id']).decode('utf-8'), '.apps.googleusercontent.com']) + client_secret = b64decode(key_sets[switch]['secret']).decode('utf-8') elif switch == 'own': api_key = self._json_api['keys']['personal']['api_key'] - client_id = u''.join([self._json_api['keys']['personal']['client_id'], u'.apps.googleusercontent.com']) + client_id = ''.join([self._json_api['keys']['personal']['client_id'], '.apps.googleusercontent.com']) client_secret = self._json_api['keys']['personal']['client_secret'] else: api_key = b64decode(key_sets['provided'][switch]['key']).decode('utf-8') - client_id = u''.join([b64decode(key_sets['provided'][switch]['id']).decode('utf-8'), u'.apps.googleusercontent.com']) + client_id = ''.join([b64decode(key_sets['provided'][switch]['id']).decode('utf-8'), '.apps.googleusercontent.com']) client_secret = b64decode(key_sets['provided'][switch]['secret']).decode('utf-8') return api_key, client_id, client_secret @@ -152,14 +149,13 @@ def _api_keys_changed(self, switch): if last_set_hash != current_set_hash: self.changed = True return current_set_hash - else: - self.changed = False - return None + self.changed = False + return None def _get_key_set_hash(self, switch, old=False): api_key, client_id, client_secret = self.get_api_keys(switch) if old and switch == 'own': - client_id = client_id.replace(u'.apps.googleusercontent.com', u'') + client_id = client_id.replace('.apps.googleusercontent.com', '') m = md5() m.update(api_key.encode('utf-8')) m.update(client_id.encode('utf-8')) @@ -209,9 +205,9 @@ def _strip_api_keys(self, api_key, client_id, client_secret): return return_key, return_id, return_secret -notification_data = {'use_httpd': (__settings.use_mpd_videos() or - __settings.use_mpd_live_streams()) or - (__settings.api_config_page()), +notification_data = {'use_httpd': (__settings.use_mpd_videos() + or __settings.use_mpd_live_streams() + or __settings.api_config_page()), 'httpd_port': __settings.httpd_port(), 'whitelist': __settings.httpd_whitelist(), 'httpd_address': __settings.httpd_listen() @@ -227,9 +223,7 @@ def _strip_api_keys(self, api_key, client_id, client_secret): api = dict() youtube_tv = dict() -_current_switch = _api_check.get_current_switch() - -api['key'], api['id'], api['secret'] = _api_check.get_api_keys(_current_switch) +api['key'], api['id'], api['secret'] = _api_check.get_api_keys(_api_check.get_current_switch()) youtube_tv['key'], youtube_tv['id'], youtube_tv['secret'] = _api_check.get_api_keys('youtube-tv') diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 2948c4992..4135ac025 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -323,22 +323,20 @@ def _get_config_type(self, client_id, client_secret=None): using_conf_main = ((client_id == self.CONFIGS['main'].get('id')) and (client_secret == self.CONFIGS['main'].get('secret'))) if not using_conf_main and not using_conf_tv: return 'None' - elif using_conf_tv: + if using_conf_tv: return 'YouTube-TV' - elif using_conf_main: + if using_conf_main: return 'YouTube-Kodi' - else: - return 'Unknown' + return 'Unknown' @staticmethod def _get_response_dump(response, json_data=None): if json_data: return json_data - else: + try: + return response.json() + except ValueError: try: - return response.json() - except ValueError: - try: - return response.text - except: - return 'None' + return response.text + except: + return 'None' diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 81afc78ab..d8b1eeb64 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -138,6 +138,7 @@ def _update_videos(self, video_ids, live_details=False, suppress_errors=False): if self.handle_error(json_data, suppress_errors) or suppress_errors: return result + return {} @staticmethod def _make_list_of_50(list_of_ids): @@ -188,6 +189,7 @@ def _update_playlists(self, playlists_ids): if self.handle_error(json_data): return result + return {} def get_playlists(self, playlists_ids): list_of_50s = self._make_list_of_50(playlists_ids) diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py index 8db38e1dd..ae89edf24 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py @@ -32,7 +32,7 @@ def get_signature(self, signature): json_script_engine = JsonScriptEngine(json_script) return json_script_engine.execute(signature) - return u'' + return '' def _load_javascript(self, javascript): function_name = self._find_signature_function_name(javascript) diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 2286d2c3d..ef2f4ab2b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -221,7 +221,8 @@ def _get(self, lang_code='en', language=None, no_asr=False): base_url = None if base_url: - subtitle_url = self._set_query_param(base_url, + subtitle_url = self._set_query_param( + base_url, ('type', 'track'), ('fmt', 'vtt'), ('tlang', lang_code) if has_translation else (None, None), diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 386b4b7a0..e10804369 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -139,8 +139,7 @@ def update_channel_infos(provider, context, channel_id_dict, subscription_id_dic yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id) if context.get_path() == '/subscriptions/list/': - channel = title.lower() - channel = channel.replace(',', '') + channel = title.lower().replace(',', '') if channel in filter_list: yt_context_menu.append_remove_my_subscriptions_filter(context_menu, provider, context, title) else: @@ -295,16 +294,15 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N video_item.set_scheduled_start_utc(datetime) start_date, start_time = utils.datetime_parser.get_scheduled_start(datetime) if start_date: - title = u'({live} {date}@{time}) {title}' \ + title = '({live} {date}@{time}) {title}' \ .format(live=context.localize(provider.LOCAL_MAP['youtube.live']), date=start_date, time=start_time, title=snippet['title']) else: - title = u'({live} @ {time}) {title}' \ - .format(live=context.localize(provider.LOCAL_MAP['youtube.live']), date=start_date, time=start_time, title=snippet['title']) + title = '({live} @ {time}) {title}' \ + .format(live=context.localize(provider.LOCAL_MAP['youtube.live']), time=start_time, title=snippet['title']) video_item.set_title(title) - else: - # set the title - if not video_item.get_title(): - video_item.set_title(snippet['title']) + # set the title + elif not video_item.get_title(): + video_item.set_title(snippet['title']) """ This is experimental. We try to get the most information out of the title of a video. @@ -401,27 +399,25 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N if playlist_match: playlist_id = playlist_match.group('playlist_id') # we support all playlist except 'Watch History' - if playlist_id: - if playlist_id != 'HL' and playlist_id.strip().lower() != 'wl': - playlist_item_id = playlist_item_id_dict[video_id] - video_item.set_playlist_id(playlist_id) - video_item.set_playlist_item_id(playlist_item_id) - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove']), - 'RunPlugin(%s)' % context.create_uri( - ['playlist', 'remove', 'video'], - {'playlist_id': playlist_id, 'video_id': playlist_item_id, - 'video_name': video_item.get_name()}))) + if playlist_id and playlist_id != 'HL' and playlist_id.strip().lower() != 'wl': + playlist_item_id = playlist_item_id_dict[video_id] + video_item.set_playlist_id(playlist_id) + video_item.set_playlist_item_id(playlist_item_id) + context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove']), + 'RunPlugin(%s)' % context.create_uri( + ['playlist', 'remove', 'video'], + {'playlist_id': playlist_id, 'video_id': playlist_item_id, + 'video_name': video_item.get_name()}))) is_history = re.match('^/special/watch_history_tv/$', context.get_path()) if is_history: yt_context_menu.append_clear_watch_history(context_menu, provider, context) - # got to [CHANNEL] - if channel_id and channel_name: - # only if we are not directly in the channel provide a jump to the channel - if kodion.utils.create_path('channel', channel_id) != context.get_path(): - video_item.set_channel_id(channel_id) - yt_context_menu.append_go_to_channel(context_menu, provider, context, channel_id, channel_name) + # got to [CHANNEL], only if we are not directly in the channel provide a jump to the channel + if (channel_id and channel_name and + kodion.utils.create_path('channel', channel_id) != context.get_path()): + video_item.set_channel_id(channel_id) + yt_context_menu.append_go_to_channel(context_menu, provider, context, channel_id, channel_name) if provider.is_logged_in(): # subscribe to the channel of the video @@ -630,10 +626,9 @@ def get_shelf_index_by_title(context, json_data, shelf_title): context.log_debug('Found shelf index |{index}| for |{title}|'.format(index=str(shelf_index), title=shelf_title)) break - if shelf_index is not None: - if 0 > shelf_index >= len(contents): - context.log_debug('Shelf index |{index}| out of range |0-{content_length}|'.format(index=str(shelf_index), content_length=str(len(contents)))) - shelf_index = None + if shelf_index is not None and 0 > shelf_index >= len(contents): + context.log_debug('Shelf index |{index}| out of range |0-{content_length}|'.format(index=str(shelf_index), content_length=str(len(contents)))) + shelf_index = None return shelf_index diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index e3ac3d70c..4f7621e9a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -2,7 +2,7 @@ """ Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2016-present plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. @@ -1068,7 +1068,8 @@ def _normalize_url(url): url = urljoin('https://www.youtube.com', url) return url - def _load_hls_manifest(self, url, live_type=None, meta_info=None, headers=None, playback_stats=None): + def _load_hls_manifest(self, url, live_type=None, meta_info=None, + headers=None, playback_stats=None): if not url: return [] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py b/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py index e1b56666f..941e86e34 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py @@ -120,10 +120,7 @@ def append_add_my_subscriptions_filter(context_menu, provider, context, channel_ def append_rate_video(context_menu, provider, context, video_id, refresh_container=False): - if refresh_container: - refresh_container = '1' - else: - refresh_container = '0' + refresh_container = '1' if refresh_container else '0' context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.rate']), 'RunPlugin(%s)' % context.create_uri(['video', 'rate'], {'video_id': video_id, @@ -158,7 +155,7 @@ def append_refresh(context_menu, provider, context): context_menu.append((context.localize(provider.LOCAL_MAP['youtube.refresh']), 'Container.Refresh')) -def append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name=u''): +def append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name=''): if channel_name: text = context.localize(provider.LOCAL_MAP['youtube.subscribe_to']) % context.get_ui().bold(channel_name) context_menu.append( diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index faa621b68..f06b86509 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -28,12 +28,11 @@ def _do_logout(): refresh_tokens = list(set(refresh_tokens)) for _refresh_token in refresh_tokens: provider.get_client(context).revoke(_refresh_token) - else: - if signout_access_manager.has_refresh_token(): - refresh_tokens = signout_access_manager.get_refresh_token().split('|') - refresh_tokens = list(set(refresh_tokens)) - for _refresh_token in refresh_tokens: - provider.get_client(context).revoke(_refresh_token) + elif signout_access_manager.has_refresh_token(): + refresh_tokens = signout_access_manager.get_refresh_token().split('|') + refresh_tokens = list(set(refresh_tokens)) + for _refresh_token in refresh_tokens: + provider.get_client(context).revoke(_refresh_token) provider.reset_client() @@ -70,7 +69,7 @@ def _do_login(_for_tv=False): steps = ((10 * 60 * 1000) // interval) # 10 Minutes dialog.set_total(steps) - for i in range(steps): + for _ in range(steps): dialog.update() try: if _for_tv: @@ -97,18 +96,18 @@ def _do_login(_for_tv=False): _expires_in = 0 return _access_token, _expires_in, _refresh_token - elif json_data['error'] != u'authorization_pending': + if json_data['error'] != 'authorization_pending': message = json_data['error'] title = '%s: %s' % (context.get_name(), message) context.get_ui().show_notification(message, title) context.log_error('Error requesting access token: |%s|' % message) if dialog.is_aborted(): - dialog.close() - return '', 0, '' + break context.sleep(interval) dialog.close() + return '', 0, '' if mode == 'out': _do_logout() diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 9de573632..e62163555 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -63,8 +63,8 @@ def play_video(provider, context): if video_stream is None: return False - is_video = True if video_stream.get('video') else False - is_live = video_stream.get('Live') is True + is_video = video_stream.get('video') + is_live = video_stream.get('Live') if is_video and video_stream['video'].get('rtmpe', False): message = context.localize(provider.LOCAL_MAP['youtube.error.rtmpe_not_supported']) @@ -225,8 +225,8 @@ def _load_videos(_page_token='', _progress_dialog=None): if (context.get_param('play', '') == '1') and (context.get_handle() == -1): player.play(playlist_index=playlist_position) - return - elif context.get_param('play', '') == '1': + return False + if context.get_param('play', '') == '1': return videos[playlist_position] return True @@ -257,5 +257,5 @@ def play_channel_live(provider, context): if context.get_handle() == -1: player.play(playlist_index=0) - else: - return video_item + return False + return video_item diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index cdf9bbc0d..6d1a32d7d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -55,9 +55,8 @@ def _process_add_video(provider, context, keymap_action=False): context.get_ui().set_focus_next_item() return True - else: - context.log_debug('Cannot add to playlist id |%s|' % playlist_id) + context.log_debug('Cannot add to playlist id |%s|' % playlist_id) return False @@ -71,12 +70,14 @@ def _process_remove_video(provider, context): video_id = context.get_param('video_id', '') video_name = context.get_param('video_name', '') - if not playlist_id and not video_id: # keymap support - if listitem_playlist_id and listitem_playlist_id.startswith('PL') \ - and listitem_playlist_item_id and listitem_playlist_item_id.startswith('UE'): - playlist_id = listitem_playlist_id - video_id = listitem_playlist_item_id - keymap_action = True + # keymap support + if (not playlist_id and not video_id and listitem_playlist_id + and listitem_playlist_id.startswith('PL') + and listitem_playlist_item_id + and listitem_playlist_item_id.startswith('UE')): + playlist_id = listitem_playlist_id + video_id = listitem_playlist_item_id + keymap_action = True if not playlist_id: raise kodion.KodionException('Playlist/Remove: missing playlist_id') @@ -209,17 +210,16 @@ def _process_select_playlist(provider, context): new_context = context.clone(new_params=new_params) _process_add_video(provider, new_context, keymap_action) break - elif result == 'playlist.next': + if result == 'playlist.next': continue - elif result != -1: + if result != -1: new_params = {} new_params.update(context.get_params()) new_params['playlist_id'] = result new_context = context.clone(new_params=new_params) _process_add_video(provider, new_context, keymap_action) break - else: - break + break def _process_rename_playlist(provider, context): @@ -287,17 +287,16 @@ def _history_playlist_id_change(context, method): def process(method, category, provider, context): if method == 'add' and category == 'video': return _process_add_video(provider, context) - elif method == 'remove' and category == 'video': + if method == 'remove' and category == 'video': return _process_remove_video(provider, context) - elif method == 'remove' and category == 'playlist': + if method == 'remove' and category == 'playlist': return _process_remove_playlist(provider, context) - elif method == 'select' and category == 'playlist': + if method == 'select' and category == 'playlist': return _process_select_playlist(provider, context) - elif method == 'rename' and category == 'playlist': + if method == 'rename' and category == 'playlist': return _process_rename_playlist(provider, context) - elif (method == 'set' or method == 'remove') and category == 'watchlater': + if method in {'set', 'remove'} and category == 'watchlater': return _watchlater_playlist_id_change(context, method) - elif (method == 'set' or method == 'remove') and category == 'history': + if method in {'set', 'remove'} and category == 'history': return _history_playlist_id_change(context, method) - else: - raise kodion.KodionException("Unknown category '%s' or method '%s'" % (category, method)) + raise kodion.KodionException("Unknown category '%s' or method '%s'" % (category, method)) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 9b5bb4b7e..559cba4d2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -11,49 +11,49 @@ from ...kodion.utils import ip_api -DEFAULT_LANGUAGES = {u'items': [{u'snippet': {u'name': u'Afrikaans', u'hl': u'af'}, u'id': u'af'}, {u'snippet': {u'name': u'Azerbaijani', u'hl': u'az'}, u'id': u'az'}, {u'snippet': {u'name': u'Indonesian', u'hl': u'id'}, u'id': u'id'}, {u'snippet': {u'name': u'Malay', u'hl': u'ms'}, u'id': u'ms'}, - {u'snippet': {u'name': u'Catalan', u'hl': u'ca'}, u'id': u'ca'}, {u'snippet': {u'name': u'Czech', u'hl': u'cs'}, u'id': u'cs'}, {u'snippet': {u'name': u'Danish', u'hl': u'da'}, u'id': u'da'}, {u'snippet': {u'name': u'German', u'hl': u'de'}, u'id': u'de'}, - {u'snippet': {u'name': u'Estonian', u'hl': u'et'}, u'id': u'et'}, {u'snippet': {u'name': u'English (United Kingdom)', u'hl': u'en-GB'}, u'id': u'en-GB'}, {u'snippet': {u'name': u'English', u'hl': u'en'}, u'id': u'en'}, - {u'snippet': {u'name': u'Spanish (Spain)', u'hl': u'es'}, u'id': u'es'}, {u'snippet': {u'name': u'Spanish (Latin America)', u'hl': u'es-419'}, u'id': u'es-419'}, {u'snippet': {u'name': u'Basque', u'hl': u'eu'}, u'id': u'eu'}, - {u'snippet': {u'name': u'Filipino', u'hl': u'fil'}, u'id': u'fil'}, {u'snippet': {u'name': u'French', u'hl': u'fr'}, u'id': u'fr'}, {u'snippet': {u'name': u'French (Canada)', u'hl': u'fr-CA'}, u'id': u'fr-CA'}, {u'snippet': {u'name': u'Galician', u'hl': u'gl'}, u'id': u'gl'}, - {u'snippet': {u'name': u'Croatian', u'hl': u'hr'}, u'id': u'hr'}, {u'snippet': {u'name': u'Zulu', u'hl': u'zu'}, u'id': u'zu'}, {u'snippet': {u'name': u'Icelandic', u'hl': u'is'}, u'id': u'is'}, {u'snippet': {u'name': u'Italian', u'hl': u'it'}, u'id': u'it'}, - {u'snippet': {u'name': u'Swahili', u'hl': u'sw'}, u'id': u'sw'}, {u'snippet': {u'name': u'Latvian', u'hl': u'lv'}, u'id': u'lv'}, {u'snippet': {u'name': u'Lithuanian', u'hl': u'lt'}, u'id': u'lt'}, {u'snippet': {u'name': u'Hungarian', u'hl': u'hu'}, u'id': u'hu'}, - {u'snippet': {u'name': u'Dutch', u'hl': u'nl'}, u'id': u'nl'}, {u'snippet': {u'name': u'Norwegian', u'hl': u'no'}, u'id': u'no'}, {u'snippet': {u'name': u'Uzbek', u'hl': u'uz'}, u'id': u'uz'}, {u'snippet': {u'name': u'Polish', u'hl': u'pl'}, u'id': u'pl'}, - {u'snippet': {u'name': u'Portuguese (Portugal)', u'hl': u'pt-PT'}, u'id': u'pt-PT'}, {u'snippet': {u'name': u'Portuguese (Brazil)', u'hl': u'pt'}, u'id': u'pt'}, {u'snippet': {u'name': u'Romanian', u'hl': u'ro'}, u'id': u'ro'}, - {u'snippet': {u'name': u'Albanian', u'hl': u'sq'}, u'id': u'sq'}, {u'snippet': {u'name': u'Slovak', u'hl': u'sk'}, u'id': u'sk'}, {u'snippet': {u'name': u'Slovenian', u'hl': u'sl'}, u'id': u'sl'}, {u'snippet': {u'name': u'Finnish', u'hl': u'fi'}, u'id': u'fi'}, - {u'snippet': {u'name': u'Swedish', u'hl': u'sv'}, u'id': u'sv'}, {u'snippet': {u'name': u'Vietnamese', u'hl': u'vi'}, u'id': u'vi'}, {u'snippet': {u'name': u'Turkish', u'hl': u'tr'}, u'id': u'tr'}, {u'snippet': {u'name': u'Bulgarian', u'hl': u'bg'}, u'id': u'bg'}, - {u'snippet': {u'name': u'Kyrgyz', u'hl': u'ky'}, u'id': u'ky'}, {u'snippet': {u'name': u'Kazakh', u'hl': u'kk'}, u'id': u'kk'}, {u'snippet': {u'name': u'Macedonian', u'hl': u'mk'}, u'id': u'mk'}, {u'snippet': {u'name': u'Mongolian', u'hl': u'mn'}, u'id': u'mn'}, - {u'snippet': {u'name': u'Russian', u'hl': u'ru'}, u'id': u'ru'}, {u'snippet': {u'name': u'Serbian', u'hl': u'sr'}, u'id': u'sr'}, {u'snippet': {u'name': u'Ukrainian', u'hl': u'uk'}, u'id': u'uk'}, {u'snippet': {u'name': u'Greek', u'hl': u'el'}, u'id': u'el'}, - {u'snippet': {u'name': u'Armenian', u'hl': u'hy'}, u'id': u'hy'}, {u'snippet': {u'name': u'Hebrew', u'hl': u'iw'}, u'id': u'iw'}, {u'snippet': {u'name': u'Urdu', u'hl': u'ur'}, u'id': u'ur'}, {u'snippet': {u'name': u'Arabic', u'hl': u'ar'}, u'id': u'ar'}, - {u'snippet': {u'name': u'Persian', u'hl': u'fa'}, u'id': u'fa'}, {u'snippet': {u'name': u'Nepali', u'hl': u'ne'}, u'id': u'ne'}, {u'snippet': {u'name': u'Marathi', u'hl': u'mr'}, u'id': u'mr'}, {u'snippet': {u'name': u'Hindi', u'hl': u'hi'}, u'id': u'hi'}, - {u'snippet': {u'name': u'Bengali', u'hl': u'bn'}, u'id': u'bn'}, {u'snippet': {u'name': u'Punjabi', u'hl': u'pa'}, u'id': u'pa'}, {u'snippet': {u'name': u'Gujarati', u'hl': u'gu'}, u'id': u'gu'}, {u'snippet': {u'name': u'Tamil', u'hl': u'ta'}, u'id': u'ta'}, - {u'snippet': {u'name': u'Telugu', u'hl': u'te'}, u'id': u'te'}, {u'snippet': {u'name': u'Kannada', u'hl': u'kn'}, u'id': u'kn'}, {u'snippet': {u'name': u'Malayalam', u'hl': u'ml'}, u'id': u'ml'}, {u'snippet': {u'name': u'Sinhala', u'hl': u'si'}, u'id': u'si'}, - {u'snippet': {u'name': u'Thai', u'hl': u'th'}, u'id': u'th'}, {u'snippet': {u'name': u'Lao', u'hl': u'lo'}, u'id': u'lo'}, {u'snippet': {u'name': u'Myanmar (Burmese)', u'hl': u'my'}, u'id': u'my'}, {u'snippet': {u'name': u'Georgian', u'hl': u'ka'}, u'id': u'ka'}, - {u'snippet': {u'name': u'Amharic', u'hl': u'am'}, u'id': u'am'}, {u'snippet': {u'name': u'Khmer', u'hl': u'km'}, u'id': u'km'}, {u'snippet': {u'name': u'Chinese', u'hl': u'zh-CN'}, u'id': u'zh-CN'}, {u'snippet': {u'name': u'Chinese (Taiwan)', u'hl': u'zh-TW'}, u'id': u'zh-TW'}, - {u'snippet': {u'name': u'Chinese (Hong Kong)', u'hl': u'zh-HK'}, u'id': u'zh-HK'}, {u'snippet': {u'name': u'Japanese', u'hl': u'ja'}, u'id': u'ja'}, {u'snippet': {u'name': u'Korean', u'hl': u'ko'}, u'id': u'ko'}]} -DEFAULT_REGIONS = {u'items': [{u'snippet': {u'gl': u'DZ', u'name': u'Algeria'}, u'id': u'DZ'}, {u'snippet': {u'gl': u'AR', u'name': u'Argentina'}, u'id': u'AR'}, {u'snippet': {u'gl': u'AU', u'name': u'Australia'}, u'id': u'AU'}, {u'snippet': {u'gl': u'AT', u'name': u'Austria'}, u'id': u'AT'}, - {u'snippet': {u'gl': u'AZ', u'name': u'Azerbaijan'}, u'id': u'AZ'}, {u'snippet': {u'gl': u'BH', u'name': u'Bahrain'}, u'id': u'BH'}, {u'snippet': {u'gl': u'BY', u'name': u'Belarus'}, u'id': u'BY'}, {u'snippet': {u'gl': u'BE', u'name': u'Belgium'}, u'id': u'BE'}, - {u'snippet': {u'gl': u'BA', u'name': u'Bosnia and Herzegovina'}, u'id': u'BA'}, {u'snippet': {u'gl': u'BR', u'name': u'Brazil'}, u'id': u'BR'}, {u'snippet': {u'gl': u'BG', u'name': u'Bulgaria'}, u'id': u'BG'}, {u'snippet': {u'gl': u'CA', u'name': u'Canada'}, u'id': u'CA'}, - {u'snippet': {u'gl': u'CL', u'name': u'Chile'}, u'id': u'CL'}, {u'snippet': {u'gl': u'CO', u'name': u'Colombia'}, u'id': u'CO'}, {u'snippet': {u'gl': u'HR', u'name': u'Croatia'}, u'id': u'HR'}, {u'snippet': {u'gl': u'CZ', u'name': u'Czech Republic'}, u'id': u'CZ'}, - {u'snippet': {u'gl': u'DK', u'name': u'Denmark'}, u'id': u'DK'}, {u'snippet': {u'gl': u'EG', u'name': u'Egypt'}, u'id': u'EG'}, {u'snippet': {u'gl': u'EE', u'name': u'Estonia'}, u'id': u'EE'}, {u'snippet': {u'gl': u'FI', u'name': u'Finland'}, u'id': u'FI'}, - {u'snippet': {u'gl': u'FR', u'name': u'France'}, u'id': u'FR'}, {u'snippet': {u'gl': u'GE', u'name': u'Georgia'}, u'id': u'GE'}, {u'snippet': {u'gl': u'DE', u'name': u'Germany'}, u'id': u'DE'}, {u'snippet': {u'gl': u'GH', u'name': u'Ghana'}, u'id': u'GH'}, - {u'snippet': {u'gl': u'GR', u'name': u'Greece'}, u'id': u'GR'}, {u'snippet': {u'gl': u'HK', u'name': u'Hong Kong'}, u'id': u'HK'}, {u'snippet': {u'gl': u'HU', u'name': u'Hungary'}, u'id': u'HU'}, {u'snippet': {u'gl': u'IS', u'name': u'Iceland'}, u'id': u'IS'}, - {u'snippet': {u'gl': u'IN', u'name': u'India'}, u'id': u'IN'}, {u'snippet': {u'gl': u'ID', u'name': u'Indonesia'}, u'id': u'ID'}, {u'snippet': {u'gl': u'IQ', u'name': u'Iraq'}, u'id': u'IQ'}, {u'snippet': {u'gl': u'IE', u'name': u'Ireland'}, u'id': u'IE'}, - {u'snippet': {u'gl': u'IL', u'name': u'Israel'}, u'id': u'IL'}, {u'snippet': {u'gl': u'IT', u'name': u'Italy'}, u'id': u'IT'}, {u'snippet': {u'gl': u'JM', u'name': u'Jamaica'}, u'id': u'JM'}, {u'snippet': {u'gl': u'JP', u'name': u'Japan'}, u'id': u'JP'}, - {u'snippet': {u'gl': u'JO', u'name': u'Jordan'}, u'id': u'JO'}, {u'snippet': {u'gl': u'KZ', u'name': u'Kazakhstan'}, u'id': u'KZ'}, {u'snippet': {u'gl': u'KE', u'name': u'Kenya'}, u'id': u'KE'}, {u'snippet': {u'gl': u'KW', u'name': u'Kuwait'}, u'id': u'KW'}, - {u'snippet': {u'gl': u'LV', u'name': u'Latvia'}, u'id': u'LV'}, {u'snippet': {u'gl': u'LB', u'name': u'Lebanon'}, u'id': u'LB'}, {u'snippet': {u'gl': u'LY', u'name': u'Libya'}, u'id': u'LY'}, {u'snippet': {u'gl': u'LT', u'name': u'Lithuania'}, u'id': u'LT'}, - {u'snippet': {u'gl': u'LU', u'name': u'Luxembourg'}, u'id': u'LU'}, {u'snippet': {u'gl': u'MK', u'name': u'Macedonia'}, u'id': u'MK'}, {u'snippet': {u'gl': u'MY', u'name': u'Malaysia'}, u'id': u'MY'}, {u'snippet': {u'gl': u'MX', u'name': u'Mexico'}, u'id': u'MX'}, - {u'snippet': {u'gl': u'ME', u'name': u'Montenegro'}, u'id': u'ME'}, {u'snippet': {u'gl': u'MA', u'name': u'Morocco'}, u'id': u'MA'}, {u'snippet': {u'gl': u'NP', u'name': u'Nepal'}, u'id': u'NP'}, {u'snippet': {u'gl': u'NL', u'name': u'Netherlands'}, u'id': u'NL'}, - {u'snippet': {u'gl': u'NZ', u'name': u'New Zealand'}, u'id': u'NZ'}, {u'snippet': {u'gl': u'NG', u'name': u'Nigeria'}, u'id': u'NG'}, {u'snippet': {u'gl': u'NO', u'name': u'Norway'}, u'id': u'NO'}, {u'snippet': {u'gl': u'OM', u'name': u'Oman'}, u'id': u'OM'}, - {u'snippet': {u'gl': u'PK', u'name': u'Pakistan'}, u'id': u'PK'}, {u'snippet': {u'gl': u'PE', u'name': u'Peru'}, u'id': u'PE'}, {u'snippet': {u'gl': u'PH', u'name': u'Philippines'}, u'id': u'PH'}, {u'snippet': {u'gl': u'PL', u'name': u'Poland'}, u'id': u'PL'}, - {u'snippet': {u'gl': u'PT', u'name': u'Portugal'}, u'id': u'PT'}, {u'snippet': {u'gl': u'PR', u'name': u'Puerto Rico'}, u'id': u'PR'}, {u'snippet': {u'gl': u'QA', u'name': u'Qatar'}, u'id': u'QA'}, {u'snippet': {u'gl': u'RO', u'name': u'Romania'}, u'id': u'RO'}, - {u'snippet': {u'gl': u'RU', u'name': u'Russia'}, u'id': u'RU'}, {u'snippet': {u'gl': u'SA', u'name': u'Saudi Arabia'}, u'id': u'SA'}, {u'snippet': {u'gl': u'SN', u'name': u'Senegal'}, u'id': u'SN'}, {u'snippet': {u'gl': u'RS', u'name': u'Serbia'}, u'id': u'RS'}, - {u'snippet': {u'gl': u'SG', u'name': u'Singapore'}, u'id': u'SG'}, {u'snippet': {u'gl': u'SK', u'name': u'Slovakia'}, u'id': u'SK'}, {u'snippet': {u'gl': u'SI', u'name': u'Slovenia'}, u'id': u'SI'}, {u'snippet': {u'gl': u'ZA', u'name': u'South Africa'}, u'id': u'ZA'}, - {u'snippet': {u'gl': u'KR', u'name': u'South Korea'}, u'id': u'KR'}, {u'snippet': {u'gl': u'ES', u'name': u'Spain'}, u'id': u'ES'}, {u'snippet': {u'gl': u'LK', u'name': u'Sri Lanka'}, u'id': u'LK'}, {u'snippet': {u'gl': u'SE', u'name': u'Sweden'}, u'id': u'SE'}, - {u'snippet': {u'gl': u'CH', u'name': u'Switzerland'}, u'id': u'CH'}, {u'snippet': {u'gl': u'TW', u'name': u'Taiwan'}, u'id': u'TW'}, {u'snippet': {u'gl': u'TZ', u'name': u'Tanzania'}, u'id': u'TZ'}, {u'snippet': {u'gl': u'TH', u'name': u'Thailand'}, u'id': u'TH'}, - {u'snippet': {u'gl': u'TN', u'name': u'Tunisia'}, u'id': u'TN'}, {u'snippet': {u'gl': u'TR', u'name': u'Turkey'}, u'id': u'TR'}, {u'snippet': {u'gl': u'UG', u'name': u'Uganda'}, u'id': u'UG'}, {u'snippet': {u'gl': u'UA', u'name': u'Ukraine'}, u'id': u'UA'}, - {u'snippet': {u'gl': u'AE', u'name': u'United Arab Emirates'}, u'id': u'AE'}, {u'snippet': {u'gl': u'GB', u'name': u'United Kingdom'}, u'id': u'GB'}, {u'snippet': {u'gl': u'US', u'name': u'United States'}, u'id': u'US'}, {u'snippet': {u'gl': u'VN', u'name': u'Vietnam'}, u'id': u'VN'}, - {u'snippet': {u'gl': u'YE', u'name': u'Yemen'}, u'id': u'YE'}, {u'snippet': {u'gl': u'ZW', u'name': u'Zimbabwe'}, u'id': u'ZW'}]} +DEFAULT_LANGUAGES = {'items': [{'snippet': {'name': 'Afrikaans', 'hl': 'af'}, 'id': 'af'}, {'snippet': {'name': 'Azerbaijani', 'hl': 'az'}, 'id': 'az'}, {'snippet': {'name': 'Indonesian', 'hl': 'id'}, 'id': 'id'}, {'snippet': {'name': 'Malay', 'hl': 'ms'}, 'id': 'ms'}, + {'snippet': {'name': 'Catalan', 'hl': 'ca'}, 'id': 'ca'}, {'snippet': {'name': 'Czech', 'hl': 'cs'}, 'id': 'cs'}, {'snippet': {'name': 'Danish', 'hl': 'da'}, 'id': 'da'}, {'snippet': {'name': 'German', 'hl': 'de'}, 'id': 'de'}, + {'snippet': {'name': 'Estonian', 'hl': 'et'}, 'id': 'et'}, {'snippet': {'name': 'English (United Kingdom)', 'hl': 'en-GB'}, 'id': 'en-GB'}, {'snippet': {'name': 'English', 'hl': 'en'}, 'id': 'en'}, + {'snippet': {'name': 'Spanish (Spain)', 'hl': 'es'}, 'id': 'es'}, {'snippet': {'name': 'Spanish (Latin America)', 'hl': 'es-419'}, 'id': 'es-419'}, {'snippet': {'name': 'Basque', 'hl': 'eu'}, 'id': 'eu'}, + {'snippet': {'name': 'Filipino', 'hl': 'fil'}, 'id': 'fil'}, {'snippet': {'name': 'French', 'hl': 'fr'}, 'id': 'fr'}, {'snippet': {'name': 'French (Canada)', 'hl': 'fr-CA'}, 'id': 'fr-CA'}, {'snippet': {'name': 'Galician', 'hl': 'gl'}, 'id': 'gl'}, + {'snippet': {'name': 'Croatian', 'hl': 'hr'}, 'id': 'hr'}, {'snippet': {'name': 'Zulu', 'hl': 'zu'}, 'id': 'zu'}, {'snippet': {'name': 'Icelandic', 'hl': 'is'}, 'id': 'is'}, {'snippet': {'name': 'Italian', 'hl': 'it'}, 'id': 'it'}, + {'snippet': {'name': 'Swahili', 'hl': 'sw'}, 'id': 'sw'}, {'snippet': {'name': 'Latvian', 'hl': 'lv'}, 'id': 'lv'}, {'snippet': {'name': 'Lithuanian', 'hl': 'lt'}, 'id': 'lt'}, {'snippet': {'name': 'Hungarian', 'hl': 'hu'}, 'id': 'hu'}, + {'snippet': {'name': 'Dutch', 'hl': 'nl'}, 'id': 'nl'}, {'snippet': {'name': 'Norwegian', 'hl': 'no'}, 'id': 'no'}, {'snippet': {'name': 'Uzbek', 'hl': 'uz'}, 'id': 'uz'}, {'snippet': {'name': 'Polish', 'hl': 'pl'}, 'id': 'pl'}, + {'snippet': {'name': 'Portuguese (Portugal)', 'hl': 'pt-PT'}, 'id': 'pt-PT'}, {'snippet': {'name': 'Portuguese (Brazil)', 'hl': 'pt'}, 'id': 'pt'}, {'snippet': {'name': 'Romanian', 'hl': 'ro'}, 'id': 'ro'}, + {'snippet': {'name': 'Albanian', 'hl': 'sq'}, 'id': 'sq'}, {'snippet': {'name': 'Slovak', 'hl': 'sk'}, 'id': 'sk'}, {'snippet': {'name': 'Slovenian', 'hl': 'sl'}, 'id': 'sl'}, {'snippet': {'name': 'Finnish', 'hl': 'fi'}, 'id': 'fi'}, + {'snippet': {'name': 'Swedish', 'hl': 'sv'}, 'id': 'sv'}, {'snippet': {'name': 'Vietnamese', 'hl': 'vi'}, 'id': 'vi'}, {'snippet': {'name': 'Turkish', 'hl': 'tr'}, 'id': 'tr'}, {'snippet': {'name': 'Bulgarian', 'hl': 'bg'}, 'id': 'bg'}, + {'snippet': {'name': 'Kyrgyz', 'hl': 'ky'}, 'id': 'ky'}, {'snippet': {'name': 'Kazakh', 'hl': 'kk'}, 'id': 'kk'}, {'snippet': {'name': 'Macedonian', 'hl': 'mk'}, 'id': 'mk'}, {'snippet': {'name': 'Mongolian', 'hl': 'mn'}, 'id': 'mn'}, + {'snippet': {'name': 'Russian', 'hl': 'ru'}, 'id': 'ru'}, {'snippet': {'name': 'Serbian', 'hl': 'sr'}, 'id': 'sr'}, {'snippet': {'name': 'Ukrainian', 'hl': 'uk'}, 'id': 'uk'}, {'snippet': {'name': 'Greek', 'hl': 'el'}, 'id': 'el'}, + {'snippet': {'name': 'Armenian', 'hl': 'hy'}, 'id': 'hy'}, {'snippet': {'name': 'Hebrew', 'hl': 'iw'}, 'id': 'iw'}, {'snippet': {'name': 'Urdu', 'hl': 'ur'}, 'id': 'ur'}, {'snippet': {'name': 'Arabic', 'hl': 'ar'}, 'id': 'ar'}, + {'snippet': {'name': 'Persian', 'hl': 'fa'}, 'id': 'fa'}, {'snippet': {'name': 'Nepali', 'hl': 'ne'}, 'id': 'ne'}, {'snippet': {'name': 'Marathi', 'hl': 'mr'}, 'id': 'mr'}, {'snippet': {'name': 'Hindi', 'hl': 'hi'}, 'id': 'hi'}, + {'snippet': {'name': 'Bengali', 'hl': 'bn'}, 'id': 'bn'}, {'snippet': {'name': 'Punjabi', 'hl': 'pa'}, 'id': 'pa'}, {'snippet': {'name': 'Gujarati', 'hl': 'gu'}, 'id': 'gu'}, {'snippet': {'name': 'Tamil', 'hl': 'ta'}, 'id': 'ta'}, + {'snippet': {'name': 'Telugu', 'hl': 'te'}, 'id': 'te'}, {'snippet': {'name': 'Kannada', 'hl': 'kn'}, 'id': 'kn'}, {'snippet': {'name': 'Malayalam', 'hl': 'ml'}, 'id': 'ml'}, {'snippet': {'name': 'Sinhala', 'hl': 'si'}, 'id': 'si'}, + {'snippet': {'name': 'Thai', 'hl': 'th'}, 'id': 'th'}, {'snippet': {'name': 'Lao', 'hl': 'lo'}, 'id': 'lo'}, {'snippet': {'name': 'Myanmar (Burmese)', 'hl': 'my'}, 'id': 'my'}, {'snippet': {'name': 'Georgian', 'hl': 'ka'}, 'id': 'ka'}, + {'snippet': {'name': 'Amharic', 'hl': 'am'}, 'id': 'am'}, {'snippet': {'name': 'Khmer', 'hl': 'km'}, 'id': 'km'}, {'snippet': {'name': 'Chinese', 'hl': 'zh-CN'}, 'id': 'zh-CN'}, {'snippet': {'name': 'Chinese (Taiwan)', 'hl': 'zh-TW'}, 'id': 'zh-TW'}, + {'snippet': {'name': 'Chinese (Hong Kong)', 'hl': 'zh-HK'}, 'id': 'zh-HK'}, {'snippet': {'name': 'Japanese', 'hl': 'ja'}, 'id': 'ja'}, {'snippet': {'name': 'Korean', 'hl': 'ko'}, 'id': 'ko'}]} +DEFAULT_REGIONS = {'items': [{'snippet': {'gl': 'DZ', 'name': 'Algeria'}, 'id': 'DZ'}, {'snippet': {'gl': 'AR', 'name': 'Argentina'}, 'id': 'AR'}, {'snippet': {'gl': 'AU', 'name': 'Australia'}, 'id': 'AU'}, {'snippet': {'gl': 'AT', 'name': 'Austria'}, 'id': 'AT'}, + {'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}, 'id': 'AZ'}, {'snippet': {'gl': 'BH', 'name': 'Bahrain'}, 'id': 'BH'}, {'snippet': {'gl': 'BY', 'name': 'Belarus'}, 'id': 'BY'}, {'snippet': {'gl': 'BE', 'name': 'Belgium'}, 'id': 'BE'}, + {'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}, 'id': 'BA'}, {'snippet': {'gl': 'BR', 'name': 'Brazil'}, 'id': 'BR'}, {'snippet': {'gl': 'BG', 'name': 'Bulgaria'}, 'id': 'BG'}, {'snippet': {'gl': 'CA', 'name': 'Canada'}, 'id': 'CA'}, + {'snippet': {'gl': 'CL', 'name': 'Chile'}, 'id': 'CL'}, {'snippet': {'gl': 'CO', 'name': 'Colombia'}, 'id': 'CO'}, {'snippet': {'gl': 'HR', 'name': 'Croatia'}, 'id': 'HR'}, {'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}, 'id': 'CZ'}, + {'snippet': {'gl': 'DK', 'name': 'Denmark'}, 'id': 'DK'}, {'snippet': {'gl': 'EG', 'name': 'Egypt'}, 'id': 'EG'}, {'snippet': {'gl': 'EE', 'name': 'Estonia'}, 'id': 'EE'}, {'snippet': {'gl': 'FI', 'name': 'Finland'}, 'id': 'FI'}, + {'snippet': {'gl': 'FR', 'name': 'France'}, 'id': 'FR'}, {'snippet': {'gl': 'GE', 'name': 'Georgia'}, 'id': 'GE'}, {'snippet': {'gl': 'DE', 'name': 'Germany'}, 'id': 'DE'}, {'snippet': {'gl': 'GH', 'name': 'Ghana'}, 'id': 'GH'}, + {'snippet': {'gl': 'GR', 'name': 'Greece'}, 'id': 'GR'}, {'snippet': {'gl': 'HK', 'name': 'Hong Kong'}, 'id': 'HK'}, {'snippet': {'gl': 'HU', 'name': 'Hungary'}, 'id': 'HU'}, {'snippet': {'gl': 'IS', 'name': 'Iceland'}, 'id': 'IS'}, + {'snippet': {'gl': 'IN', 'name': 'India'}, 'id': 'IN'}, {'snippet': {'gl': 'ID', 'name': 'Indonesia'}, 'id': 'ID'}, {'snippet': {'gl': 'IQ', 'name': 'Iraq'}, 'id': 'IQ'}, {'snippet': {'gl': 'IE', 'name': 'Ireland'}, 'id': 'IE'}, + {'snippet': {'gl': 'IL', 'name': 'Israel'}, 'id': 'IL'}, {'snippet': {'gl': 'IT', 'name': 'Italy'}, 'id': 'IT'}, {'snippet': {'gl': 'JM', 'name': 'Jamaica'}, 'id': 'JM'}, {'snippet': {'gl': 'JP', 'name': 'Japan'}, 'id': 'JP'}, + {'snippet': {'gl': 'JO', 'name': 'Jordan'}, 'id': 'JO'}, {'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}, 'id': 'KZ'}, {'snippet': {'gl': 'KE', 'name': 'Kenya'}, 'id': 'KE'}, {'snippet': {'gl': 'KW', 'name': 'Kuwait'}, 'id': 'KW'}, + {'snippet': {'gl': 'LV', 'name': 'Latvia'}, 'id': 'LV'}, {'snippet': {'gl': 'LB', 'name': 'Lebanon'}, 'id': 'LB'}, {'snippet': {'gl': 'LY', 'name': 'Libya'}, 'id': 'LY'}, {'snippet': {'gl': 'LT', 'name': 'Lithuania'}, 'id': 'LT'}, + {'snippet': {'gl': 'LU', 'name': 'Luxembourg'}, 'id': 'LU'}, {'snippet': {'gl': 'MK', 'name': 'Macedonia'}, 'id': 'MK'}, {'snippet': {'gl': 'MY', 'name': 'Malaysia'}, 'id': 'MY'}, {'snippet': {'gl': 'MX', 'name': 'Mexico'}, 'id': 'MX'}, + {'snippet': {'gl': 'ME', 'name': 'Montenegro'}, 'id': 'ME'}, {'snippet': {'gl': 'MA', 'name': 'Morocco'}, 'id': 'MA'}, {'snippet': {'gl': 'NP', 'name': 'Nepal'}, 'id': 'NP'}, {'snippet': {'gl': 'NL', 'name': 'Netherlands'}, 'id': 'NL'}, + {'snippet': {'gl': 'NZ', 'name': 'New Zealand'}, 'id': 'NZ'}, {'snippet': {'gl': 'NG', 'name': 'Nigeria'}, 'id': 'NG'}, {'snippet': {'gl': 'NO', 'name': 'Norway'}, 'id': 'NO'}, {'snippet': {'gl': 'OM', 'name': 'Oman'}, 'id': 'OM'}, + {'snippet': {'gl': 'PK', 'name': 'Pakistan'}, 'id': 'PK'}, {'snippet': {'gl': 'PE', 'name': 'Peru'}, 'id': 'PE'}, {'snippet': {'gl': 'PH', 'name': 'Philippines'}, 'id': 'PH'}, {'snippet': {'gl': 'PL', 'name': 'Poland'}, 'id': 'PL'}, + {'snippet': {'gl': 'PT', 'name': 'Portugal'}, 'id': 'PT'}, {'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}, 'id': 'PR'}, {'snippet': {'gl': 'QA', 'name': 'Qatar'}, 'id': 'QA'}, {'snippet': {'gl': 'RO', 'name': 'Romania'}, 'id': 'RO'}, + {'snippet': {'gl': 'RU', 'name': 'Russia'}, 'id': 'RU'}, {'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}, 'id': 'SA'}, {'snippet': {'gl': 'SN', 'name': 'Senegal'}, 'id': 'SN'}, {'snippet': {'gl': 'RS', 'name': 'Serbia'}, 'id': 'RS'}, + {'snippet': {'gl': 'SG', 'name': 'Singapore'}, 'id': 'SG'}, {'snippet': {'gl': 'SK', 'name': 'Slovakia'}, 'id': 'SK'}, {'snippet': {'gl': 'SI', 'name': 'Slovenia'}, 'id': 'SI'}, {'snippet': {'gl': 'ZA', 'name': 'South Africa'}, 'id': 'ZA'}, + {'snippet': {'gl': 'KR', 'name': 'South Korea'}, 'id': 'KR'}, {'snippet': {'gl': 'ES', 'name': 'Spain'}, 'id': 'ES'}, {'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}, 'id': 'LK'}, {'snippet': {'gl': 'SE', 'name': 'Sweden'}, 'id': 'SE'}, + {'snippet': {'gl': 'CH', 'name': 'Switzerland'}, 'id': 'CH'}, {'snippet': {'gl': 'TW', 'name': 'Taiwan'}, 'id': 'TW'}, {'snippet': {'gl': 'TZ', 'name': 'Tanzania'}, 'id': 'TZ'}, {'snippet': {'gl': 'TH', 'name': 'Thailand'}, 'id': 'TH'}, + {'snippet': {'gl': 'TN', 'name': 'Tunisia'}, 'id': 'TN'}, {'snippet': {'gl': 'TR', 'name': 'Turkey'}, 'id': 'TR'}, {'snippet': {'gl': 'UG', 'name': 'Uganda'}, 'id': 'UG'}, {'snippet': {'gl': 'UA', 'name': 'Ukraine'}, 'id': 'UA'}, + {'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}, 'id': 'AE'}, {'snippet': {'gl': 'GB', 'name': 'United Kingdom'}, 'id': 'GB'}, {'snippet': {'gl': 'US', 'name': 'United States'}, 'id': 'US'}, {'snippet': {'gl': 'VN', 'name': 'Vietnam'}, 'id': 'VN'}, + {'snippet': {'gl': 'YE', 'name': 'Yemen'}, 'id': 'YE'}, {'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}, 'id': 'ZW'}]} def _process_language(provider, context): @@ -70,7 +70,7 @@ def _process_language(provider, context): else: items = json_data['items'] language_list = [] - invalid_ids = [u'es-419'] # causes hl not a valid language error. Issue #418 + invalid_ids = ['es-419'] # causes hl not a valid language error. Issue #418 for item in items: if item['id'] in invalid_ids: continue diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 89ad2b119..c7b5c3fe8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -334,31 +334,30 @@ def process(category, provider, context): if category == 'related_videos': return _process_related_videos(provider, context) - elif category == 'popular_right_now': + if category == 'popular_right_now': return _process_popular_right_now(provider, context) - elif category == 'recommendations': + if category == 'recommendations': return _process_recommendations(provider, context) - elif category == 'browse_channels': + if category == 'browse_channels': return _process_browse_channels(provider, context) - elif category == 'new_uploaded_videos_tv': + if category == 'new_uploaded_videos_tv': return _process_new_uploaded_videos_tv(provider, context) - elif category == 'new_uploaded_videos_tv_filtered': + if category == 'new_uploaded_videos_tv_filtered': return _process_new_uploaded_videos_tv_filtered(provider, context) - elif category == 'disliked_videos': + if category == 'disliked_videos': return _process_disliked_videos(provider, context) - elif category == 'live': + if category == 'live': return _process_live_events(provider, context) - elif category == 'upcoming_live': + if category == 'upcoming_live': return _process_live_events(provider, context, event_type='upcoming') - elif category == 'completed_live': + if category == 'completed_live': return _process_live_events(provider, context, event_type='completed') - elif category == 'description_links': + if category == 'description_links': return _process_description_links(provider, context) - elif category == 'parent_comments': + if category == 'parent_comments': return _process_parent_comments(provider, context) - elif category == 'child_comments': + if category == 'child_comments': return _process_child_comments(provider, context) - elif category == 'saved_playlists': + if category == 'saved_playlists': return _process_saved_playlists_tv(provider, context) - else: - raise kodion.KodionException("YouTube special category '%s' not found" % category) + raise kodion.KodionException("YouTube special category '%s' not found" % category) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index 9dd98b0a5..712308492 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -30,9 +30,9 @@ def _process_add(provider, context): listitem_subscription_id = context.get_ui().get_info_label('Container.ListItem(0).Property(subscription_id)') subscription_id = context.get_param('subscription_id', '') - if not subscription_id: - if listitem_subscription_id and listitem_subscription_id.lower().startswith('uc'): - subscription_id = listitem_subscription_id + if (not subscription_id and listitem_subscription_id + and listitem_subscription_id.lower().startswith('uc')): + subscription_id = listitem_subscription_id if subscription_id: json_data = provider.get_client(context).subscribe(subscription_id) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 7d900ea4d..16cece658 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -52,11 +52,10 @@ def _process_rate_video(provider, context, re_match): if rating != current_rating: rating_items.append((context.localize(provider.LOCAL_MAP['youtube.video.rate.%s' % rating]), rating)) result = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.video.rate']), rating_items) + elif rating_param != current_rating: + result = rating_param else: - if rating_param != current_rating: - result = rating_param - else: - result = -1 + result = -1 if result != -1: notify_message = '' @@ -85,6 +84,8 @@ def _process_rate_video(provider, context, re_match): audible=False ) + return True + def _process_more_for_video(provider, context): video_id = context.get_param('video_id', '') @@ -124,7 +125,6 @@ def _process_more_for_video(provider, context): def process(method, provider, context, re_match): if method == 'rate': return _process_rate_video(provider, context, re_match) - elif method == 'more': + if method == 'more': return _process_more_for_video(provider, context) - else: - raise kodion.KodionException("Unknown method '%s'" % method) + raise kodion.KodionException("Unknown method '%s'" % method) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index ab284c4e3..1982a4777 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -30,6 +30,7 @@ import xbmcgui import xbmcplugin + try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: @@ -220,29 +221,29 @@ def get_dev_config(context, addon_id, dev_configs): if dev_config and not context.get_settings().allow_dev_keys(): context.log_debug('Developer config ignored') return None - elif dev_config: + + if dev_config: if not dev_config.get('main') or not dev_config['main'].get('key') \ or not dev_config['main'].get('system') or not dev_config.get('origin') \ or not dev_config['main'].get('id') or not dev_config['main'].get('secret'): context.log_error('Error loading developer config: |invalid structure| ' 'expected: |{"origin": ADDON_ID, "main": {"system": SYSTEM_NAME, "key": API_KEY, "id": CLIENT_ID, "secret": CLIENT_SECRET}}|') return dict() + dev_origin = dev_config['origin'] + dev_main = dev_config['main'] + dev_system = dev_main['system'] + if dev_system == 'JSONStore': + dev_key = b64decode(dev_main['key']) + dev_id = b64decode(dev_main['id']) + dev_secret = b64decode(dev_main['secret']) else: - dev_origin = dev_config['origin'] - dev_main = dev_config['main'] - dev_system = dev_main['system'] - if dev_system == 'JSONStore': - dev_key = b64decode(dev_main['key']) - dev_id = b64decode(dev_main['id']) - dev_secret = b64decode(dev_main['secret']) - else: - dev_key = dev_main['key'] - dev_id = dev_main['id'] - dev_secret = dev_main['secret'] - context.log_debug('Using developer config: origin: |{0}| system |{1}|'.format(dev_origin, dev_system)) - return {'origin': dev_origin, 'main': {'id': dev_id, 'secret': dev_secret, 'key': dev_key, 'system': dev_system}} - else: - return dict() + dev_key = dev_main['key'] + dev_id = dev_main['id'] + dev_secret = dev_main['secret'] + context.log_debug('Using developer config: origin: |{0}| system |{1}|'.format(dev_origin, dev_system)) + return {'origin': dev_origin, 'main': {'id': dev_id, 'secret': dev_secret, 'key': dev_key, 'system': dev_system}} + + return dict() def reset_client(self): self._client = None @@ -279,11 +280,10 @@ def get_client(self, context): context.log_debug('API key origin changed, clearing cache. |%s|' % dev_origin) access_manager.set_last_origin(dev_origin) self.get_resource_manager(context).clear() - else: - if api_last_origin != 'plugin.video.youtube': - context.log_debug('API key origin changed, clearing cache. |plugin.video.youtube|') - access_manager.set_last_origin('plugin.video.youtube') - self.get_resource_manager(context).clear() + elif api_last_origin != 'plugin.video.youtube': + context.log_debug('API key origin changed, clearing cache. |plugin.video.youtube|') + access_manager.set_last_origin('plugin.video.youtube') + self.get_resource_manager(context).clear() if dev_id: access_tokens = access_manager.get_dev_access_token(dev_id).split('|') @@ -375,20 +375,17 @@ def get_client(self, context): access_manager.update_dev_access_token(dev_id, access_token='', refresh_token='') else: access_manager.update_access_token(access_token='', refresh_token='') + elif dev_id: + access_manager.update_dev_access_token(dev_id, '') else: - if dev_id: - access_manager.update_dev_access_token(dev_id, '') - else: - access_manager.update_access_token('') + access_manager.update_access_token('') # we clear the cache, so none cached data of an old account will be displayed. self.get_resource_manager(context).clear() # in debug log the login status self._is_logged_in = len(access_tokens) == 2 - if self._is_logged_in: - context.log_debug('User is logged in') - else: - context.log_debug('User is not logged in') + context.log_debug('User is logged in' if self._is_logged_in else + 'User is not logged in') if len(access_tokens) == 0: access_tokens = ['', ''] @@ -553,9 +550,9 @@ def _on_channel(self, context, re_match): method = re_match.group('method') channel_id = re_match.group('channel_id') - if method == 'channel' and channel_id and channel_id.lower() == 'property': - if listitem_channel_id and listitem_channel_id.lower().startswith(('mine', 'uc')): - context.execute('Container.Update(%s)' % context.create_uri(['channel', listitem_channel_id])) # redirect if keymap, without redirect results in 'invalid handle -1' + if (method == 'channel' and channel_id and channel_id.lower() == 'property' + and listitem_channel_id and listitem_channel_id.lower().startswith(('mine', 'uc'))): + context.execute('Container.Update(%s)' % context.create_uri(['channel', listitem_channel_id])) # redirect if keymap, without redirect results in 'invalid handle -1' if method == 'channel' and not channel_id: return False @@ -757,22 +754,21 @@ def on_play(self, context, re_match): if not redirect: context.log_debug('Redirecting playback, handle is -1') context.execute(builtin % context.create_uri(['play'], {'video_id': params['video_id']})) - return + return False if 'playlist_id' in params and (context.get_handle() != -1): builtin = 'RunPlugin(%s)' stream_url = context.create_uri(['play'], params) xbmcplugin.setResolvedUrl(handle=context.get_handle(), succeeded=False, listitem=xbmcgui.ListItem(path=stream_url)) context.execute(builtin % context.create_uri(['play'], params)) - return + return False if 'video_id' in params and 'playlist_id' not in params: return yt_play.play_video(self, context) - elif 'playlist_id' in params: + if 'playlist_id' in params: return yt_play.play_playlist(self, context) - elif 'channel_id' in params and 'live' in params: - if int(params['live']) > 0: - return yt_play.play_channel_live(self, context) + if 'channel_id' in params and 'live' in params and int(params['live']) > 0: + return yt_play.play_channel_live(self, context) return False @kodion.RegisterProviderPath('^/video/(?P[^/]+)/$') @@ -869,7 +865,7 @@ def switch_to_user(_user): result = ui.on_select(context.localize(self.LOCAL_MAP['youtube.switch.user']), users) if result == -1: return True - elif result == 0: + if result == 0: user = add_user(access_manager_users) else: user = user_index_map[result - 1] @@ -975,11 +971,13 @@ def _on_sign(self, context, re_match): if (mode == 'in') and context.get_access_manager().has_refresh_token(): yt_login.process('out', self, context, sign_out_refresh=False) - if not sign_out_confirmed: - if (mode == 'out') and context.get_ui().on_yes_no_input(context.localize(self.LOCAL_MAP['youtube.sign.out']), context.localize(self.LOCAL_MAP['youtube.are.you.sure'])): - sign_out_confirmed = True + if (not sign_out_confirmed and mode == 'out' + and context.get_ui().on_yes_no_input( + context.localize(self.LOCAL_MAP['youtube.sign.out']), + context.localize(self.LOCAL_MAP['youtube.are.you.sure']))): + sign_out_confirmed = True - if (mode == 'in') or ((mode == 'out') and sign_out_confirmed): + if mode == 'in' or (mode == 'out' and sign_out_confirmed): yt_login.process(mode, self, context) return False @@ -1111,12 +1109,9 @@ def configure_addon(self, context, re_match): local_ranges = ('10.', '172.16.', '192.168.') addresses = [iface[4][0] for iface in socket.getaddrinfo(socket.gethostname(), None) if iface[4][0].startswith(local_ranges)] + ['127.0.0.1', '0.0.0.0'] selected_address = context.get_ui().on_select(context.localize(self.LOCAL_MAP['youtube.select.listen.ip']), addresses) - if selected_address == -1: - return False - else: + if selected_address != -1: context.get_settings().set_httpd_listen(addresses[selected_address]) - else: - return False + return False # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/my_subscriptions/filter/$') @@ -1142,9 +1137,8 @@ def manage_my_subscription_filter(self, context, re_match): if action == 'add': if channel_name not in filter_list: filter_list.append(channel_name) - elif action == 'remove': - if channel_name in filter_list: - filter_list = [chan_name for chan_name in filter_list if chan_name != channel_name] + elif action == 'remove' and channel_name in filter_list: + filter_list = [chan_name for chan_name in filter_list if chan_name != channel_name] modified_string = ','.join(filter_list).lstrip(',') if filter_string != modified_string: @@ -1180,26 +1174,25 @@ def maintenance_actions(self, context, re_match): context.get_playback_history().clear() context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) elif action == 'reset': - if maint_type == 'access_manager': - if context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.reset.access.manager.confirm'])): - try: - context.get_function_cache().clear() - access_manager = context.get_access_manager() - client = self.get_client(context) - if access_manager.has_refresh_token(): - refresh_tokens = access_manager.get_refresh_token().split('|') - refresh_tokens = list(set(refresh_tokens)) - for refresh_token in refresh_tokens: - try: - client.revoke(refresh_token) - except: - pass - self.reset_client() - access_manager.update_access_token(access_token='', refresh_token='') - context.get_ui().refresh_container() - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) - except: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.failed'])) + if maint_type == 'access_manager' and context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.reset.access.manager.confirm'])): + try: + context.get_function_cache().clear() + access_manager = context.get_access_manager() + client = self.get_client(context) + if access_manager.has_refresh_token(): + refresh_tokens = access_manager.get_refresh_token().split('|') + refresh_tokens = list(set(refresh_tokens)) + for refresh_token in refresh_tokens: + try: + client.revoke(refresh_token) + except: + pass + self.reset_client() + access_manager.update_access_token(access_token='', refresh_token='') + context.get_ui().refresh_container() + context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) + except: + context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.failed'])) elif action == 'delete': _maint_files = {'function_cache': 'cache.sqlite', 'search_cache': 'search.sqlite', @@ -1240,16 +1233,15 @@ def maintenance_actions(self, context, re_match): context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) else: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.failed'])) - elif action == 'install': - if maint_type == 'inputstreamhelper': - if context.get_system_version().get_version()[0] >= 17: - try: - xbmcaddon.Addon('script.module.inputstreamhelper') - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.inputstreamhelper.is.installed'])) - except RuntimeError: - context.execute('InstallAddon(script.module.inputstreamhelper)') - else: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.requires.krypton'])) + elif action == 'install' and maint_type == 'inputstreamhelper': + if context.get_system_version().get_version()[0] >= 17: + try: + xbmcaddon.Addon('script.module.inputstreamhelper') + context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.inputstreamhelper.is.installed'])) + except RuntimeError: + context.execute('InstallAddon(script.module.inputstreamhelper)') + else: + context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.requires.krypton'])) # noinspection PyUnusedLocal @kodion.RegisterProviderPath('^/api/update/$') diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index 5f8c3831a..0a4217592 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -107,17 +107,22 @@ def get_items(_page_token=''): for item in json_data.get('items', []): items.append(item) + error = False next_page_token = json_data.get('nextPageToken') - if all_pages and (next_page_token is not None): - get_items(_page_token=next_page_token) - elif next_page_token is not None: + if not next_page_token: + return error + if all_pages: + error = get_items(_page_token=next_page_token) + else: items.append({'nextPageToken': next_page_token}) + return error - get_items(_page_token=page_token) + error = get_items(_page_token=page_token) + if error: + return error items = _append_missing_page_token(items) - return items @@ -148,17 +153,22 @@ def get_items(_page_token=''): for item in json_data.get('items', []): items.append(item) + error = False next_page_token = json_data.get('nextPageToken') - if all_pages and (next_page_token is not None): - get_items(_page_token=next_page_token) - elif next_page_token is not None: + if not next_page_token: + return error + if all_pages: + error = get_items(_page_token=next_page_token) + else: items.append({'nextPageToken': next_page_token}) + return error - get_items(_page_token=page_token) + error = get_items(_page_token=page_token) + if error: + return error items = _append_missing_page_token(items) - return items @@ -249,17 +259,22 @@ def get_items(_page_token=''): for item in json_data.get('items', []): items.append(item) + error = False next_page_token = json_data.get('nextPageToken') - if all_pages and (next_page_token is not None): - get_items(_page_token=next_page_token) - elif next_page_token is not None: + if not next_page_token: + return error + if all_pages: + error = get_items(_page_token=next_page_token) + else: items.append({'nextPageToken': next_page_token}) + return error - get_items(_page_token=page_token) + error = get_items(_page_token=page_token) + if error: + return error items = _append_missing_page_token(items) - return items @@ -310,15 +325,18 @@ def get_items(_page_token=''): for item in json_data.get('items', []): if 'snippet' in item: items.append(item) + error = False next_page_token = json_data.get('nextPageToken') - if next_page_token is not None: + if next_page_token: items.append({'nextPageToken': next_page_token}) + return error - get_items(_page_token=page_token) + error = get_items(_page_token=page_token) + if error: + return error items = _append_missing_page_token(items) - return items @@ -360,15 +378,18 @@ def get_items(_page_token=''): for item in json_data.get('items', []): items.append(item) + error = False next_page_token = json_data.get('nextPageToken') - if next_page_token is not None: + if next_page_token: items.append({'nextPageToken': next_page_token}) + return error - get_items(_page_token=page_token) + error = get_items(_page_token=page_token) + if error: + return error items = _append_missing_page_token(items) - return items From 1bb36f0256bd79f16302f8c1331619947e0c63cb Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:13:45 +1100 Subject: [PATCH 012/141] Tidy up imports - Group multiple imports - Remove star imports - Limit line length - Use from import where appropriate --- .../lib/youtube_plugin/kodion/__init__.py | 2 -- .../kodion/abstract_provider.py | 34 +++++++++++-------- .../kodion/impl/abstract_context.py | 14 +++++++- .../kodion/impl/xbmc/info_labels.py | 2 +- .../kodion/impl/xbmc/xbmc_context.py | 5 +-- .../kodion/impl/xbmc/xbmc_runner.py | 2 +- .../youtube_plugin/kodion/utils/__init__.py | 13 ++++++- .../youtube/client/login_client.py | 5 ++- .../youtube/helper/subtitles.py | 2 +- .../youtube/helper/video_info.py | 11 ++++-- .../lib/youtube_plugin/youtube/provider.py | 26 +++++++++++--- .../youtube/youtube_exceptions.py | 8 ++--- 12 files changed, 88 insertions(+), 36 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/__init__.py b/resources/lib/youtube_plugin/kodion/__init__.py index 0591b2ea3..be1e324a8 100644 --- a/resources/lib/youtube_plugin/kodion/__init__.py +++ b/resources/lib/youtube_plugin/kodion/__init__.py @@ -20,8 +20,6 @@ # import specialized implementation into the kodion namespace from .impl import Context -from .constants import * - from . import logger __all__ = ['KodionException', 'RegisterProviderPath', 'AbstractProvider', 'Context', 'utils', 'json_store', 'logger'] diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 925d91ae3..edd9efc40 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -14,9 +14,15 @@ from urllib.parse import unquote from .exceptions import KodionException -from . import items +from .items import ( + from_json, + to_jsons, + DirectoryItem, + NewSearchItem, + SearchHistoryItem +) +from .utils import to_unicode, to_utf8 from . import constants -from . import utils class AbstractProvider(object): @@ -144,11 +150,11 @@ def _internal_favorite(context, re_match): command = re_match.group('command') if command == 'add': - fav_item = items.from_json(params['item']) + fav_item = from_json(params['item']) context.get_favorite_list().add(fav_item) return None if command == 'remove': - fav_item = items.from_json(params['item']) + fav_item = from_json(params['item']) context.get_favorite_list().remove(fav_item) context.get_ui().refresh_container() return None @@ -158,7 +164,7 @@ def _internal_favorite(context, re_match): for directory_item in directory_items: context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), 'RunPlugin(%s)' % context.create_uri([constants.paths.FAVORITES, 'remove'], - params={'item': items.to_jsons(directory_item)}))] + params={'item': to_jsons(directory_item)}))] directory_item.set_context_menu(context_menu) return directory_items @@ -171,11 +177,11 @@ def _internal_watch_later(self, context, re_match): command = re_match.group('command') if command == 'add': - item = items.from_json(params['item']) + item = from_json(params['item']) context.get_watch_later_list().add(item) return None if command == 'remove': - item = items.from_json(params['item']) + item = from_json(params['item']) context.get_watch_later_list().remove(item) context.get_ui().refresh_container() return None @@ -185,7 +191,7 @@ def _internal_watch_later(self, context, re_match): for video_item in video_items: context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), 'RunPlugin(%s)' % context.create_uri([constants.paths.WATCH_LATER, 'remove'], - params={'item': items.to_jsons(video_item)}))] + params={'item': to_jsons(video_item)}))] video_item.set_context_menu(context_menu) return video_items @@ -233,7 +239,7 @@ def _internal_search(self, context, re_match): # came from page 1 of search query by '..'/back, user doesn't want to input on this path if cached_query and cached_query.get('search_query', {}).get('query'): query = cached_query.get('search_query', {}).get('query') - query = utils.to_unicode(query) + query = to_unicode(query) query = unquote(query) else: result, input_query = context.get_ui().on_keyboard_input(context.localize(constants.localize.SEARCH_TITLE)) @@ -246,7 +252,7 @@ def _internal_search(self, context, re_match): incognito = str(context.get_param('incognito', False)).lower() == 'true' channel_id = context.get_param('channel_id', '') - query = utils.to_utf8(query) + query = to_utf8(query) try: self._data_cache.set('search_query', json.dumps({'query': quote(query)})) except KeyError: @@ -267,7 +273,7 @@ def _internal_search(self, context, re_match): incognito = str(context.get_param('incognito', False)).lower() == 'true' channel_id = context.get_param('channel_id', '') query = params['q'] - query = utils.to_unicode(query) + query = to_unicode(query) if not incognito and not channel_id: try: @@ -284,16 +290,16 @@ def _internal_search(self, context, re_match): location = str(context.get_param('location', False)).lower() == 'true' # 'New Search...' - new_search_item = items.NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) + new_search_item = NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) result.append(new_search_item) for search in search_history.list(): # little fallback for old history entries - if isinstance(search, items.DirectoryItem): + if isinstance(search, DirectoryItem): search = search.get_name() # we create a new instance of the SearchItem - search_history_item = items.SearchHistoryItem(context, search, fanart=self.get_alternative_fanart(context), location=location) + search_history_item = SearchHistoryItem(context, search, fanart=self.get_alternative_fanart(context), location=location) result.append(search_history_item) if search_history.is_empty(): diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py index 078314c5c..8866a3f25 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py @@ -13,7 +13,19 @@ from .. import constants from .. import logger -from ..utils import * +from ..utils import ( + create_path, + create_uri_path, + to_utf8, + AccessManager, + DataCache, + FavoriteList, + FunctionCache, + PlaybackHistory, + SearchHistory, + SystemVersion, + WatchLaterList, +) class AbstractContext(object): diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py index 2018d3bb3..e02728a18 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py @@ -9,7 +9,7 @@ """ from ... import utils -from ...items import * +from ...items import AudioItem, DirectoryItem, ImageItem, VideoItem def _process_date(info_labels, param): diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index f36f60e86..98ab90163 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -13,10 +13,7 @@ import os import sys import weakref -from urllib.parse import quote -from urllib.parse import unquote -from urllib.parse import urlparse -from urllib.parse import parse_qsl +from urllib.parse import parse_qsl, quote, unquote, urlparse import xbmc import xbmcaddon diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py index 46fda8a72..5d881a20b 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py @@ -13,7 +13,7 @@ from ..abstract_provider_runner import AbstractProviderRunner from ...exceptions import KodionException -from ...items import * +from ...items import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem from ... import AbstractProvider from . import info_labels from . import xbmc_items diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 717fb4dc2..5365ec8f6 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -9,7 +9,18 @@ """ from . import datetime_parser -from .methods import * +from .methods import ( + create_path, + create_uri_path, + find_best_fit, + find_video_id, + loose_version, + make_dirs, + select_stream, + strip_html_from_text, + to_unicode, + to_utf8, +) from .search_history import SearchHistory from .favorite_list import FavoriteList from .watch_later_list import WatchLaterList diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 4135ac025..e9d595302 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -15,7 +15,10 @@ from ...youtube.youtube_exceptions import InvalidGrant, LoginException from ...kodion import Context -from .__config__ import api, youtube_tv, developer_keys, keys_changed +from .__config__ import (api, + developer_keys, + keys_changed, + youtube_tv,) context = Context(plugin_id='plugin.video.youtube') diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index ef2f4ab2b..f61c02808 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -7,7 +7,7 @@ """ from html import unescape -from urllib.parse import (parse_qs, urlsplit, urlunsplit, urlencode, urljoin) +from urllib.parse import parse_qs, urlsplit, urlunsplit, urlencode, urljoin import xbmcvfs import requests diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 4f7621e9a..42d7b7826 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -14,8 +14,15 @@ from json import dumps as json_dumps, loads as json_loads from html import unescape -from urllib.parse import (parse_qs, urlsplit, urlunsplit, urlencode, urljoin, - quote, unquote) +from urllib.parse import ( + parse_qs, + quote, + unquote, + urlsplit, + urlunsplit, + urlencode, + urljoin, +) import requests import xbmcvfs diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 1982a4777..280062ad3 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -17,11 +17,29 @@ from ..youtube.helper import yt_subscriptions from .. import kodion -from ..kodion.utils import FunctionCache, strip_html_from_text, get_client_ip_address, is_httpd_live, find_video_id -from ..kodion.items import * +from ..kodion.utils import ( + find_video_id, + get_client_ip_address, + is_httpd_live, + strip_html_from_text, + FunctionCache, +) +from ..kodion.items import DirectoryItem from ..youtube.client import YouTube -from .helper import v3, ResourceManager, yt_specials, yt_playlist, yt_login, yt_setup_wizard, yt_video, \ - yt_context_menu, yt_play, yt_old_actions, UrlResolver, UrlToItemConverter +from .helper import ( + v3, + yt_context_menu, + yt_login, + yt_old_actions, + yt_play, + yt_playlist, + yt_setup_wizard, + yt_specials, + yt_video, + ResourceManager, + UrlResolver, + UrlToItemConverter, +) from .youtube_exceptions import InvalidGrant, LoginException import xbmc diff --git a/resources/lib/youtube_plugin/youtube/youtube_exceptions.py b/resources/lib/youtube_plugin/youtube/youtube_exceptions.py index 1ff852d11..1e7cda824 100644 --- a/resources/lib/youtube_plugin/youtube/youtube_exceptions.py +++ b/resources/lib/youtube_plugin/youtube/youtube_exceptions.py @@ -8,16 +8,16 @@ See LICENSES/GPL-2.0-only for more information. """ -from .. import kodion +from ..kodion import KodionException -class LoginException(kodion.KodionException): +class LoginException(KodionException): pass -class YouTubeException(kodion.KodionException): +class YouTubeException(KodionException): pass -class InvalidGrant(kodion.KodionException): +class InvalidGrant(KodionException): pass From 0466d93b233933f3adde2dbb5d060c263931a405 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:09:41 +1100 Subject: [PATCH 013/141] Misc optimisations and refactoring Needs testing for regressions --- .../kodion/impl/abstract_context.py | 2 +- .../kodion/impl/abstract_settings.py | 2 +- .../kodion/impl/xbmc/info_labels.py | 2 +- .../kodion/impl/xbmc/xbmc_context.py | 42 ++-- .../kodion/impl/xbmc/xbmc_runner.py | 2 +- .../kodion/items/search_item.py | 4 +- .../lib/youtube_plugin/kodion/items/utils.py | 44 ++-- .../youtube_plugin/kodion/items/video_item.py | 15 +- .../kodion/json_store/login_tokens.py | 27 +-- .../kodion/utils/access_manager.py | 15 +- .../youtube_plugin/kodion/utils/data_cache.py | 7 +- .../kodion/utils/http_server.py | 8 +- .../youtube_plugin/kodion/utils/methods.py | 38 ++-- .../youtube/client/__config__.py | 5 +- .../youtube_plugin/youtube/client/youtube.py | 4 +- .../youtube/helper/resource_manager.py | 190 ++++++++--------- .../youtube/helper/url_to_item_converter.py | 37 ++-- .../youtube_plugin/youtube/helper/utils.py | 197 ++++++++++-------- .../lib/youtube_plugin/youtube/helper/v3.py | 68 +++--- .../youtube/helper/video_info.py | 22 +- .../youtube_plugin/youtube/helper/yt_play.py | 2 +- .../youtube/helper/yt_specials.py | 6 +- .../lib/youtube_plugin/youtube/provider.py | 30 ++- resources/lib/youtube_requests.py | 27 +-- 24 files changed, 377 insertions(+), 419 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py index 8866a3f25..321087b66 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_context.py @@ -159,7 +159,7 @@ def create_uri(self, path='/', params=None): else: uri = "%s://%s/" % ('plugin', str(self._plugin_id)) - if len(params) > 0: + if params: # make a copy of the map uri_params = {} uri_params.update(params) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index 14868fab6..c231cea4d 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -57,7 +57,7 @@ def get_bool(self, setting_id, default_value): if value is None or value == '': return default_value - if value != 'false' and value != 'true': + if value not in {'false', 'true'}: return default_value return value == 'true' diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py index e02728a18..86f8f9427 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py @@ -169,7 +169,7 @@ def create_from_item(base_item): _process_list_value(info_labels, 'cast', base_item.get_cast()) # Audio and Video - if isinstance(base_item, AudioItem) or isinstance(base_item, VideoItem): + if isinstance(base_item, (AudioItem, VideoItem)): # 'title' = 'Blow Your Head Off' (string) _process_string_value(info_labels, 'title', base_item.get_title()) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 98ab90163..867296b51 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -56,14 +56,9 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override # after that try to get the params if len(sys.argv) > 2: params = sys.argv[2][1:] - if len(params) > 0: + if params: self._uri = '?'.join([self._uri, params]) - - self._params = {} - params = dict(parse_qsl(params)) - for _param in params: - item = params[_param] - self._params[_param] = item + self._params = dict(parse_qsl(params)) self._ui = None self._video_playlist = None @@ -184,29 +179,22 @@ def get_settings(self): return self._settings def localize(self, text_id, default_text=''): - result = None - if isinstance(text_id, int): - """ - We want to use all localization strings! - Addons should only use the range 30000 thru 30999 (see: http://kodi.wiki/view/Language_support) but we - do it anyway. I want some of the localized strings for the views of a skin. - """ - if text_id >= 0 and (text_id < 30000 or text_id > 30999): - result = xbmc.getLocalizedString(text_id) - if result is not None and result: - result = utils.to_unicode(result) - - if not result: + if not isinstance(text_id, int): try: - result = self._addon.getLocalizedString(int(text_id)) - if result is not None and result: - result = utils.to_unicode(result) + text_id = int(text_id) except ValueError: - pass - - if not result: - result = default_text + return default_text + if text_id <= 0: + return default_text + """ + We want to use all localization strings! + Addons should only use the range 30000 thru 30999 (see: http://kodi.wiki/view/Language_support) but we + do it anyway. I want some of the localized strings for the views of a skin. + """ + source = self._addon if 30000 <= text_id < 31000 else xbmc + result = source.getLocalizedString(text_id) + result = utils.to_unicode(result) if result else default_text return result def set_content_type(self, content_type): diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py index 5d881a20b..454ab1d11 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py @@ -46,7 +46,7 @@ def run(self, provider, context=None): if isinstance(result, bool) and not result: xbmcplugin.endOfDirectory(self.handle, succeeded=False) - elif isinstance(result, VideoItem) or isinstance(result, AudioItem) or isinstance(result, UriItem): + elif isinstance(result, (VideoItem, AudioItem, UriItem)): self._set_resolved_url(context, result) elif isinstance(result, DirectoryItem): self._add_directory(context, result) diff --git a/resources/lib/youtube_plugin/kodion/items/search_item.py b/resources/lib/youtube_plugin/kodion/items/search_item.py index 273da2831..f04e3ed6e 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_item.py @@ -21,9 +21,7 @@ def __init__(self, context, alt_name=None, image=None, fanart=None, location=Fal if image is None: image = context.create_resource_path('media/search.png') - params = dict() - if location: - params = {'location': location} + params = {'location': location} if location else {} DirectoryItem.__init__(self, name, context.create_uri([constants.paths.SEARCH, 'list'], params=params), image=image) if fanart: diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index 9cdd6b6a4..cabfc1885 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -10,10 +10,18 @@ import json -from .video_item import VideoItem -from .directory_item import DirectoryItem from .audio_item import AudioItem +from .directory_item import DirectoryItem from .image_item import ImageItem +from .video_item import VideoItem + + +_ITEM_TYPES = { + 'AudioItem': AudioItem, + 'DirectoryItem': DirectoryItem, + 'ImageItem': ImageItem, + 'VideoItem': VideoItem, +} def from_json(json_data): @@ -24,25 +32,16 @@ def from_json(json_data): """ def _from_json(_json_data): - mapping = {'VideoItem': lambda: VideoItem('', ''), - 'DirectoryItem': lambda: DirectoryItem('', ''), - 'AudioItem': lambda: AudioItem('', ''), - 'ImageItem': lambda: ImageItem('', '')} - - item = None - item_type = _json_data.get('type', None) - for key in mapping: - if item_type == key: - item = mapping[key]() - break - - if item is None: + item_type = _json_data.get('type') + if not item_type or item_type not in _ITEM_TYPES: return _json_data + item = _ITEM_TYPES[item_type]() + data = _json_data.get('data', {}) - for key in data: + for key, value in data.items(): if hasattr(item, key): - setattr(item, key, data[key]) + setattr(item, key, value) return item @@ -66,14 +65,9 @@ def _to_json(obj): if isinstance(obj, dict): return obj.__dict__ - mapping = {VideoItem: 'VideoItem', - DirectoryItem: 'DirectoryItem', - AudioItem: 'AudioItem', - ImageItem: 'ImageItem'} - - for key in mapping: - if isinstance(obj, key): - return {'type': mapping[key], 'data': obj.__dict__} + for name, item_type in _ITEM_TYPES.items(): + if isinstance(obj, item_type): + return {'type': name, 'data': obj.__dict__} return obj.__dict__ diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index acb0620f9..5bcb90384 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -56,7 +56,7 @@ def __init__(self, name, uri, image='', fanart=''): self._playlist_item_id = None def set_play_count(self, play_count): - self._play_count = int(play_count) + self._play_count = int(play_count or 0) def get_play_count(self): return self._play_count @@ -177,7 +177,7 @@ def set_duration_from_minutes(self, minutes): self.set_duration_from_seconds(int(minutes) * 60) def set_duration_from_seconds(self, seconds): - self._duration = int(seconds) + self._duration = int(seconds or 0) def get_duration(self): return self._duration @@ -245,7 +245,10 @@ def set_mediatype(self, mediatype): self._mediatype = mediatype def get_mediatype(self): - if self._mediatype not in ['video', 'movie', 'tvshow', 'season', 'episode', 'musicvideo']: + if (self._mediatype not in {'video', + 'movie', + 'tvshow', 'season', 'episode', + 'musicvideo'}): self._mediatype = 'video' return self._mediatype @@ -265,19 +268,19 @@ def get_license_key(self): return self.license_key def set_last_played(self, last_played): - self._last_played = last_played + self._last_played = last_played or '' def get_last_played(self): return self._last_played def set_start_percent(self, start_percent): - self._start_percent = start_percent + self._start_percent = start_percent or '' def get_start_percent(self): return self._start_percent def set_start_time(self, start_time): - self._start_time = start_time + self._start_time = start_time or '' def get_start_time(self): return self._start_time diff --git a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py index e575ae3ff..58d586f9c 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py +++ b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py @@ -32,7 +32,7 @@ def set_defaults(self): if 'last_origin' not in data['access_manager']: data['access_manager']['last_origin'] = 'plugin.video.youtube' if 'developers' not in data['access_manager']: - data['access_manager']['developers'] = dict() + data['access_manager']['developers'] = {} # clean up if data['access_manager']['current_user'] == 'default': @@ -61,24 +61,13 @@ def set_defaults(self): data['access_manager']['users'][current_user]['watch_history'] = 'HL' # ensure all users have uuid - uuids = list() - uuid_update = False - for k in list(data['access_manager']['users'].keys()): - c_uuid = data['access_manager']['users'][k].get('id') - if c_uuid: - uuids.append(c_uuid) - else: - if not uuid_update: - uuid_update = True - - if uuid_update: - for k in list(data['access_manager']['users'].keys()): - c_uuid = data['access_manager']['users'][k].get('id') - if not c_uuid: - g_uuid = uuid.uuid4().hex - while g_uuid in uuids: - g_uuid = uuid.uuid4().hex - data['access_manager']['users'][k]['id'] = g_uuid + uuids = set() + for user in data['access_manager']['users'].values(): + c_uuid = user.get('id') + while not c_uuid or c_uuid in uuids: + c_uuid = uuid.uuid4().hex + uuids.add(c_uuid) + user['id'] = c_uuid # end uuid check self.save(data) diff --git a/resources/lib/youtube_plugin/kodion/utils/access_manager.py b/resources/lib/youtube_plugin/kodion/utils/access_manager.py index f3d3a0464..f542aa747 100644 --- a/resources/lib/youtube_plugin/kodion/utils/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/utils/access_manager.py @@ -39,15 +39,12 @@ def get_new_user(self, user_name=''): :param user_name: string, users name :return: a new user dict """ - uuids = list() - new_uuid = uuid.uuid4().hex - - for k in list(self._json['access_manager']['users'].keys()): - user_uuid = self._json['access_manager']['users'][k].get('id') - if user_uuid: - uuids.append(user_uuid) - - while new_uuid in uuids: + uuids = [ + user.get('id') + for user in self._json['access_manager']['users'].values() + ] + new_uuid = None + while not new_uuid or new_uuid in uuids: new_uuid = uuid.uuid4().hex return {'access_token': '', 'refresh_token': '', 'token_expires': -1, 'last_key_hash': '', diff --git a/resources/lib/youtube_plugin/kodion/utils/data_cache.py b/resources/lib/youtube_plugin/kodion/utils/data_cache.py index 62ced2269..abee69e65 100644 --- a/resources/lib/youtube_plugin/kodion/utils/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/utils/data_cache.py @@ -49,7 +49,9 @@ def _decode(obj): for item in query_result: cached_time = item[1] if cached_time is None: - logger.log_error('Data Cache [get_items]: cached_time is None while getting {content_id}'.format(content_id=str(item[0]))) + logger.log_error('Data Cache [get_items]: cached_time is None while getting {content_id}'.format( + content_id=item[0] + )) cached_time = current_time # this is so stupid, but we have the function 'total_seconds' only starting with python 2.7 diff_seconds = self.get_seconds_diff(cached_time) @@ -116,8 +118,7 @@ def _encode(obj): self._open() - for key in list(items.keys()): - item = items[key] + for key, item in items.items(): self._execute(needs_commit, query, values=[key, current_time, _encode(json.dumps(item))]) needs_commit = False diff --git a/resources/lib/youtube_plugin/kodion/utils/http_server.py b/resources/lib/youtube_plugin/kodion/utils/http_server.py index 8e3202719..c63f0a02a 100644 --- a/resources/lib/youtube_plugin/kodion/utils/http_server.py +++ b/resources/lib/youtube_plugin/kodion/utils/http_server.py @@ -237,11 +237,11 @@ def do_POST(self): if size_limit: self.send_header('X-Limit-Video', 'max={size_limit}px'.format(size_limit=str(size_limit))) - for d in list(result.headers.items()): - if re.match('^[Cc]ontent-[Ll]ength$', d[0]): - self.send_header(d[0], response_length) + for header, value in result.headers.items(): + if re.match('^[Cc]ontent-[Ll]ength$', header): + self.send_header(header, response_length) else: - self.send_header(d[0], d[1]) + self.send_header(header, value) self.end_headers() for chunk in self.get_chunks(response_body): diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 490759552..c8daf481c 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -55,7 +55,7 @@ def to_utf8(text): def to_unicode(text): result = text - if isinstance(text, str) or isinstance(text, bytes): + if isinstance(text, (bytes, str)): try: result = text.decode('utf-8', 'ignore') except (AttributeError, UnicodeEncodeError): @@ -65,27 +65,24 @@ def to_unicode(text): def find_best_fit(data, compare_method=None): + if isinstance(data, dict): + data = data.values() + try: - return next(item for item in data if item['container'] == 'mpd') + return next(item for item in data if item.get('container') == 'mpd') except StopIteration: pass - result = None + if not compare_method: + return None + result = None last_fit = -1 - if isinstance(data, dict): - for key in list(data.keys()): - item = data[key] - fit = abs(compare_method(item)) - if last_fit == -1 or fit < last_fit: - last_fit = fit - result = item - elif isinstance(data, list): - for item in data: - fit = abs(compare_method(item)) - if last_fit == -1 or fit < last_fit: - last_fit = fit - result = item + for item in data: + fit = abs(compare_method(item)) + if last_fit == -1 or fit < last_fit: + last_fit = fit + result = item return result @@ -144,7 +141,7 @@ def _find_best_fit_video(_stream_data): sorted_stream_data_list = sorted(stream_data_list, key=_sort_stream_data) context.log_debug('selectable streams: %d' % len(sorted_stream_data_list)) - log_streams = list() + log_streams = [] for sorted_stream_data in sorted_stream_data_list: log_data = copy.deepcopy(sorted_stream_data) if 'license_info' in log_data: @@ -157,9 +154,10 @@ def _find_best_fit_video(_stream_data): selected_stream_data = None if ask_for_quality and len(sorted_stream_data_list) > 1: - items = list() - for sorted_stream_data in sorted_stream_data_list: - items.append((sorted_stream_data['title'], sorted_stream_data)) + items = [ + (sorted_stream_data['title'], sorted_stream_data) + for sorted_stream_data in sorted_stream_data_list + ] result = context.get_ui().on_select(context.localize(localize.SELECT_VIDEO_QUALITY), items) if result != -1: diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index ac952299c..a7ee4818b 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -220,11 +220,10 @@ def _strip_api_keys(self, api_key, client_id, client_secret): keys_changed = _api_check.changed current_user = _api_check.get_current_user() -api = dict() -youtube_tv = dict() - +api = {} api['key'], api['id'], api['secret'] = _api_check.get_api_keys(_api_check.get_current_switch()) +youtube_tv = {} youtube_tv['key'], youtube_tv['id'], youtube_tv['secret'] = _api_check.get_api_keys('youtube-tv') developer_keys = _api_check.get_api_keys('developer') diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 41e7eb3ca..834d56daa 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -398,7 +398,7 @@ def helper(video_id, responses): channel_counts.setdefault(channel_id, 0) if channel_counts[channel_id] <= 3: # Use the item - channel_counts[channel_id] = channel_counts[channel_id] + 1 + channel_counts[channel_id] += 1 item["page_number"] = counter // 50 sorted_items.append(item) else: @@ -775,7 +775,7 @@ def _perform(_page_token, _offset, _result): _result['items'] = cached[cache_items_key] """ no cache, get uploads data from web """ - if len(_result['items']) == 0: + if not _result['items']: # get all subscriptions channel ids sub_page_token = True sub_channel_ids = [] diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index d8b1eeb64..ee62dad50 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -35,13 +35,8 @@ def _get_playlist_data(self, playlist_id): return self._playlist_data.get(playlist_id, {}) def _update_channels(self, channel_ids): - result = dict() - json_data = dict() - channel_ids_to_update = list() - channel_ids_cached = list() - updated_channel_ids = list() - - data_cache = self._context.get_data_cache() + json_data = None + updated_channel_ids = [] function_cache = self._context.get_function_cache() for channel_id in channel_ids: @@ -55,102 +50,90 @@ def _update_channels(self, channel_ids): self._context.log_debug('Channel "mine" not found: %s' % json_data) channel_id = None - json_data = dict() + json_data = None if channel_id: updated_channel_ids.append(channel_id) channel_ids = updated_channel_ids + data_cache = self._context.get_data_cache() channel_data = data_cache.get_items(DataCache.ONE_MONTH, channel_ids) - for channel_id in channel_ids: - if not channel_data.get(channel_id): - channel_ids_to_update.append(channel_id) - else: - channel_ids_cached.append(channel_id) - result.update(channel_data) - if len(channel_ids_cached) > 0: - self._context.log_debug('Found cached data for channels |%s|' % ', '.join(channel_ids_cached)) - - if len(channel_ids_to_update) > 0: - self._context.log_debug('No data for channels |%s| cached' % ', '.join(channel_ids_to_update)) - data = [] - list_of_50s = self._make_list_of_50(channel_ids_to_update) - for list_of_50 in list_of_50s: - data.append(self._youtube_client.get_channels(list_of_50)) + channel_ids = set(channel_ids) + channel_ids_cached = set(channel_data) + channel_ids_to_update = channel_ids - channel_ids_cached + channel_ids_cached = channel_ids & channel_ids_cached - channel_data = dict() - yt_items = [] - for response in data: - yt_items += response.get('items', []) - - for yt_item in yt_items: - channel_id = str(yt_item['id']) - channel_data[channel_id] = yt_item - result[channel_id] = yt_item + result = channel_data + if channel_ids_cached: + self._context.log_debug('Found cached data for channels |%s|' % ', '.join(channel_ids_cached)) + if channel_ids_to_update: + self._context.log_debug('No data for channels |%s| cached' % ', '.join(channel_ids_to_update)) + json_data = [ + self._youtube_client.get_channels(list_of_50) + for list_of_50 in self._list_batch(channel_ids_to_update, n=50) + ] + channel_data = { + yt_item['id']: yt_item + for batch in json_data + for yt_item in batch.get('items', []) + if yt_item + } + result.update(channel_data) data_cache.set_all(channel_data) - self._context.log_debug('Cached data for channels |%s|' % ', '.join(list(channel_data.keys()))) + self._context.log_debug('Cached data for channels |%s|' % ', '.join(channel_data)) if self.handle_error(json_data): return result - - return result + return {} def _update_videos(self, video_ids, live_details=False, suppress_errors=False): - result = dict() - json_data = dict() - video_ids_to_update = list() - video_ids_cached = list() - + json_data = None data_cache = self._context.get_data_cache() - video_data = data_cache.get_items(DataCache.ONE_MONTH, video_ids) - for video_id in video_ids: - if not video_data.get(video_id): - video_ids_to_update.append(video_id) - else: - video_ids_cached.append(video_id) - result.update(video_data) - if len(video_ids_cached) > 0: + + video_ids = set(video_ids) + video_ids_cached = set(video_data) + video_ids_to_update = video_ids - video_ids_cached + video_ids_cached = video_ids & video_ids_cached + + result = video_data + if video_ids_cached: self._context.log_debug('Found cached data for videos |%s|' % ', '.join(video_ids_cached)) - if len(video_ids_to_update) > 0: + if video_ids_to_update: self._context.log_debug('No data for videos |%s| cached' % ', '.join(video_ids_to_update)) json_data = self._youtube_client.get_videos(video_ids_to_update, live_details) - video_data = dict() - yt_items = json_data.get('items', []) - for yt_item in yt_items: - video_id = str(yt_item['id']) - video_data[video_id] = yt_item - result[video_id] = yt_item + video_data = { + yt_item['id']: yt_item + for yt_item in json_data.get('items', []) + if yt_item + } + result.update(video_data) data_cache.set_all(video_data) - self._context.log_debug('Cached data for videos |%s|' % ', '.join(list(video_data.keys()))) + self._context.log_debug('Cached data for videos |%s|' % ', '.join(video_data)) - played_items = dict() if self._context.get_settings().use_local_history(): playback_history = self._context.get_playback_history() played_items = playback_history.get_items(video_ids) - - for k in list(result.keys()): - result[k]['play_data'] = played_items.get(k, dict()) + for video_id, play_data in played_items.items(): + result[video_id]['play_data'] = play_data if self.handle_error(json_data, suppress_errors) or suppress_errors: return result return {} @staticmethod - def _make_list_of_50(list_of_ids): - list_of_50 = [] - pos = 0 - while pos < len(list_of_ids): - list_of_50.append(list_of_ids[pos:pos + 50]) - pos += 50 - return list_of_50 + def _list_batch(input_list, n=50): + if not isinstance(input_list, (list, tuple)): + input_list = list(input_list) + for i in range(0, len(input_list), n): + yield input_list[i:i + n] def get_videos(self, video_ids, live_details=False, suppress_errors=False): - list_of_50s = self._make_list_of_50(video_ids) + list_of_50s = self._list_batch(video_ids, n=50) result = {} for list_of_50 in list_of_50s: @@ -158,41 +141,37 @@ def get_videos(self, video_ids, live_details=False, suppress_errors=False): return result def _update_playlists(self, playlists_ids): - result = dict() - json_data = dict() - playlist_ids_to_update = list() - playlists_ids_cached = list() - + json_data = None data_cache = self._context.get_data_cache() - playlist_data = data_cache.get_items(DataCache.ONE_MONTH, playlists_ids) - for playlist_id in playlists_ids: - if not playlist_data.get(playlist_id): - playlist_ids_to_update.append(playlist_id) - else: - playlists_ids_cached.append(playlist_id) - result.update(playlist_data) - if len(playlists_ids_cached) > 0: + + playlists_ids = set(playlists_ids) + playlists_ids_cached = set(playlist_data) + playlist_ids_to_update = playlists_ids - playlists_ids_cached + playlists_ids_cached = playlists_ids & playlists_ids_cached + + result = playlist_data + if playlists_ids_cached: self._context.log_debug('Found cached data for playlists |%s|' % ', '.join(playlists_ids_cached)) - if len(playlist_ids_to_update) > 0: + if playlist_ids_to_update: self._context.log_debug('No data for playlists |%s| cached' % ', '.join(playlist_ids_to_update)) json_data = self._youtube_client.get_playlists(playlist_ids_to_update) - playlist_data = dict() - yt_items = json_data.get('items', []) - for yt_item in yt_items: - playlist_id = str(yt_item['id']) - playlist_data[playlist_id] = yt_item - result[playlist_id] = yt_item + playlist_data = { + yt_item['id']: yt_item + for yt_item in json_data.get('items', []) + if yt_item + } + result.update(playlist_data) data_cache.set_all(playlist_data) - self._context.log_debug('Cached data for playlists |%s|' % ', '.join(list(playlist_data.keys()))) + self._context.log_debug('Cached data for playlists |%s|' % ', '.join(playlist_data)) if self.handle_error(json_data): return result return {} def get_playlists(self, playlists_ids): - list_of_50s = self._make_list_of_50(playlists_ids) + list_of_50s = self._list_batch(playlists_ids, n=50) result = {} for list_of_50 in list_of_50s: @@ -207,8 +186,9 @@ def get_related_playlists(self, channel_id): if channel_id != 'mine': item = result.get(channel_id, {}) else: - for key in list(result.keys()): - item = result[key] + for item in result.values(): + if item: + break if item is None: return {} @@ -216,7 +196,7 @@ def get_related_playlists(self, channel_id): return item.get('contentDetails', {}).get('relatedPlaylists', {}) def get_channels(self, channel_ids): - list_of_50s = self._make_list_of_50(channel_ids) + list_of_50s = self._list_batch(channel_ids, n=50) result = {} for list_of_50 in list_of_50s: @@ -228,20 +208,20 @@ def get_fanarts(self, channel_ids): return {} result = self._update_channels(channel_ids) - + banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', + 'bannerTvImageUrl', 'bannerExternalUrl'] # transform - for key in list(result.keys()): - item = result[key] - - # set an empty url - result[key] = u'' + for key, item in result.items(): images = item.get('brandingSettings', {}).get('image', {}) - banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', 'bannerTvImageUrl', 'bannerExternalUrl'] for banner in banners: - image = images.get(banner, '') - if image: - result[key] = image - break + image = images.get(banner) + if not image: + continue + result[key] = image + break + else: + # set an empty url + result[key] = '' return result @@ -262,7 +242,7 @@ def handle_error(self, json_data, suppress_errors=False): message = context.localize(30731) ok_dialog = True - if reason == 'quotaExceeded' or reason == 'dailyLimitExceeded': + elif reason in {'quotaExceeded', 'dailyLimitExceeded'}: message_timeout = 7000 if not suppress_errors: diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index b8859d7cb..a8e3c2d69 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -87,7 +87,7 @@ def add_urls(self, urls, provider, context): def get_items(self, provider, context, title_required=True): result = [] - if self._flatten and len(self._channel_ids) > 0: + if self._flatten and self._channel_ids: # remove duplicates self._channel_ids = list(set(self._channel_ids)) @@ -98,7 +98,7 @@ def get_items(self, provider, context, title_required=True): channels_item.set_fanart(provider.get_fanart(context)) result.append(channels_item) - if self._flatten and len(self._playlist_ids) > 0: + if self._flatten and self._playlist_ids: # remove duplicates self._playlist_ids = list(set(self._playlist_ids)) @@ -124,39 +124,42 @@ def get_video_items(self, provider, context, title_required=True): incognito = str(context.get_param('incognito', False)).lower() == 'true' use_play_data = not incognito - if len(self._video_items) == 0: + if not self._video_items: channel_id_dict = {} utils.update_video_infos(provider, context, self._video_id_dict, None, channel_id_dict, use_play_data=use_play_data) utils.update_fanarts(provider, context, channel_id_dict) - for key in self._video_id_dict: - video_item = self._video_id_dict[key] - if not title_required or (title_required and video_item.get_title()): - self._video_items.append(video_item) + self._video_items = [ + video_item + for video_item in self._video_id_dict.values() + if not title_required or video_item.get_title() + ] return self._video_items def get_playlist_items(self, provider, context): - if len(self._playlist_items) == 0: + if not self._playlist_items: channel_id_dict = {} utils.update_playlist_infos(provider, context, self._playlist_id_dict, channel_id_dict) utils.update_fanarts(provider, context, channel_id_dict) - for key in self._playlist_id_dict: - playlist_item = self._playlist_id_dict[key] - if playlist_item.get_name(): - self._playlist_items.append(playlist_item) + self._playlist_items = [ + playlist_item + for playlist_item in self._playlist_id_dict.values() + if playlist_item.get_name() + ] return self._playlist_items def get_channel_items(self, provider, context): - if len(self._channel_items) == 0: + if not self._channel_items: channel_id_dict = {} utils.update_fanarts(provider, context, channel_id_dict) - for key in self._channel_id_dict: - channel_item = self._channel_id_dict[key] - if channel_item.get_name(): - self._channel_items.append(channel_item) + self._channel_items = [ + channel_item + for channel_item in self._channel_id_dict.values() + if channel_item.get_name() + ] return self._channel_items diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index e10804369..1539216f0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -94,16 +94,23 @@ def make_comment_item(context, provider, snippet, uri, total_replies=0): return comment_item -def update_channel_infos(provider, context, channel_id_dict, subscription_id_dict=None, channel_items_dict=None): - if subscription_id_dict is None: - subscription_id_dict = {} +def update_channel_infos(provider, context, channel_id_dict, + subscription_id_dict=None, + channel_items_dict=None, + data=None): + channel_ids = list(channel_id_dict) + if not channel_ids and not data: + return + + if not data: + resource_manager = provider.get_resource_manager(context) + data = resource_manager.get_channels(channel_ids) - channel_ids = list(channel_id_dict.keys()) - if len(channel_ids) == 0: + if not data: return - resource_manager = provider.get_resource_manager(context) - channel_data = resource_manager.get_channels(channel_ids) + if subscription_id_dict is None: + subscription_id_dict = {} filter_list = [] if context.get_path() == '/subscriptions/list/': @@ -112,9 +119,10 @@ def update_channel_infos(provider, context, channel_id_dict, subscription_id_dic filter_list = filter_string.split(',') filter_list = [x.lower() for x in filter_list] - thumb_size = context.get_settings().use_thumbnail_size() - for channel_id in list(channel_data.keys()): - yt_item = channel_data[channel_id] + thumb_size = context.get_settings().use_thumbnail_size + banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', 'bannerTvImageUrl'] + + for channel_id, yt_item in data.items(): channel_item = channel_id_dict[channel_id] snippet = yt_item['snippet'] @@ -144,17 +152,15 @@ def update_channel_infos(provider, context, channel_id_dict, subscription_id_dic yt_context_menu.append_remove_my_subscriptions_filter(context_menu, provider, context, title) else: yt_context_menu.append_add_my_subscriptions_filter(context_menu, provider, context, title) - channel_item.set_context_menu(context_menu) - fanart = u'' fanart_images = yt_item.get('brandingSettings', {}).get('image', {}) - banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', 'bannerTvImageUrl'] for banner in banners: - fanart = fanart_images.get(banner, u'') + fanart = fanart_images.get(banner) if fanart: break - + else: + fanart = '' channel_item.set_fanart(fanart) # update channel mapping @@ -164,21 +170,26 @@ def update_channel_infos(provider, context, channel_id_dict, subscription_id_dic channel_items_dict[channel_id].append(channel_item) -def update_playlist_infos(provider, context, playlist_id_dict, channel_items_dict=None): - playlist_ids = list(playlist_id_dict.keys()) - if len(playlist_ids) == 0: +def update_playlist_infos(provider, context, playlist_id_dict, + channel_items_dict=None, + data=None): + playlist_ids = list(playlist_id_dict) + if not playlist_ids and not data: return - resource_manager = provider.get_resource_manager(context) - access_manager = context.get_access_manager() - playlist_data = resource_manager.get_playlists(playlist_ids) + if not data: + resource_manager = provider.get_resource_manager(context) + data = resource_manager.get_playlists(playlist_ids) + + if not data: + return + access_manager = context.get_access_manager() custom_watch_later_id = access_manager.get_watch_later_id() custom_history_id = access_manager.get_watch_history_id() - thumb_size = context.get_settings().use_thumbnail_size() - for playlist_id in list(playlist_data.keys()): - yt_item = playlist_data[playlist_id] + + for playlist_id, yt_item in data.items(): playlist_item = playlist_id_dict[playlist_id] snippet = yt_item['snippet'] @@ -221,7 +232,7 @@ def update_playlist_infos(provider, context, playlist_id_dict, channel_items_dic else: yt_context_menu.append_set_as_history(context_menu, provider, context, playlist_id, title) - if len(context_menu) > 0: + if context_menu: playlist_item.set_context_menu(context_menu) # update channel mapping @@ -231,25 +242,37 @@ def update_playlist_infos(provider, context, playlist_id_dict, channel_items_dic channel_items_dict[channel_id].append(playlist_item) -def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=None, channel_items_dict=None, live_details=False, use_play_data=True): - settings = context.get_settings() - ui = context.get_ui() +def update_video_infos(provider, context, video_id_dict, + playlist_item_id_dict=None, + channel_items_dict=None, + live_details=False, + use_play_data=True, + data=None): + video_ids = list(video_id_dict) + if not video_ids and not data: + return + + if not data: + resource_manager = provider.get_resource_manager(context) + data = resource_manager.get_videos(video_ids, + live_details=live_details, + suppress_errors=True) - video_ids = list(video_id_dict.keys()) - if len(video_ids) == 0: + if not data: return if not playlist_item_id_dict: playlist_item_id_dict = {} - resource_manager = provider.get_resource_manager(context) - video_data = resource_manager.get_videos(video_ids, live_details=live_details, - suppress_errors=True) + settings = context.get_settings() + show_channel_name = settings.get_bool('youtube.view.description.show_channel_name', True) + alternate_player = settings.is_support_alternative_player_enabled() thumb_size = settings.use_thumbnail_size() thumb_stamp = get_thumb_timestamp() - for video_id in list(video_data.keys()): + ui = context.get_ui() + + for video_id, yt_item in data.items(): datetime = None - yt_item = video_data.get(video_id) video_item = video_id_dict[video_id] # set mediatype @@ -259,36 +282,36 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N continue snippet = yt_item['snippet'] # crash if not conform - play_data = yt_item['play_data'] + play_data = use_play_data and yt_item.get('play_data') video_item.live = snippet.get('liveBroadcastContent') == 'live' # duration - if not video_item.live and use_play_data and play_data.get('total_time'): - video_item.set_duration_from_seconds(float(play_data.get('total_time'))) + if not video_item.live and play_data and 'total_time' in play_data: + duration = float(play_data['total_time'] or 0) else: - duration = yt_item.get('contentDetails', {}).get('duration', '') + duration = yt_item.get('contentDetails', {}).get('duration') if duration: - duration = utils.datetime_parser.parse(duration) - # we subtract 1 seconds because YouTube returns +1 second to much - video_item.set_duration_from_seconds(duration.seconds - 1) + # subtract 1s because YouTube duration is +1s too long + duration = utils.datetime_parser.parse(duration).seconds - 1 + if duration: + video_item.set_duration_from_seconds(duration) - if not video_item.live and use_play_data: - # play count - if play_data.get('play_count'): - video_item.set_play_count(int(play_data.get('play_count'))) + if not video_item.live and play_data: + if 'play_count' in play_data: + video_item.set_play_count(play_data['play_count']) - if play_data.get('played_percent'): - video_item.set_start_percent(play_data.get('played_percent')) + if 'played_percent' in play_data: + video_item.set_start_percent(play_data['played_percent']) - if play_data.get('played_time'): - video_item.set_start_time(play_data.get('played_time')) + if 'played_time' in play_data: + video_item.set_start_time(play_data['played_time']) - if play_data.get('last_played'): - video_item.set_last_played(play_data.get('last_played')) + if 'last_played' in play_data: + video_item.set_last_played(play_data['last_played']) elif video_item.live: video_item.set_play_count(0) - scheduled_start = video_data[video_id].get('liveStreamingDetails', {}).get('scheduledStartTime') + scheduled_start = yt_item.get('liveStreamingDetails', {}).get('scheduledStartTime') if scheduled_start: datetime = utils.datetime_parser.parse(scheduled_start) video_item.set_scheduled_start_utc(datetime) @@ -324,7 +347,7 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N # plot channel_name = snippet.get('channelTitle', '') description = kodion.utils.strip_html_from_text(snippet['description']) - if channel_name and settings.get_bool('youtube.view.description.show_channel_name', True): + if show_channel_name and channel_name: description = '%s[CR][CR]%s' % (ui.uppercase(ui.bold(channel_name)), description) video_item.set_studio(channel_name) # video_item.add_cast(channel_name) @@ -384,7 +407,7 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id) # 'play with...' (external player) - if settings.is_support_alternative_player_enabled(): + if alternate_player: yt_context_menu.append_play_with(context_menu, provider, context) if provider.is_logged_in(): @@ -424,7 +447,7 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N video_item.set_subscription_id(channel_id) yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name) - if not video_item.live and use_play_data: + if not video_item.live and play_data: if play_data.get('play_count') is None or int(play_data.get('play_count')) == 0: yt_context_menu.append_mark_watched(context_menu, provider, context, video_id) else: @@ -447,7 +470,7 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=N yt_context_menu.append_play_ask_for_quality(context_menu, provider, context, video_id) - if len(context_menu) > 0: + if context_menu: video_item.set_context_menu(context_menu, replace=replace_context_menu) @@ -522,7 +545,7 @@ def update_play_info(provider, context, video_id, video_item, video_stream, use_ yt_item = video_data[video_id] snippet = yt_item['snippet'] # crash if not conform - play_data = yt_item['play_data'] + play_data = use_play_data and yt_item.get('play_data') video_item.live = snippet.get('liveBroadcastContent') == 'live' # set the title @@ -530,28 +553,28 @@ def update_play_info(provider, context, video_id, video_item, video_stream, use_ video_item.set_title(snippet['title']) # duration - if not video_item.live and use_play_data and play_data.get('total_time'): - video_item.set_duration_from_seconds(float(play_data.get('total_time'))) + if not video_item.live and play_data and 'total_time' in play_data: + duration = float(play_data['total_time'] or 0) else: - duration = yt_item.get('contentDetails', {}).get('duration', '') + duration = yt_item.get('contentDetails', {}).get('duration') if duration: - duration = utils.datetime_parser.parse(duration) - # we subtract 1 seconds because YouTube returns +1 second to much - video_item.set_duration_from_seconds(duration.seconds - 1) + # subtract 1s because YouTube duration is +1s too long + duration = utils.datetime_parser.parse(duration).seconds - 1 + if duration: + video_item.set_duration_from_seconds(duration) - if not video_item.live and use_play_data: - # play count - if play_data.get('play_count'): - video_item.set_play_count(int(play_data.get('play_count'))) + if not video_item.live and play_data: + if 'play_count' in play_data: + video_item.set_play_count(play_data['play_count']) - if play_data.get('played_percent'): - video_item.set_start_percent(play_data.get('played_percent')) + if 'played_percent' in play_data: + video_item.set_start_percent(play_data['played_percent']) - if play_data.get('played_time'): - video_item.set_start_time(play_data.get('played_time')) + if 'played_time' in play_data: + video_item.set_start_time(play_data['played_time']) - if play_data.get('last_played'): - video_item.set_last_played(play_data.get('last_played')) + if 'last_played' in play_data: + video_item.set_last_played(play_data['last_played']) # plot channel_name = snippet.get('channelTitle', '') @@ -581,19 +604,23 @@ def update_play_info(provider, context, video_id, video_item, video_stream, use_ return video_item -def update_fanarts(provider, context, channel_items_dict): +def update_fanarts(provider, context, channel_items_dict, data=None): # at least we need one channel id - channel_ids = list(channel_items_dict.keys()) - if len(channel_ids) == 0: + channel_ids = list(channel_items_dict) + if not channel_ids and not data: return - fanarts = provider.get_resource_manager(context).get_fanarts(channel_ids) + if not data: + resource_manager = provider.get_resource_manager(context) + data = resource_manager.get_fanarts(channel_ids) + + if not data: + return - for channel_id in channel_ids: - channel_items = channel_items_dict[channel_id] + for channel_id, channel_items in channel_items_dict.items(): for channel_item in channel_items: # only set not empty fanarts - fanart = fanarts.get(channel_id, '') + fanart = data.get(channel_id, '') if fanart: channel_item.set_fanart(fanart) @@ -623,11 +650,15 @@ def get_shelf_index_by_title(context, json_data, shelf_title): title = shelf.get('shelfRenderer', {}).get('title', {}).get('runs', [{}])[0].get('text', '') if title.lower() == shelf_title.lower(): shelf_index = idx - context.log_debug('Found shelf index |{index}| for |{title}|'.format(index=str(shelf_index), title=shelf_title)) + context.log_debug('Found shelf index |{index}| for |{title}|'.format( + index=shelf_index, title=shelf_title + )) break if shelf_index is not None and 0 > shelf_index >= len(contents): - context.log_debug('Shelf index |{index}| out of range |0-{content_length}|'.format(index=str(shelf_index), content_length=str(len(contents)))) + context.log_debug('Shelf index |{index}| out of range |0-{content_length}|'.format( + index=shelf_index, content_length=len(contents) + )) shelf_index = None return shelf_index diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 403cfc5b4..186a60c33 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -27,7 +27,7 @@ def _process_list_response(provider, context, json_data): thumb_size = context.get_settings().use_thumbnail_size() yt_items = json_data.get('items', []) - if len(yt_items) == 0: + if not yt_items: context.log_warning('List of search result is empty') return result @@ -48,9 +48,9 @@ def _process_list_response(provider, context, json_data): image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) video_item = items.VideoItem(title, item_uri, image=image) video_item.video_id = video_id @@ -66,9 +66,9 @@ def _process_list_response(provider, context, json_data): image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) channel_item = items.DirectoryItem(title, item_uri, image=image) channel_item.set_fanart(provider.get_fanart(context)) @@ -86,9 +86,9 @@ def _process_list_response(provider, context, json_data): title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) item_params = {'guide_id': guide_id} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['special', 'browse_channels'], item_params) guide_item = items.DirectoryItem(title, item_uri) guide_item.set_fanart(provider.get_fanart(context)) @@ -100,9 +100,9 @@ def _process_list_response(provider, context, json_data): channel_id = snippet['resourceId']['channelId'] item_params = {} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) channel_item = items.DirectoryItem(title, item_uri, image=image) channel_item.set_fanart(provider.get_fanart(context)) @@ -125,9 +125,9 @@ def _process_list_response(provider, context, json_data): channel_id = 'mine' item_params = {} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) playlist_item = items.DirectoryItem(title, item_uri, image=image) playlist_item.set_fanart(provider.get_fanart(context)) @@ -144,9 +144,9 @@ def _process_list_response(provider, context, json_data): image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) video_item = items.VideoItem(title, item_uri, image=image) video_item.video_id = video_id @@ -175,9 +175,9 @@ def _process_list_response(provider, context, json_data): image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) video_item = items.VideoItem(title, item_uri, image=image) video_item.video_id = video_id @@ -213,9 +213,9 @@ def _process_list_response(provider, context, json_data): image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) video_item = items.VideoItem(title, item_uri, image=image) video_item.video_id = video_id @@ -238,9 +238,9 @@ def _process_list_response(provider, context, json_data): # channel_name = snippet.get('channelTitle', '') item_params = {} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) playlist_item = items.DirectoryItem(title, item_uri, image=image) playlist_item.set_fanart(provider.get_fanart(context)) @@ -253,9 +253,9 @@ def _process_list_response(provider, context, json_data): image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) channel_item = items.DirectoryItem(title, item_uri, image=image) channel_item.set_fanart(provider.get_fanart(context)) @@ -352,7 +352,7 @@ def handle_error(provider, context, json_data): message = context.localize(provider.LOCAL_MAP['youtube.api.key.incorrect']) message_timeout = 7000 - if reason == 'quotaExceeded' or reason == 'dailyLimitExceeded': + if reason in {'quotaExceeded', 'dailyLimitExceeded'}: message_timeout = 7000 if ok_dialog: @@ -366,23 +366,7 @@ def handle_error(provider, context, json_data): def _parse_kind(item): - kind = item.get('kind', '').split('#') - - if len(kind) < 1: - return False, '' - - if len(kind) < 2: - try: - _ = kind.index('youtube') - return True, '' - except ValueError: - return False, str(kind[0]).lower() - - try: - idx = kind.index('youtube') - if idx == 0: - return True, str(kind[1]).lower() - except ValueError: - pass - - return False, str(kind[1]).lower() + parts = item.get('kind', '').split('#') + is_youtube = parts[0] == 'youtube' + kind = parts[1 if len(parts) > 1 else 0].lower() + return is_youtube, kind diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 42d7b7826..0c6fc0040 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -2143,13 +2143,15 @@ def _filter_group(previous_group, previous_stream, item): main_stream['multi_audio'] = True filepath = '{0}{1}.mpd'.format(basepath, self.video_id) - success = None - with xbmcvfs.File(filepath, 'w') as mpd_file: - success = mpd_file.write(str(out)) - if not success: - return None, None - return 'http://{0}:{1}/{2}.mpd'.format( - _settings.httpd_listen(for_request=True), - _settings.httpd_port(), - self.video_id - ), main_stream + try: + with xbmcvfs.File(filepath, 'w') as mpd_file: + success = mpd_file.write(str(out)) + except (IOError, OSError): + success = False + if success: + return 'http://{0}:{1}/{2}.mpd'.format( + _settings.httpd_listen(for_request=True), + _settings.httpd_port(), + self.video_id + ), main_stream + return None, None diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index e62163555..e4e2ec096 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -53,7 +53,7 @@ def play_video(provider, context): context.log_error(traceback.print_exc()) return False - if len(video_streams) == 0: + if not video_streams: message = context.localize(provider.LOCAL_MAP['youtube.error.no_video_streams_found']) context.get_ui().show_notification(message, time_milliseconds=5000) return False diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index c7b5c3fe8..8a9c0e114 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -183,7 +183,7 @@ def _extract_urls(_video_id): progress_dialog.close() - if len(result) == 0: + if not result: progress_dialog.close() context.get_ui().on_ok(title=context.localize(provider.LOCAL_MAP['youtube.video.description.links']), text=context.localize( @@ -253,13 +253,13 @@ def _display_playlists(_playlist_ids): channel_ids = context.get_param('channel_ids', '') if channel_ids: channel_ids = channel_ids.split(',') - if len(channel_ids) > 0: + if channel_ids: return _display_channels(channel_ids) playlist_ids = context.get_param('playlist_ids', '') if playlist_ids: playlist_ids = playlist_ids.split(',') - if len(playlist_ids) > 0: + if playlist_ids: return _display_playlists(playlist_ids) context.log_error('Missing video_id or playlist_ids for description links') diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 280062ad3..7ede578b6 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -226,7 +226,7 @@ def get_dev_config(context, addon_id, dev_configs): _dev_config = context.get_ui().get_home_window_property('configs') context.get_ui().clear_home_window_property('configs') - dev_config = dict() + dev_config = {} if _dev_config is not None: context.log_debug('Using window property for developer keys is deprecated, instead use the youtube_registration module.') try: @@ -246,7 +246,7 @@ def get_dev_config(context, addon_id, dev_configs): or not dev_config['main'].get('id') or not dev_config['main'].get('secret'): context.log_error('Error loading developer config: |invalid structure| ' 'expected: |{"origin": ADDON_ID, "main": {"system": SYSTEM_NAME, "key": API_KEY, "id": CLIENT_ID, "secret": CLIENT_SECRET}}|') - return dict() + return {} dev_origin = dev_config['origin'] dev_main = dev_config['main'] dev_system = dev_main['system'] @@ -261,7 +261,7 @@ def get_dev_config(context, addon_id, dev_configs): context.log_debug('Using developer config: origin: |{0}| system |{1}|'.format(dev_origin, dev_system)) return {'origin': dev_origin, 'main': {'id': dev_id, 'secret': dev_secret, 'key': dev_key, 'system': dev_system}} - return dict() + return {} def reset_client(self): self._client = None @@ -285,12 +285,10 @@ def get_client(self, context): dev_id = context.get_param('addon_id', None) dev_configs = YouTube.CONFIGS.get('developer') dev_config = self.get_dev_config(context, dev_id, dev_configs) - dev_keys = dict() - if dev_config: - dev_keys = dev_config.get('main') + dev_keys = dev_config.get('main') if dev_config else None client = None - refresh_tokens = list() + refresh_tokens = [] if dev_id: dev_origin = dev_config.get('origin') if dev_config.get('origin') else dev_id @@ -308,13 +306,13 @@ def get_client(self, context): if len(access_tokens) != 2 or access_manager.is_dev_access_token_expired(dev_id): # reset access_token access_manager.update_dev_access_token(dev_id, '') - access_tokens = list() + access_tokens = [] else: access_tokens = access_manager.get_access_token().split('|') if len(access_tokens) != 2 or access_manager.is_access_token_expired(): # reset access_token access_manager.update_access_token('') - access_tokens = list() + access_tokens = [] if dev_id: if dev_keys: @@ -405,7 +403,7 @@ def get_client(self, context): context.log_debug('User is logged in' if self._is_logged_in else 'User is not logged in') - if len(access_tokens) == 0: + if not access_tokens: access_tokens = ['', ''] client.set_access_token(access_token=access_tokens[1]) client.set_access_token_tv(access_token_tv=access_tokens[0]) @@ -445,7 +443,7 @@ def on_uri2addon(self, context, re_match): url_converter = UrlToItemConverter(flatten=True) url_converter.add_urls([res_url], self, context) items = url_converter.get_items(self, context, title_required=False) - if len(items) > 0: + if items: return items[0] return False @@ -596,7 +594,7 @@ def _on_channel(self, context, re_match): # we correct the channel id based on the username items = json_data.get('items', []) - if len(items) > 0: + if items: if method == 'user': channel_id = items[0]['id'] else: @@ -666,7 +664,7 @@ def _on_my_location(self, context, re_match): self.set_content_type(context, kodion.constants.content_type.FILES) settings = context.get_settings() - result = list() + result = [] # search search_item = kodion.items.SearchItem(context, image=context.create_resource_path('media', 'search.png'), @@ -1199,8 +1197,7 @@ def maintenance_actions(self, context, re_match): client = self.get_client(context) if access_manager.has_refresh_token(): refresh_tokens = access_manager.get_refresh_token().split('|') - refresh_tokens = list(set(refresh_tokens)) - for refresh_token in refresh_tokens: + for refresh_token in set(refresh_tokens): try: client.revoke(refresh_token) except: @@ -1614,8 +1611,7 @@ def set_content_type(context, content_type): kodion.constants.sort_method.DATE) def handle_exception(self, context, exception_to_handle): - if (isinstance(exception_to_handle, InvalidGrant) or - isinstance(exception_to_handle, LoginException)): + if isinstance(exception_to_handle, (InvalidGrant, LoginException)): ok_dialog = False message_timeout = 5000 diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index 0a4217592..513546a64 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -78,7 +78,7 @@ def get_videos(video_id, addon_id=None): if not handle_error(context, json_data): return [json_data] - return [item for item in json_data.get('items', [])] + return json_data.get('items', []) def get_activities(channel_id, page_token='', all_pages=False, addon_id=None): @@ -105,8 +105,7 @@ def get_items(_page_token=''): if not handle_error(context, json_data): return [json_data] - for item in json_data.get('items', []): - items.append(item) + items.extend(json_data.get('items', [])) error = False next_page_token = json_data.get('nextPageToken') @@ -151,8 +150,7 @@ def get_items(_page_token=''): if not handle_error(context, json_data): return [json_data] - for item in json_data.get('items', []): - items.append(item) + items.extend(json_data.get('items', [])) error = False next_page_token = json_data.get('nextPageToken') @@ -189,7 +187,7 @@ def get_channel_id(channel_name, addon_id=None): if not handle_error(context, json_data): return [json_data] - return [item for item in json_data.get('items', [])] + return json_data.get('items', []) def get_channels(channel_id, addon_id=None): @@ -209,7 +207,7 @@ def get_channels(channel_id, addon_id=None): if not handle_error(context, json_data): return [json_data] - return [item for item in json_data.get('items', [])] + return json_data.get('items', []) def get_channel_sections(channel_id, addon_id=None): @@ -229,7 +227,7 @@ def get_channel_sections(channel_id, addon_id=None): if not handle_error(context, json_data): return [json_data] - return [item for item in json_data.get('items', [])] + return json_data.get('items', []) def get_playlists_of_channel(channel_id, page_token='', all_pages=False, addon_id=None): @@ -257,8 +255,7 @@ def get_items(_page_token=''): if not handle_error(context, json_data): return [json_data] - for item in json_data.get('items', []): - items.append(item) + items.extend(json_data.get('items', [])) error = False next_page_token = json_data.get('nextPageToken') @@ -295,7 +292,7 @@ def get_playlists(playlist_id, addon_id=None): if not handle_error(context, json_data): return [json_data] - return [item for item in json_data.get('items', [])] + return json_data.get('items', []) def get_related_videos(video_id, page_token='', addon_id=None): @@ -322,9 +319,8 @@ def get_items(_page_token=''): if not handle_error(context, json_data): return [json_data] - for item in json_data.get('items', []): - if 'snippet' in item: - items.append(item) + items.extend([item for item in json_data.get('items', []) + if 'snippet' in item]) error = False next_page_token = json_data.get('nextPageToken') @@ -376,8 +372,7 @@ def get_items(_page_token=''): if not handle_error(context, json_data): return [json_data] - for item in json_data.get('items', []): - items.append(item) + items.extend(json_data.get('items', [])) error = False next_page_token = json_data.get('nextPageToken') From 97dfa3a32734d56a46ecafa0212f48ba9df17744 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:42:59 +1100 Subject: [PATCH 014/141] Update release names --- resources/lib/youtube_plugin/kodion/utils/system_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py index 09f0553fe..de01118ce 100644 --- a/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -45,9 +45,9 @@ def __init__(self, version, releasename, appname): self._releasename = 'Unknown Release' if self._version >= (21, 0): - self._releasename = 'O*****' + self._releasename = 'Omega' elif self._version >= (20, 0): - self._releasename = 'N*****' + self._releasename = 'Nexus' elif self._version >= (19, 0): self._releasename = 'Matrix' elif self._version >= (18, 0): From 86d1792d3039520b50dbb85d159313a9a7755c09 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:01:03 +1100 Subject: [PATCH 015/141] ISA - fix logic & naming for text, vars and methods - IA to ISA, ia to isa, adaptive to isa - MPD to ISA, mpd to isa when non MPEG-DASH specific - new HLS/hls variables and methods for HLS specific functionality --- .../resource.language.en_au/strings.po | 14 ++--- .../resource.language.en_gb/strings.po | 14 ++--- .../resource.language.en_nz/strings.po | 24 ++++----- .../resource.language.en_us/strings.po | 14 ++--- .../kodion/constants/const_settings.py | 4 +- .../kodion/impl/abstract_settings.py | 20 +++---- .../kodion/impl/xbmc/xbmc_context.py | 14 ++--- .../kodion/impl/xbmc/xbmc_items.py | 53 ++++++++----------- .../youtube_plugin/kodion/items/video_item.py | 20 +++++-- .../kodion/utils/http_server.py | 4 +- .../youtube_plugin/kodion/utils/methods.py | 2 +- .../youtube_plugin/kodion/utils/monitor.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 7 ++- .../youtube/helper/video_info.py | 8 +-- .../lib/youtube_plugin/youtube/provider.py | 4 +- resources/settings.xml | 31 ++++++----- 16 files changed, 120 insertions(+), 115 deletions(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index e83f6d632..287c9965c 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -53,7 +53,7 @@ msgstr "" # empty strings from id 30003 to 30006 msgctxt "#30007" -msgid "Use MPEG-DASH" +msgid "Use InputStream Adaptive" msgstr "" msgctxt "#30008" @@ -614,7 +614,7 @@ msgid "Force SSL certificate verification" msgstr "" msgctxt "#30579" -msgid "MPEG-DASH is enabled in the YouTube settings, however InputStream Adaptive appears to be disabled. Would you like to enable InputStream Adaptive now?" +msgid "InputStream Adaptive is activated in the YouTube settings, however the add-on has been disabled. Would you like to enable InputStream Adaptive now?" msgstr "" msgctxt "#30580" @@ -766,11 +766,11 @@ msgid "Must be signed in." msgstr "" msgctxt "#30617" -msgid "MPEG-DASH" +msgid "InputStream Adaptive" msgstr "" msgctxt "#30618" -msgid "Enable mpeg-dash proxy" +msgid "Enable MPEG-DASH proxy" msgstr "" msgctxt "#30619" @@ -782,7 +782,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)" msgstr "" msgctxt "#30622" @@ -1050,7 +1050,7 @@ msgid "data cache" msgstr "" msgctxt "#30688" -msgid "Use for videos" +msgid "Use MPEG-DASH for videos" msgstr "" msgctxt "#30689" @@ -1190,7 +1190,7 @@ msgid "Enable HDR video" msgstr "" msgctxt "#30723" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)[CR]> 1080p and HDR requires InputStream Adaptive >= 2.3.14" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)[CR]HDR and >1080p video requires InputStream Adaptive >= 2.3.14" msgstr "" msgctxt "#30724" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 6856bd109..a78adb225 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -53,7 +53,7 @@ msgstr "" # empty strings from id 30003 to 30006 msgctxt "#30007" -msgid "Use MPEG-DASH" +msgid "Use InputStream Adaptive" msgstr "" msgctxt "#30008" @@ -614,7 +614,7 @@ msgid "Force SSL certificate verification" msgstr "" msgctxt "#30579" -msgid "MPEG-DASH is enabled in the YouTube settings, however InputStream Adaptive appears to be disabled. Would you like to enable InputStream Adaptive now?" +msgid "InputStream Adaptive is activated in the YouTube settings, however the add-on has been disabled. Would you like to enable InputStream Adaptive now?" msgstr "" msgctxt "#30580" @@ -766,11 +766,11 @@ msgid "Must be signed in." msgstr "" msgctxt "#30617" -msgid "MPEG-DASH" +msgid "InputStream Adaptive" msgstr "" msgctxt "#30618" -msgid "Enable mpeg-dash proxy" +msgid "Enable MPEG-DASH proxy" msgstr "" msgctxt "#30619" @@ -782,7 +782,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)" msgstr "" msgctxt "#30622" @@ -1050,7 +1050,7 @@ msgid "data cache" msgstr "" msgctxt "#30688" -msgid "Use for videos" +msgid "Use MPEG-DASH for videos" msgstr "" msgctxt "#30689" @@ -1190,7 +1190,7 @@ msgid "Enable HDR video" msgstr "" msgctxt "#30723" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)[CR]> 1080p and HDR requires InputStream Adaptive >= 2.3.14" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)[CR]HDR and >1080p video requires InputStream Adaptive >= 2.3.14" msgstr "" msgctxt "#30724" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index a768a5e92..7dc6ba76d 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -52,7 +52,7 @@ msgstr "" # empty strings from id 30003 to 30006 msgctxt "#30007" -msgid "Use MPEG-DASH" +msgid "Use InputStream Adaptive" msgstr "" msgctxt "#30008" @@ -610,7 +610,7 @@ msgid "Force SSL certificate verification" msgstr "" msgctxt "#30579" -msgid "MPEG-DASH is enabled in the YouTube settings, however InputStream Adaptive appears to be disabled. Would you like to enable InputStream Adaptive now?" +msgid "InputStream Adaptive is activated in the YouTube settings, however the add-on has been disabled. Would you like to enable InputStream Adaptive now?" msgstr "" msgctxt "#30580" @@ -762,11 +762,11 @@ msgid "Must be signed in." msgstr "" msgctxt "#30617" -msgid "MPEG-DASH" +msgid "InputStream Adaptive" msgstr "" msgctxt "#30618" -msgid "Enable mpeg-dash proxy" +msgid "Enable MPEG-DASH proxy" msgstr "" msgctxt "#30619" @@ -778,7 +778,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)" msgstr "" msgctxt "#30622" @@ -1046,7 +1046,7 @@ msgid "data cache" msgstr "" msgctxt "#30688" -msgid "Use for videos" +msgid "Use MPEG-DASH for videos" msgstr "" msgctxt "#30689" @@ -1182,19 +1182,19 @@ msgid "Default to WEBM adaptation set (4K)" msgstr "" msgctxt "#30722" -msgid "HDR" +msgid "Enable HDR video" msgstr "" msgctxt "#30723" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)[CR]> 1080p and HDR requires InputStream Adaptive >= 2.3.14" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)[CR]HDR and >1080p video requires InputStream Adaptive >= 2.3.14" msgstr "" msgctxt "#30724" -msgid "Limit to 30fps" +msgid "Enable high framerate video" msgstr "" msgctxt "#30725" -msgid "1440p (HD)" +msgid "1440p (QHD)" msgstr "" msgctxt "#30726" @@ -1202,11 +1202,11 @@ msgid "Uploads" msgstr "" msgctxt "#30727" -msgid "Adaptive (MP4/H264)" +msgid "Enable H.264 video" msgstr "" msgctxt "#30728" -msgid "Adaptive (WEBM/VP9)" +msgid "Enable VP9 video" msgstr "" msgctxt "#30729" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index 8e30389e1..574fb67d4 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -54,7 +54,7 @@ msgstr "Password" # empty strings from id 30003 to 30006 msgctxt "#30007" -msgid "Use MPEG-DASH" +msgid "Use InputStream Adaptive" msgstr "" msgctxt "#30008" @@ -615,7 +615,7 @@ msgid "Force SSL certificate verification" msgstr "" msgctxt "#30579" -msgid "MPEG-DASH is enabled in the YouTube settings, however InputStream Adaptive appears to be disabled. Would you like to enable InputStream Adaptive now?" +msgid "InputStream Adaptive is activated in the YouTube settings, however the add-on has been disabled. Would you like to enable InputStream Adaptive now?" msgstr "" msgctxt "#30580" @@ -767,11 +767,11 @@ msgid "Must be signed in." msgstr "" msgctxt "#30617" -msgid "MPEG-DASH" +msgid "InputStream Adaptive" msgstr "" msgctxt "#30618" -msgid "Enable mpeg-dash proxy" +msgid "Enable MPEG-DASH proxy" msgstr "" msgctxt "#30619" @@ -783,7 +783,7 @@ msgid "Port %s already in use. Cannot start http server." msgstr "" msgctxt "#30621" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)" msgstr "" msgctxt "#30622" @@ -1051,7 +1051,7 @@ msgid "data cache" msgstr "" msgctxt "#30688" -msgid "Use for videos" +msgid "Use MPEG-DASH for videos" msgstr "" msgctxt "#30689" @@ -1191,7 +1191,7 @@ msgid "Enable HDR video" msgstr "" msgctxt "#30723" -msgid "Proxy is required for mpeg-dash vods (see HTTP Server)[CR]> 1080p and HDR requires InputStream Adaptive >= 2.3.14" +msgid "Proxy is required for MPEG-DASH VODs (see HTTP Server)[CR]HDR and >1080p video requires InputStream Adaptive >= 2.3.14" msgstr "" msgctxt "#30724" diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 6f64ca460..0ad5123bd 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -35,8 +35,8 @@ VIDEO_QUALITY = 'kodion.video.quality' # (int) VIDEO_QUALITY_ASK = 'kodion.video.quality.ask' # (bool) -USE_MPD = 'kodion.video.quality.mpd' # (bool) -LIVE_STREAMS = 'kodion.mpd.live_stream.selection' # (int) +USE_ISA = 'kodion.video.quality.isa' # (bool) +LIVE_STREAMS = 'kodion.live_stream.selection' # (int) MPD_VIDEOS = 'kodion.mpd.videos' # (bool) MPD_QUALITY_SELECTION = 'kodion.mpd.quality.selection' # (int) MPD_STREAM_FEATURES = 'kodion.mpd.stream.features' # (list[string]) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index c231cea4d..bb1c2b437 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -96,8 +96,8 @@ def is_support_alternative_player_enabled(self): def alternative_player_web_urls(self): return self.get_bool(SETTINGS.ALTERNATIVE_PLAYER_WEB_URLS, False) - def use_mpd(self): - return self.get_bool(SETTINGS.USE_MPD, False) + def use_isa(self): + return self.get_bool(SETTINGS.USE_ISA, False) def subtitle_languages(self): return self.get_int(SETTINGS.SUBTITLE_LANGUAGE, 0) @@ -137,31 +137,31 @@ def allow_dev_keys(self): return self.get_bool(SETTINGS.ALLOW_DEV_KEYS, False) def use_mpd_videos(self): - if self.use_mpd(): + if self.use_isa(): return self.get_bool(SETTINGS.MPD_VIDEOS, False) return False _LIVE_STREAM_TYPES = { 0: 'mpegts', 1: 'hls', - 2: 'ia_hls', - 3: 'ia_mpd', + 2: 'isa_hls', + 3: 'isa_mpd', } def get_live_stream_type(self): - if self.use_mpd(): + if self.use_isa(): stream_type = self.get_int(SETTINGS.LIVE_STREAMS + '.1', 0) else: stream_type = self.get_int(SETTINGS.LIVE_STREAMS + '.2', 0) return self._LIVE_STREAM_TYPES.get(stream_type) or self._LIVE_STREAM_TYPES[0] - def use_adaptive_live_streams(self): - if self.use_mpd(): + def use_isa_live_streams(self): + if self.use_isa(): return self.get_int(SETTINGS.LIVE_STREAMS + '.1', 0) > 1 - return self.get_int(SETTINGS.LIVE_STREAMS + '.2', 0) > 1 + return False def use_mpd_live_streams(self): - if self.use_mpd(): + if self.use_isa(): return self.get_int(SETTINGS.LIVE_STREAMS + '.1', 0) == 3 return False diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 867296b51..3fc817fb1 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -272,7 +272,7 @@ def send_notification(self, method, data): self.execute('NotifyAll(plugin.video.youtube,%s,%s)' % (method, data)) def use_inputstream_adaptive(self): - if self._settings.use_mpd_videos() or self._settings.use_adaptive_live_streams(): + if self._settings.use_isa(): if self.addon_enabled('inputstream.adaptive'): success = True elif self.get_ui().on_yes_no_input(self.get_name(), self.localize(30579)): @@ -287,7 +287,7 @@ def use_inputstream_adaptive(self): # - required version number as string for comparison with actual installed InputStream.Adaptive version # - any Falsey value to exclude capability regardless of version # - True to include capability regardless of version - _IA_CAPABILITIES = { + _ISA_CAPABILITIES = { 'live': '2.0.12', 'drm': '2.2.12', # audio codecs @@ -315,16 +315,16 @@ def inputstream_adaptive_capabilities(self, capability=None): if not self.use_inputstream_adaptive() or not inputstream_version: return frozenset() if capability is None else None - ia_loose_version = utils.loose_version(inputstream_version) + isa_loose_version = utils.loose_version(inputstream_version) if capability is None: capabilities = frozenset( - capability for capability, version in self._IA_CAPABILITIES.items() + capability for capability, version in self._ISA_CAPABILITIES.items() if version is True - or version and ia_loose_version >= utils.loose_version(version) + or version and isa_loose_version >= utils.loose_version(version) ) return capabilities - version = self._IA_CAPABILITIES.get(capability) - return version is True or version and ia_loose_version >= utils.loose_version(version) + version = self._ISA_CAPABILITIES.get(capability) + return version is True or version and isa_loose_version >= utils.loose_version(version) def inputstream_adaptive_auto_stream_selection(self): try: diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py index 9c43c5b49..7f715b1c2 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py @@ -33,7 +33,8 @@ def set_info(self, *args, **kwargs): def to_play_item(context, play_item): - context.log_debug('Converting PlayItem |%s|' % play_item.get_uri()) + uri = play_item.get_uri() + context.log_debug('Converting PlayItem |%s|' % uri) is_strm = str(context.get_param('strm', False)).lower() == 'true' @@ -56,36 +57,28 @@ def to_play_item(context, play_item): list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) - if settings.is_support_alternative_player_enabled() and \ - settings.alternative_player_web_urls() and \ - not play_item.get_license_key(): - play_item.set_uri('https://www.youtube.com/watch?v={video_id}'.format(video_id=play_item.video_id)) + headers = play_item.get_headers() + license_key = play_item.get_license_key() + alternative_player = settings.is_support_alternative_player_enabled() - ia_enabled = context.addon_enabled('inputstream.adaptive') + if (alternative_player and settings.alternative_player_web_urls() + and not license_key): + play_item.set_uri('https://www.youtube.com/watch?v={video_id}'.format( + video_id=play_item.video_id + )) - if ia_enabled and play_item.use_mpd_video() and not play_item.live: - list_item.setContentLookup(False) - list_item.setMimeType('application/xml+dash') - list_item.setProperty('inputstream', 'inputstream.adaptive') - list_item.setProperty('inputstream.adaptive.manifest_type', 'mpd') - if 'auto' in settings.stream_select(): - list_item.setProperty('inputstream.adaptive.stream_selection_type', 'adaptive') - - if play_item.get_headers(): - list_item.setProperty('inputstream.adaptive.manifest_headers', play_item.get_headers()) - list_item.setProperty('inputstream.adaptive.stream_headers', play_item.get_headers()) + isa_enabled = settings.use_isa() and context.addon_enabled('inputstream.adaptive') - if play_item.get_license_key(): - list_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') - list_item.setProperty('inputstream.adaptive.license_key', play_item.get_license_key()) - - elif ia_enabled and play_item.live and settings.use_adaptive_live_streams(): - if settings.use_mpd_live_streams(): + if isa_enabled and play_item.use_isa_video(): + if play_item.use_mpd_video(): manifest_type = 'mpd' mime_type = 'application/xml+dash' # MPD manifest update is currently broken # Following line will force a full update but restart live stream from start - # list_item.setProperty('inputstream.adaptive.manifest_update_parameter', 'full') + # if play_item.live: + # list_item.setProperty('inputstream.adaptive.manifest_update_parameter', 'full') + if 'auto' in settings.stream_select(): + list_item.setProperty('inputstream.adaptive.stream_selection_type', 'adaptive') else: manifest_type = 'hls' mime_type = 'application/x-mpegURL' @@ -95,16 +88,14 @@ def to_play_item(context, play_item): list_item.setProperty('inputstream', 'inputstream.adaptive') list_item.setProperty('inputstream.adaptive.manifest_type', manifest_type) - if play_item.get_headers(): - list_item.setProperty('inputstream.adaptive.manifest_headers', play_item.get_headers()) - list_item.setProperty('inputstream.adaptive.stream_headers', play_item.get_headers()) + if headers: + list_item.setProperty('inputstream.adaptive.manifest_headers', headers) + list_item.setProperty('inputstream.adaptive.stream_headers', headers) - if play_item.get_license_key(): + if license_key: list_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') - list_item.setProperty('inputstream.adaptive.license_key', play_item.get_license_key()) - + list_item.setProperty('inputstream.adaptive.license_key', license_key) else: - uri = play_item.get_uri() if 'mime=' in uri: try: mime_type = uri.split('mime=', 1)[-1].split('&', 1)[0].replace('%2F', '/', 1) diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 5bcb90384..35a819be9 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -40,7 +40,7 @@ def __init__(self, name, uri, image='', fanart=''): self._studio = None self._artist = None self._play_count = None - self._uses_mpd = None + self._uses_isa = None self._mediatype = None self._last_played = None self._start_percent = None @@ -235,11 +235,23 @@ def set_date_from_datetime(self, date_time): def get_date(self): return self._date - def set_use_mpd_video(self, value=True): - self._uses_mpd = value + def set_isa_video(self, value=True): + self._uses_isa = value + + def use_isa_video(self): + return self._uses_isa + + def use_hls_video(self): + uri = self.get_uri() + if 'manifest/hls' in uri or uri.endswith('.m3u8'): + return True + return False def use_mpd_video(self): - return self._uses_mpd is True and ('manifest/dash' in self.get_uri() or self.get_uri().endswith('.mpd')) + uri = self.get_uri() + if 'manifest/dash' in uri or uri.endswith('.mpd'): + return True + return False def set_mediatype(self, mediatype): self._mediatype = mediatype diff --git a/resources/lib/youtube_plugin/kodion/utils/http_server.py b/resources/lib/youtube_plugin/kodion/utils/http_server.py index c63f0a02a..909ab0034 100644 --- a/resources/lib/youtube_plugin/kodion/utils/http_server.py +++ b/resources/lib/youtube_plugin/kodion/utils/http_server.py @@ -62,7 +62,7 @@ def connection_allowed(self): # noinspection PyPep8Naming def do_GET(self): addon = xbmcaddon.Addon('plugin.video.youtube') - mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.mpd') == 'true' + mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true' api_config_enabled = addon.getSetting('youtube.api.config.page') == 'true' # Strip trailing slash if present @@ -166,7 +166,7 @@ def do_HEAD(self): self.send_error(403) else: addon = xbmcaddon.Addon('plugin.video.youtube') - mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.mpd') == 'true' + mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true' if mpd_proxy_enabled and self.path.endswith('.mpd'): file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) if not os.path.isfile(file_path): diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index c8daf481c..dc5df817f 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -97,7 +97,7 @@ def _sort_stream_data(_stream_data): ask_for_quality = context.get_settings().ask_for_video_quality() if ask_for_quality is None else ask_for_quality video_quality = settings.get_video_quality(quality_map_override=quality_map_override) audio_only = audio_only if audio_only is not None else settings.audio_only() - adaptive_live = settings.use_adaptive_live_streams() and context.inputstream_adaptive_capabilities('live') + adaptive_live = settings.use_isa_live_streams() and context.inputstream_adaptive_capabilities('live') if not ask_for_quality: stream_data_list = [item for item in stream_data_list diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index 4105b4fd0..3c86eda88 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -35,7 +35,7 @@ def __init__(self, *args, **kwargs): self._whitelist = addon.getSetting('kodion.http.ip.whitelist') self._httpd_port = int(addon.getSetting('kodion.mpd.proxy.port')) self._old_httpd_port = self._httpd_port - self._use_httpd = (addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.mpd') == 'true') or \ + self._use_httpd = (addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true') or \ (addon.getSetting('youtube.api.config.page') == 'true') self._httpd_address = addon.getSetting('kodion.http.listen') self._old_httpd_address = self._httpd_address diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 1539216f0..30ca5f284 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -494,8 +494,11 @@ def update_play_info(provider, context, video_id, video_item, video_stream, use_ if 'headers' in video_stream: video_item.set_headers(video_stream['headers']) - # set uses_mpd - video_item.set_use_mpd_video(settings.use_mpd_videos()) + # set _uses_isa + if video_item.live: + video_item.set_isa_video(settings.use_isa_live_streams()) + elif video_item.use_hls_video() or video_item.use_mpd_video(): + video_item.set_isa_video(settings.use_isa()) license_info = video_stream.get('license_info', {}) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 0c6fc0040..811fae6bc 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1514,7 +1514,7 @@ def _get_video_info(self): 'watchtime_url': '', } - httpd_is_live = (_settings.use_mpd() and + httpd_is_live = (_settings.use_isa() and is_httpd_live(port=_settings.httpd_port())) pa_li_info = streaming_data.get('licenseInfos', []) @@ -1557,7 +1557,7 @@ def _get_video_info(self): manifest_url = None if is_live: live_type = _settings.get_live_stream_type() - if live_type == 'ia_mpd': + if live_type == 'isa_mpd': manifest_url = streaming_data.get('dashManifestUrl', '') else: stream_list.extend(self._load_hls_manifest( @@ -1637,7 +1637,7 @@ def _get_video_info(self): def _process_stream_data(self, stream_data, default_lang_code='und'): _settings = self._context.get_settings() qualities = _settings.get_mpd_video_qualities() - ia_capabilities = self._context.inputstream_adaptive_capabilities() + isa_capabilities = self._context.inputstream_adaptive_capabilities() stream_features = _settings.stream_features() allow_hdr = 'hdr' in stream_features allow_hfr = 'hfr' in stream_features @@ -1704,7 +1704,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): codec = 'vp9' elif codec.startswith('dts'): codec = 'dts' - if codec not in stream_features or codec not in ia_capabilities: + if codec not in stream_features or codec not in isa_capabilities: continue media_type, container = mime_type.split('/') bitrate = stream.get('bitrate', 0) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 7ede578b6..15f27c65f 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1090,11 +1090,11 @@ def configure_addon(self, context, re_match): if switch == 'youtube': context.addon().openSettings() context.get_ui().refresh_container() - elif switch == 'mpd': + elif switch == 'isa': if context.use_inputstream_adaptive(): xbmcaddon.Addon(id='inputstream.adaptive').openSettings() else: - settings.set_bool('kodion.video.quality.mpd', False) + settings.set_bool('kodion.video.quality.isa', False) elif switch == 'subtitles': yt_language = context.get_settings().get_string('youtube.language', 'en-US') sub_setting = context.get_settings().subtitle_languages() diff --git a/resources/settings.xml b/resources/settings.xml index 70429df9f..2ef4169f6 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -200,9 +200,9 @@ - + - + 0 false @@ -212,15 +212,15 @@ - + 0 - RunPlugin(plugin://plugin.video.youtube/config/mpd/) + RunPlugin(plugin://plugin.video.youtube/config/isa/) true - true + true @@ -234,7 +234,7 @@ true - true + true @@ -312,37 +312,36 @@ - + 0 0 - - + + - true + true - + 0 0 - - false + false @@ -358,9 +357,9 @@ - true - 1 - 1 + true + 1 + 1 From ea485a5ae3aeb9b784c8a68e054548c3398b8ff9 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:28:52 +1100 Subject: [PATCH 016/141] Add preliminary support for iOS premium streams Can be enabled from Settings > Advanced > Use alternate client details > Alternate #1 - Partially fixes #505 --- .../youtube/helper/video_info.py | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 811fae6bc..f57a5cd2d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -533,6 +533,14 @@ class VideoInfo(object): 'title': 'ac-3@384', 'dash/audio': True, 'audio': {'bitrate': 384, 'encoding': 'ac-3'}}, + # === HLS + '9994': {'container': 'hls', + 'sort': [-1080, -1], + 'title': 'HLS', + 'hls/audio': True, + 'hls/video': True, + 'audio': {'bitrate': 0, 'encoding': 'aac'}, + 'video': {'height': 0, 'encoding': 'h.264'}}, # === Live HLS '9995': {'container': 'hls', 'Live': True, @@ -837,30 +845,30 @@ def __init__(self, context, access_token='', language='en-US'): self._selected_client = None client_selection = settings.client_selection() - # All client selections use the Android client as the first option to - # ensure that the age gate setting is enforced, regardless of login - # status + # Default client selection uses the Android or iOS client as the first + # option to ensure that the age gate setting is enforced, regardless of + # login status # Alternate #1 - # Will play most videos with subtitles at full resolution with HDR - # Some restricted videos may only play at 720p - # Some restricted videos require additional requests for subtitles + # Enable iOS client to access premium streams, however other stream + # types are limited if client_selection == 1: self._prioritised_clients = ( + 'ios', 'android', - 'android_embedded', 'android_youtube_tv', 'android_testsuite', + 'android_embedded', ) # Alternate #2 - # Will play most videos at full resolution with HDR - # Most videos wont show subtitles - # Useful for testing AV1 HDR + # Used to bypass age restriction, however streams are obfuscated and + # throttled. Useful for testing n-sig de-obfuscation. elif client_selection == 2: self._prioritised_clients = ( + 'smarttv_embedded', 'android', - 'android_testsuite', 'android_youtube_tv', + 'android_testsuite', 'android_embedded', ) # Default @@ -1105,14 +1113,15 @@ def _load_hls_manifest(self, url, live_type=None, meta_info=None, if playback_stats is None: playback_stats = {} - if live_type is None: - live_type = self._context.get_settings().get_live_stream_type() - - if 'hls' in live_type: - if live_type == 'hls': - yt_format = self.FORMAT['9995'] - else: - yt_format = self.FORMAT['9996'] + yt_format = None + if not live_type: + yt_format = self.FORMAT['9994'] + elif live_type == 'hls': + yt_format = self.FORMAT['9995'] + elif live_type == 'isa_hls': + yt_format = self.FORMAT['9996'] + + if yt_format: stream = {'url': url, 'meta': meta_info, 'headers': curl_headers, @@ -1571,6 +1580,10 @@ def _get_video_info(self): manifest_url, main_stream = self._generate_mpd_manifest( video_data, audio_data, license_info.get('url') ) + stream_list.extend(self._load_hls_manifest( + streaming_data.get('hlsManifestUrl'), + None, meta_info, client['headers'], playback_stats + )) if manifest_url: video_stream = { From 63162196b3b543803ff3c8a808adb6ef3f260383 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:07:05 +1100 Subject: [PATCH 017/141] Update defaults as ISA is now a required dependency --- resources/settings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/settings.xml b/resources/settings.xml index 2ef4169f6..fa8ad911f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -204,7 +204,7 @@ 0 - false + true System.HasAddon(inputstream.adaptive) @@ -295,7 +295,7 @@ 0 - 1 + 3 @@ -314,7 +314,7 @@ 0 - 0 + 2 From be03bd039bb9cac13d75312c63bc5c63aafacae4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:26:52 +1100 Subject: [PATCH 018/141] Move youtube requests to login_client - Use common method for all Youtube requests - Use common connection pool - Use as context manager - TODO migrate subtitle and video_info client requests --- .../resource.language.en_au/strings.po | 8 + .../resource.language.en_gb/strings.po | 8 + .../resource.language.en_nz/strings.po | 8 + .../resource.language.en_us/strings.po | 8 + .../kodion/constants/const_settings.py | 5 +- .../lib/youtube_plugin/kodion/exceptions.py | 11 +- .../kodion/impl/abstract_settings.py | 5 + .../youtube/client/login_client.py | 333 ++++++++++-------- .../youtube_plugin/youtube/client/youtube.py | 52 +-- resources/settings.xml | 28 +- 10 files changed, 271 insertions(+), 195 deletions(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index 287c9965c..7d72626af 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -1352,3 +1352,11 @@ msgstr "" msgctxt "#30763" msgid "Multi-audio" msgstr "" + +msgctxt "#30764" +msgid "Requests connect timeout" +msgstr "" + +msgctxt "#30765" +msgid "Requests read timeout" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index a78adb225..461237005 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1352,3 +1352,11 @@ msgstr "" msgctxt "#30763" msgid "Multi-audio" msgstr "" + +msgctxt "#30764" +msgid "Requests connect timeout" +msgstr "" + +msgctxt "#30765" +msgid "Requests read timeout" +msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index 7dc6ba76d..168654a38 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -1348,3 +1348,11 @@ msgstr "" msgctxt "#30763" msgid "Multi-audio" msgstr "" + +msgctxt "#30764" +msgid "Requests connect timeout" +msgstr "" + +msgctxt "#30765" +msgid "Requests read timeout" +msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index 574fb67d4..731014011 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -1353,3 +1353,11 @@ msgstr "" msgctxt "#30763" msgid "Multi-audio" msgstr "" + +msgctxt "#30764" +msgid "Requests connect timeout" +msgstr "" + +msgctxt "#30765" +msgid "Requests read timeout" +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 0ad5123bd..58ac464a6 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -19,7 +19,6 @@ SUBTITLE_LANGUAGE = 'kodion.subtitle.languages.num' # (int) SUBTITLE_DOWNLOAD = 'kodion.subtitle.download' # (bool) SETUP_WIZARD = 'kodion.setup_wizard' # (bool) -VERIFY_SSL = 'simple.requests.ssl.verify' # (bool) LOCATION = 'youtube.location' # (str) LOCATION_RADIUS = 'youtube.location.radius' # (int) PLAY_COUNT_MIN_PERCENT = 'kodion.play_count.percent' # (int) @@ -42,6 +41,10 @@ MPD_STREAM_FEATURES = 'kodion.mpd.stream.features' # (list[string]) MPD_STREAM_SELECT = 'kodion.mpd.stream.select' # (int) +VERIFY_SSL = 'requests.ssl.verify' # (bool) +CONNECT_TIMEOUT = 'requests.timeout.connect' # (int) +READ_TIMEOUT = 'requests.timeout.read' # (int) + HTTPD_PORT = 'kodion.mpd.proxy.port' # (number) HTTPD_LISTEN = 'kodion.http.listen' # (string) HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (string) diff --git a/resources/lib/youtube_plugin/kodion/exceptions.py b/resources/lib/youtube_plugin/kodion/exceptions.py index 7b4aa469b..543a28e65 100644 --- a/resources/lib/youtube_plugin/kodion/exceptions.py +++ b/resources/lib/youtube_plugin/kodion/exceptions.py @@ -10,9 +10,12 @@ class KodionException(Exception): - def __init__(self, message): - Exception.__init__(self, message) - self._message = message + def __init__(self, message, **kwargs): + super(KodionException, self).__init__(message) + attrs = self.__dict__ + for attr, value in kwargs.items(): + if attr not in attrs: + setattr(self, attr, value) def get_message(self): - return self._message + return str(self) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index bb1c2b437..2dd651f39 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -133,6 +133,11 @@ def verify_ssl(self): verify = False return verify + def get_timeout(self): + connect_timeout = self.get_int(SETTINGS.CONNECT_TIMEOUT, 9) + 0.5 + read_timout = self.get_int(SETTINGS.READ_TIMEOUT, 27) + return (connect_timeout, read_timout) + def allow_dev_keys(self): return self.get_bool(SETTINGS.ALLOW_DEV_KEYS, False) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index e9d595302..e9c68d4dd 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -8,20 +8,23 @@ See LICENSES/GPL-2.0-only for more information. """ +import atexit import time from urllib.parse import parse_qsl -import requests +from requests import Session +from requests.adapters import HTTPAdapter, Retry +from requests.exceptions import InvalidJSONError, RequestException -from ...youtube.youtube_exceptions import InvalidGrant, LoginException +from ...youtube.youtube_exceptions import (InvalidGrant, + LoginException, + YouTubeException,) from ...kodion import Context from .__config__ import (api, developer_keys, keys_changed, youtube_tv,) -context = Context(plugin_id='plugin.video.youtube') - class LoginClient(object): api_keys_changed = keys_changed @@ -42,10 +45,24 @@ class LoginClient(object): 'developer': developer_keys } + http_adapter = HTTPAdapter( + pool_maxsize=10, + pool_block=True, + max_retries=Retry( + total=3, + backoff_factor=1, + status_forcelist={500, 502, 503, 504}, + allowed_methods=None, + ) + ) + + context = Context(plugin_id='plugin.video.youtube') + def __init__(self, config=None, language='en-US', region='', access_token='', access_token_tv=''): self._config = self.CONFIGS['main'] if config is None else config self._config_tv = self.CONFIGS['youtube-tv'] - self._verify = context.get_settings().verify_ssl() + self._verify = self.context.get_settings().verify_ssl() + self._timeout = self.context.get_settings().get_timeout() # the default language is always en_US (like YouTube on the WEB) if not language: language = 'en_US' @@ -58,6 +75,115 @@ def __init__(self, config=None, language='en-US', region='', access_token='', ac self._access_token_tv = access_token_tv self._log_error_callback = None + self._session = Session() + self._session.verify = self._verify + self._session.mount('https://', self.http_adapter) + atexit.register(self._session.close) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._session.close() + + @staticmethod + def _login_json_hook(response): + json_data = None + try: + json_data = response.json() + if 'error' in json_data: + raise YouTubeException('"error" in response JSON data', + json_data=json_data) + except ValueError as error: + raise InvalidJSONError(error, response=response) + return response, json_data + + @staticmethod + def _login_error_hook(error, response): + json_data = getattr(error, 'json_data', None) + if not json_data: + return None, None, None, None, LoginException + if json_data['error'] == 'authorization_pending': + return None, None, None, False, False + if (json_data['error'] == 'invalid_grant' + and json_data.get('code') == '400'): + return None, None, json_data, False, InvalidGrant(json_data) + return None, None, json_data, False, LoginException(json_data) + + def _request(self, url, method='GET', + cookies=None, data=None, headers=None, json=None, params=None, + response_hook=None, error_hook=None, + error_title=None, error_info=None, raise_exc=False, **_): + response = None + try: + response = self._session.request(method, url, + verify=self._verify, + allow_redirects=True, + timeout=self._timeout, + cookies=cookies, + data=data, + headers=headers, + json=json, + params=params) + response.raise_for_status() + if response_hook: + response = response_hook(response) + + except (RequestException, YouTubeException) as exc: + from traceback import format_exc, format_stack + + response_text = exc.response and exc.response.text + stack_trace = format_stack() + exc_tb = format_exc() + + if error_hook: + error_response = error_hook(exc, response) + _title, _info, _response, _trace, _exc = error_response + if _title is not None: + error_title = _title + if _info is not None: + error_info = _info + if _response is not None: + response_text = _response + if _trace is not None: + stack_trace = _trace + if _exc is not None: + raise_exc = _exc + + if error_title is None: + error_title = 'Request failed' + + if error_info is None: + error_info = str(exc) + elif '{' in error_info: + try: + error_info = error_info.format(exc=exc) + except (AttributeError, IndexError, KeyError): + error_info = str(exc) + + if response_text: + response_text = 'Request response:\n{0}'.format(response_text) + + if stack_trace: + stack_trace = ( + 'Stack trace (most recent call last):\n{0}'.format( + ''.join(stack_trace) + ) + ) + + self.context.log_error('\n'.join([part for part in [ + error_title, error_info, response_text, stack_trace, exc_tb + ] if part])) + + if raise_exc: + if isinstance(raise_exc, BaseException): + raise raise_exc + if not callable(raise_exc): + raise YouTubeException(error_title) + raise raise_exc(error_title) + + return response + def set_log_error(self, callback): self._log_error_callback = callback @@ -84,24 +210,14 @@ def revoke(self, refresh_token): post_data = {'token': refresh_token} - # url - url = 'https://accounts.google.com/o/oauth2/revoke' - - result = requests.post(url, data=post_data, headers=headers, verify=self._verify) - - try: - json_data = result.json() - if 'error' in json_data: - context.log_error('Revoke failed: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) - json_data.update({'code': str(result.status_code)}) - raise LoginException(json_data) - except ValueError: - json_data = None - - if result.status_code != requests.codes.ok: - response_dump = self._get_response_dump(result, json_data) - context.log_error('Revoke failed: Code: |%s| Response dump: |%s|' % (str(result.status_code), response_dump)) - raise LoginException('Logout Failed') + self._request('https://accounts.google.com/o/oauth2/revoke', + method='POST', data=post_data, headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Logout Failed', + error_info='Revoke failed: {exc}', + raise_exc=LoginException + ) def refresh_token_tv(self, refresh_token): client_id = str(self.CONFIGS['youtube-tv']['id']) @@ -116,45 +232,32 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): client_id = client_id or self._config['id'] client_secret = client_secret or self._config['secret'] - post_data = {'client_id': client_id, 'client_secret': client_secret, 'refresh_token': refresh_token, 'grant_type': 'refresh_token'} - # url - url = 'https://www.googleapis.com/oauth2/v4/token' - config_type = self._get_config_type(client_id, client_secret) - context.log_debug('Refresh token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s|' % - (config_type, client_id[:5], client_secret[:5])) + client_summary = ''.join([ + '(config_type: |', config_type, '|', + ' client_id: |', client_id[:5], '...|', + ' client_secret: |', client_secret[:5], '...|)' + ]) + self.context.log_debug('Refresh token for ' + client_summary) + + result, json_data = self._request('https://www.googleapis.com/oauth2/v4/token', + method='POST', data=post_data, headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Login Failed', + error_info='Refresh failed for ' + client_summary + ': {exc}', + raise_exc=LoginException + ) - result = requests.post(url, data=post_data, headers=headers, verify=self._verify) - - try: - json_data = result.json() - if 'error' in json_data: - context.log_error('Refresh Failed: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) - json_data.update({'code': str(result.status_code)}) - if json_data['error'] == 'invalid_grant' and json_data['code'] == '400': - raise InvalidGrant(json_data) - raise LoginException(json_data) - except ValueError: - json_data = None - - if result.status_code != requests.codes.ok: - response_dump = self._get_response_dump(result, json_data) - context.log_error('Refresh failed: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s| Code: |%s| Response dump |%s|' % - (config_type, client_id[:5], client_secret[:5], str(result.status_code), response_dump)) - raise LoginException('Login Failed') - - if result.headers.get('content-type', '').startswith('application/json'): - if not json_data: - json_data = result.json() + if json_data: access_token = json_data['access_token'] expires_in = time.time() + int(json_data.get('expires_in', 3600)) return access_token, expires_in - return '', '' def request_access_token_tv(self, code, client_id='', client_secret=''): @@ -170,50 +273,28 @@ def request_access_token(self, code, client_id='', client_secret=''): client_id = client_id or self._config['id'] client_secret = client_secret or self._config['secret'] - post_data = {'client_id': client_id, 'client_secret': client_secret, 'code': code, 'grant_type': 'http://oauth.net/grant_type/device/1.0'} - # url - url = 'https://www.googleapis.com/oauth2/v4/token' - config_type = self._get_config_type(client_id, client_secret) - context.log_debug('Requesting access token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s|' % - (config_type, client_id[:5], client_secret[:5])) - - result = requests.post(url, data=post_data, headers=headers, verify=self._verify) - - authorization_pending = False - try: - json_data = result.json() - if 'error' in json_data: - if json_data['error'] != u'authorization_pending': - context.log_error('Requesting access token: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) - json_data.update({'code': str(result.status_code)}) - raise LoginException(json_data) - else: - authorization_pending = True - except ValueError: - json_data = None - - if (result.status_code != requests.codes.ok) and not authorization_pending: - response_dump = self._get_response_dump(result, json_data) - context.log_error('Requesting access token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s| Code: |%s| Response dump |%s|' % - (config_type, client_id[:5], client_secret[:5], str(result.status_code), response_dump)) - raise LoginException('Login Failed: Code %s' % str(result.status_code)) - - if result.headers.get('content-type', '').startswith('application/json'): - if json_data: - return json_data - else: - return result.json() - else: - response_dump = self._get_response_dump(result, json_data) - context.log_error('Requesting access token: Config: |%s| Client id [:5]: |%s| Client secret [:5]: |%s| Code: |%s| Response dump |%s|' % - (config_type, client_id[:5], client_secret[:5], str(result.status_code), response_dump)) - raise LoginException('Login Failed: Unknown response') + client_summary = ''.join([ + '(config_type: |', config_type, '|', + ' client_id: |', client_id[:5], '...|', + ' client_secret: |', client_secret[:5], '...|)' + ]) + self.context.log_debug('Requesting access token for ' + client_summary) + + result, json_data = self._request('https://www.googleapis.com/oauth2/v4/token', + method='POST', data=post_data, headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Login Failed', + error_info='Access token request failed for ' + client_summary + ': {exc}', + raise_exc=LoginException('Login Failed: Unknown response') + ) + return json_data def request_device_and_user_code_tv(self): client_id = str(self.CONFIGS['youtube-tv']['id']) @@ -226,44 +307,25 @@ def request_device_and_user_code(self, client_id=''): 'Content-Type': 'application/x-www-form-urlencoded'} client_id = client_id or self._config['id'] - post_data = {'client_id': client_id, 'scope': 'https://www.googleapis.com/auth/youtube'} - # url - url = 'https://accounts.google.com/o/oauth2/device/code' - config_type = self._get_config_type(client_id) - context.log_debug('Requesting device and user code: Config: |%s| Client id [:5]: |%s|' % - (config_type, client_id[:5])) - - result = requests.post(url, data=post_data, headers=headers, verify=self._verify) - - try: - json_data = result.json() - if 'error' in json_data: - context.log_error('Requesting device and user code failed: Code: |%s| JSON: |%s|' % (str(result.status_code), json_data)) - json_data.update({'code': str(result.status_code)}) - raise LoginException(json_data) - except ValueError: - json_data = None - - if result.status_code != requests.codes.ok: - response_dump = self._get_response_dump(result, json_data) - context.log_error('Requesting device and user code failed: Config: |%s| Client id [:5]: |%s| Code: |%s| Response dump |%s|' % - (config_type, client_id[:5], str(result.status_code), response_dump)) - raise LoginException('Login Failed') - - if result.headers.get('content-type', '').startswith('application/json'): - if json_data: - return json_data - else: - return result.json() - else: - response_dump = self._get_response_dump(result, json_data) - context.log_error('Requesting access token: Config: |%s| Client id [:5]: |%s| Code: |%s| Response dump |%s|' % - (config_type, client_id[:5], str(result.status_code), response_dump)) - raise LoginException('Login Failed: Unknown response') + client_summary = ''.join([ + '(config_type: |', config_type, '|', + ' client_id: |', client_id[:5], '...|)', + ]) + self.context.log_debug('Requesting device and user code for ' + client_summary) + + result, json_data = self._request('https://accounts.google.com/o/oauth2/device/code', + method='POST', data=post_data, headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Login Failed', + error_info='Requesting device and user code failed for ' + client_summary + ': {exc}', + raise_exc=LoginException('Login Failed: Unknown response') + ) + return json_data def get_access_token(self): return self._access_token @@ -300,12 +362,11 @@ def authenticate(self, username, password): # 'callerSig': '24bb24c05e47e0aefa68a58a766179d9b613a600', 'Passwd': password.encode('utf-8')} - # url - url = 'https://android.clients.google.com/auth' - - result = requests.post(url, data=post_data, headers=headers, verify=self._verify) - if result.status_code != requests.codes.ok: - raise LoginException('Login Failed') + result = self._request('https://android.clients.google.com/auth', + method='POST', data=post_data, headers=headers, + error_title='Login Failed', + raise_exc=LoginException + ) lines = result.text.replace('\n', '&') params = dict(parse_qsl(lines)) @@ -331,15 +392,3 @@ def _get_config_type(self, client_id, client_secret=None): if using_conf_main: return 'YouTube-Kodi' return 'Unknown' - - @staticmethod - def _get_response_dump(response, json_data=None): - if json_data: - return json_data - try: - return response.json() - except ValueError: - try: - return response.text - except: - return 'None' diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 834d56daa..ee64358aa 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -12,13 +12,9 @@ import json import re import threading -import traceback import xml.etree.ElementTree as ET -import requests - from .login_client import LoginClient -from ..youtube_exceptions import YouTubeException from ..helper.video_info import VideoInfo from ...kodion import Context from ...kodion.utils import datetime_parser @@ -90,10 +86,8 @@ def update_watch_history(self, video_id, url): if self._access_token: params['access_token'] = self._access_token - try: - _ = requests.get(url, params=params, headers=headers, verify=self._verify, allow_redirects=True) - except: - _context.log_error('Failed to update watch history |%s|' % traceback.print_exc()) + self._request(url, params=params, headers=headers, + error_msg='Failed to update watch history') def get_video_streams(self, context, video_id): video_info = VideoInfo(context, access_token=self._access_token_tv, @@ -822,22 +816,12 @@ def _perform(_page_token, _offset, _result): 'Accept-Language': 'en-US,en;q=0.7,de;q=0.3' } - session = requests.Session() - session.headers = headers - session.verify = self._verify - adapter = requests.adapters.HTTPAdapter(pool_maxsize=5, pool_block=True) - session.mount("https://", adapter) responses = [] def fetch_xml(_url, _responses): - try: - _response = session.get(_url, timeout=(3.05, 27)) - _response.raise_for_status() - except requests.exceptions.RequestException as error: - _context.log_debug('Response: {0}'.format(error.response and error.response.text)) - _context.log_error('Failed |%s|' % traceback.print_exc()) - return - _responses.append(_response) + _response = self._request(_url, headers=headers) + if _response: + _responses.append(_response) threads = [] for channel_id in sub_channel_ids: @@ -851,7 +835,6 @@ def fetch_xml(_url, _responses): for thread in threads: thread.join(30) - session.close() for response in responses: if response: @@ -1045,31 +1028,6 @@ def _perform(_playlist_idx, _page_token, _offset, _result): return result - def _request(self, url, method='GET', - cookies=None, data=None, headers=None, json=None, params=None, - error_msg=None, raise_error=False, timeout=(3.05, 27), **_): - try: - result = requests.request(method, url, - verify=self._verify, - allow_redirects=True, - timeout=timeout, - cookies=cookies, - data=data, - headers=headers, - json=json, - params=params) - result.raise_for_status() - except requests.exceptions.RequestException as error: - response = error.response and error.response.text - _context.log_debug('Response: {0}'.format(response)) - _context.log_error('{0}\n{1}'.format( - error_msg or 'Request failed', traceback.format_exc() - )) - if raise_error: - raise YouTubeException(error_msg) - return None - return result - def perform_v3_request(self, method='GET', headers=None, path=None, post_data=None, params=None, no_login=False): diff --git a/resources/settings.xml b/resources/settings.xml index fa8ad911f..ae4d15438 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -623,11 +623,37 @@ - + 0 true + + 0 + 9 + + 3 + 3 + 120 + + + false + 14045 + + + + 0 + 27 + + 10 + 1 + 120 + + + false + 14045 + + 0 false From 98fc07df5b1620742bc2e92b4cc875faae40927d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:28:22 +1100 Subject: [PATCH 019/141] Use new client request context manager --- .../lib/youtube_plugin/youtube/provider.py | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 15f27c65f..34bdd8433 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -339,13 +339,6 @@ def get_client(self, context): if refresh_tokens: refresh_tokens = refresh_tokens.split('|') context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) - # create a new access_token - - if dev_keys: - client = YouTube(language=language, region=region, items_per_page=items_per_page, config=dev_keys) - else: - client = YouTube(language=language, region=region, items_per_page=items_per_page, config=youtube_config) - else: context.log_debug('Selecting YouTube config "%s"' % youtube_config['system']) @@ -363,19 +356,23 @@ def get_client(self, context): if refresh_tokens: refresh_tokens = refresh_tokens.split('|') context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) - # create a new access_token - client = YouTube(language=language, region=region, items_per_page=items_per_page, config=youtube_config) - if client: - if len(access_tokens) != 2 and len(refresh_tokens) == 2: - try: + client = YouTube(language=language, + region=region, + items_per_page=items_per_page, + config=dev_keys if dev_keys else youtube_config) - access_token_kodi, expires_in_kodi = client.refresh_token(refresh_tokens[1]) + with client: + if not refresh_tokens or not refresh_tokens[0]: + client.set_log_error(context.log_error) + self._client = client + # create new access tokens + elif len(access_tokens) != 2 and len(refresh_tokens) == 2: + try: + access_token_kodi, expires_in_kodi = client.refresh_token(refresh_tokens[1]) access_token_tv, expires_in_tv = client.refresh_token_tv(refresh_tokens[0]) - access_tokens = [access_token_tv, access_token_kodi] - access_token = '%s|%s' % (access_token_tv, access_token_kodi) expires_in = min(expires_in_tv, expires_in_kodi) if dev_id: @@ -407,15 +404,9 @@ def get_client(self, context): access_tokens = ['', ''] client.set_access_token(access_token=access_tokens[1]) client.set_access_token_tv(access_token_tv=access_tokens[0]) - self._client = client - self._client.set_log_error(context.log_error) - else: - self._client = YouTube(items_per_page=items_per_page, language=language, region=region, config=youtube_config) - self._client.set_log_error(context.log_error) - - # in debug log the login status - context.log_debug('User is not logged in') + client.set_log_error(context.log_error) + self._client = client return self._client def get_resource_manager(self, context): From b024e41d717a63c5478c21a92c4324e8ce85b8ca Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 31 Oct 2023 18:26:59 +1100 Subject: [PATCH 020/141] Ensure headers are added to cURL playback requests --- resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py index 7f715b1c2..a23ce255b 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py @@ -103,6 +103,8 @@ def to_play_item(context, play_item): list_item.setContentLookup(False) except: pass + if not alternative_player and headers and uri.startswith('http'): + play_item.set_uri('|'.join([uri, headers])) if not is_strm: if play_item.get_play_count() == 0: From 51e0b6addd098d0ef3e5a0312bfe8076cc20d3b0 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:32:09 +1100 Subject: [PATCH 021/141] Split LoginClient into base & client request class Replace client build and request methods from VideoInfo --- .../youtube_plugin/kodion/utils/requests.py | 120 +++++++ .../youtube/client/login_client.py | 158 ++------- .../youtube/client/request_client.py | 322 +++++++++++++++++ .../youtube_plugin/youtube/client/youtube.py | 22 +- .../youtube/helper/video_info.py | 334 +----------------- .../lib/youtube_plugin/youtube/provider.py | 3 +- 6 files changed, 505 insertions(+), 454 deletions(-) create mode 100644 resources/lib/youtube_plugin/kodion/utils/requests.py create mode 100644 resources/lib/youtube_plugin/youtube/client/request_client.py diff --git a/resources/lib/youtube_plugin/kodion/utils/requests.py b/resources/lib/youtube_plugin/kodion/utils/requests.py new file mode 100644 index 000000000..64594907e --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/utils/requests.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +import atexit + +from traceback import format_exc, format_stack + +from requests import Session +from requests.adapters import HTTPAdapter, Retry +from requests.exceptions import RequestException + + +class BaseRequestsClass(object): + http_adapter = HTTPAdapter( + pool_maxsize=10, + pool_block=True, + max_retries=Retry( + total=3, + backoff_factor=1, + status_forcelist={500, 502, 503, 504}, + allowed_methods=None, + ) + ) + + def __init__(self, context, exc_type=RequestException): + self._context = context + self._verify = self._context.get_settings().verify_ssl() + self._timeout = self._context.get_settings().get_timeout() + self._default_exc = exc_type + + self._session = Session() + self._session.verify = self._verify + self._session.mount('https://', self.http_adapter) + atexit.register(self._session.close) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._session.close() + + def request(self, url, method='GET', + cookies=None, data=None, headers=None, json=None, params=None, + response_hook=None, error_hook=None, + error_title=None, error_info=None, raise_exc=False, **_): + response = None + try: + response = self._session.request(method, url, + verify=self._verify, + allow_redirects=True, + timeout=self._timeout, + cookies=cookies, + data=data, + headers=headers, + json=json, + params=params) + if response_hook: + response = response_hook(response) + else: + response.raise_for_status() + + except (RequestException, self._default_exc) as exc: + response_text = exc.response and exc.response.text + stack_trace = format_stack() + exc_tb = format_exc() + + if error_hook: + error_response = error_hook(exc, response) + _title, _info, _response, _trace, _exc = error_response + if _title is not None: + error_title = _title + if _info is not None: + error_info = _info + if _response is not None: + response = _response + response_text = str(_response) + if _trace is not None: + stack_trace = _trace + if _exc is not None: + raise_exc = _exc + + if error_title is None: + error_title = 'Request failed' + + if error_info is None: + error_info = str(exc) + elif '{' in error_info: + try: + error_info = error_info.format(exc=exc) + except (AttributeError, IndexError, KeyError): + error_info = str(exc) + + if response_text: + response_text = 'Request response:\n{0}'.format(response_text) + + if stack_trace: + stack_trace = ( + 'Stack trace (most recent call last):\n{0}'.format( + ''.join(stack_trace) + ) + ) + + self._context.log_error('\n'.join([part for part in [ + error_title, error_info, response_text, stack_trace, exc_tb + ] if part])) + + if raise_exc: + if isinstance(raise_exc, BaseException): + raise raise_exc from exc + if not callable(raise_exc): + raise self._default_exc(error_title) from exc + raise raise_exc(error_title) from exc + + return response diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index e9c68d4dd..0524027e9 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -8,25 +8,26 @@ See LICENSES/GPL-2.0-only for more information. """ -import atexit import time from urllib.parse import parse_qsl -from requests import Session -from requests.adapters import HTTPAdapter, Retry -from requests.exceptions import InvalidJSONError, RequestException +from requests.exceptions import InvalidJSONError -from ...youtube.youtube_exceptions import (InvalidGrant, - LoginException, - YouTubeException,) -from ...kodion import Context -from .__config__ import (api, - developer_keys, - keys_changed, - youtube_tv,) +from .request_client import YouTubeRequestClient +from ...youtube.youtube_exceptions import ( + InvalidGrant, + LoginException, + YouTubeException, +) +from .__config__ import ( + api, + developer_keys, + keys_changed, + youtube_tv, +) -class LoginClient(object): +class LoginClient(YouTubeRequestClient): api_keys_changed = keys_changed CONFIGS = { @@ -45,46 +46,25 @@ class LoginClient(object): 'developer': developer_keys } - http_adapter = HTTPAdapter( - pool_maxsize=10, - pool_block=True, - max_retries=Retry( - total=3, - backoff_factor=1, - status_forcelist={500, 502, 503, 504}, - allowed_methods=None, - ) - ) - - context = Context(plugin_id='plugin.video.youtube') + def __init__(self, context, config=None, language='en-US', region='', + access_token='', access_token_tv=''): + self._context = context - def __init__(self, config=None, language='en-US', region='', access_token='', access_token_tv=''): self._config = self.CONFIGS['main'] if config is None else config self._config_tv = self.CONFIGS['youtube-tv'] - self._verify = self.context.get_settings().verify_ssl() - self._timeout = self.context.get_settings().get_timeout() # the default language is always en_US (like YouTube on the WEB) if not language: language = 'en_US' - language = language.replace('-', '_') - self._language = language self._region = region + self._access_token = access_token self._access_token_tv = access_token_tv - self._log_error_callback = None - self._session = Session() - self._session.verify = self._verify - self._session.mount('https://', self.http_adapter) - atexit.register(self._session.close) - - def __enter__(self): - return self + self._log_error_callback = None - def __exit__(self, exc_type, exc_value, traceback): - self._session.close() + super(LoginClient, self).__init__(context=context) @staticmethod def _login_json_hook(response): @@ -93,10 +73,12 @@ def _login_json_hook(response): json_data = response.json() if 'error' in json_data: raise YouTubeException('"error" in response JSON data', - json_data=json_data) + json_data=json_data, + response=response,) except ValueError as error: raise InvalidJSONError(error, response=response) - return response, json_data + response.raise_for_status() + return json_data @staticmethod def _login_error_hook(error, response): @@ -104,86 +86,12 @@ def _login_error_hook(error, response): if not json_data: return None, None, None, None, LoginException if json_data['error'] == 'authorization_pending': - return None, None, None, False, False + return None, None, json_data, False, False if (json_data['error'] == 'invalid_grant' and json_data.get('code') == '400'): return None, None, json_data, False, InvalidGrant(json_data) return None, None, json_data, False, LoginException(json_data) - def _request(self, url, method='GET', - cookies=None, data=None, headers=None, json=None, params=None, - response_hook=None, error_hook=None, - error_title=None, error_info=None, raise_exc=False, **_): - response = None - try: - response = self._session.request(method, url, - verify=self._verify, - allow_redirects=True, - timeout=self._timeout, - cookies=cookies, - data=data, - headers=headers, - json=json, - params=params) - response.raise_for_status() - if response_hook: - response = response_hook(response) - - except (RequestException, YouTubeException) as exc: - from traceback import format_exc, format_stack - - response_text = exc.response and exc.response.text - stack_trace = format_stack() - exc_tb = format_exc() - - if error_hook: - error_response = error_hook(exc, response) - _title, _info, _response, _trace, _exc = error_response - if _title is not None: - error_title = _title - if _info is not None: - error_info = _info - if _response is not None: - response_text = _response - if _trace is not None: - stack_trace = _trace - if _exc is not None: - raise_exc = _exc - - if error_title is None: - error_title = 'Request failed' - - if error_info is None: - error_info = str(exc) - elif '{' in error_info: - try: - error_info = error_info.format(exc=exc) - except (AttributeError, IndexError, KeyError): - error_info = str(exc) - - if response_text: - response_text = 'Request response:\n{0}'.format(response_text) - - if stack_trace: - stack_trace = ( - 'Stack trace (most recent call last):\n{0}'.format( - ''.join(stack_trace) - ) - ) - - self.context.log_error('\n'.join([part for part in [ - error_title, error_info, response_text, stack_trace, exc_tb - ] if part])) - - if raise_exc: - if isinstance(raise_exc, BaseException): - raise raise_exc - if not callable(raise_exc): - raise YouTubeException(error_title) - raise raise_exc(error_title) - - return response - def set_log_error(self, callback): self._log_error_callback = callback @@ -210,7 +118,7 @@ def revoke(self, refresh_token): post_data = {'token': refresh_token} - self._request('https://accounts.google.com/o/oauth2/revoke', + self.request('https://accounts.google.com/o/oauth2/revoke', method='POST', data=post_data, headers=headers, response_hook=self._login_json_hook, error_hook=self._login_error_hook, @@ -243,9 +151,9 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): ' client_id: |', client_id[:5], '...|', ' client_secret: |', client_secret[:5], '...|)' ]) - self.context.log_debug('Refresh token for ' + client_summary) + self._context.log_debug('Refresh token for ' + client_summary) - result, json_data = self._request('https://www.googleapis.com/oauth2/v4/token', + json_data = self.request('https://www.googleapis.com/oauth2/v4/token', method='POST', data=post_data, headers=headers, response_hook=self._login_json_hook, error_hook=self._login_error_hook, @@ -284,9 +192,9 @@ def request_access_token(self, code, client_id='', client_secret=''): ' client_id: |', client_id[:5], '...|', ' client_secret: |', client_secret[:5], '...|)' ]) - self.context.log_debug('Requesting access token for ' + client_summary) + self._context.log_debug('Requesting access token for ' + client_summary) - result, json_data = self._request('https://www.googleapis.com/oauth2/v4/token', + json_data = self.request('https://www.googleapis.com/oauth2/v4/token', method='POST', data=post_data, headers=headers, response_hook=self._login_json_hook, error_hook=self._login_error_hook, @@ -315,9 +223,9 @@ def request_device_and_user_code(self, client_id=''): '(config_type: |', config_type, '|', ' client_id: |', client_id[:5], '...|)', ]) - self.context.log_debug('Requesting device and user code for ' + client_summary) + self._context.log_debug('Requesting device and user code for ' + client_summary) - result, json_data = self._request('https://accounts.google.com/o/oauth2/device/code', + json_data = self.request('https://accounts.google.com/o/oauth2/device/code', method='POST', data=post_data, headers=headers, response_hook=self._login_json_hook, error_hook=self._login_error_hook, @@ -362,7 +270,7 @@ def authenticate(self, username, password): # 'callerSig': '24bb24c05e47e0aefa68a58a766179d9b613a600', 'Passwd': password.encode('utf-8')} - result = self._request('https://android.clients.google.com/auth', + result = self.request('https://android.clients.google.com/auth', method='POST', data=post_data, headers=headers, error_title='Login Failed', raise_exc=LoginException diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py new file mode 100644 index 000000000..7b2c482fd --- /dev/null +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from ...kodion.utils.requests import BaseRequestsClass +from ...youtube.youtube_exceptions import YouTubeException + + +class YouTubeRequestClient(BaseRequestsClass): + CLIENTS = { + # 4k no VP9 HDR + # Limited subtitle availability + 'android_testsuite': { + '_id': 30, + '_query_subtitles': True, + 'json': { + 'params': '2AMBCgIQBg', + 'context': { + 'client': { + 'clientName': 'ANDROID_TESTSUITE', + 'clientVersion': '1.9', + 'androidSdkVersion': '29', + 'osName': 'Android', + 'osVersion': '10', + 'platform': 'MOBILE', + }, + }, + }, + 'headers': { + 'User-Agent': ('com.google.android.youtube/' + '{json[context][client][clientVersion]}' + ' (Linux; U; {json[context][client][osName]}' + ' {json[context][client][osVersion]};' + ' {json[context][client][gl]}) gzip'), + 'X-YouTube-Client-Name': '{_id}', + 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', + }, + 'params': { + 'key': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', + }, + }, + 'android': { + '_id': 3, + 'json': { + 'params': '2AMBCgIQBg', + 'context': { + 'client': { + 'clientName': 'ANDROID', + 'clientVersion': '17.31.35', + 'androidSdkVersion': '30', + 'osName': 'Android', + 'osVersion': '11', + 'platform': 'MOBILE', + }, + }, + }, + 'headers': { + 'User-Agent': ('com.google.android.youtube/' + '{json[context][client][clientVersion]}' + ' (Linux; U; {json[context][client][osName]}' + ' {json[context][client][osVersion]};' + ' {json[context][client][gl]}) gzip'), + 'X-YouTube-Client-Name': '{_id}', + 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', + }, + 'params': { + 'key': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', + }, + }, + # Only for videos that allow embedding + # Limited to 720p on some videos + 'android_embedded': { + '_id': 55, + 'json': { + 'params': '2AMBCgIQBg', + 'context': { + 'client': { + 'clientName': 'ANDROID_EMBEDDED_PLAYER', + 'clientVersion': '17.36.4', + 'clientScreen': 'EMBED', + 'androidSdkVersion': '29', + 'osName': 'Android', + 'osVersion': '10', + 'platform': 'MOBILE', + }, + }, + 'thirdParty': { + 'embedUrl': 'https://www.youtube.com/embed/{json[videoId]}', + }, + }, + 'headers': { + 'User-Agent': ('com.google.android.youtube/' + '{json[context][client][clientVersion]}' + ' (Linux; U; {json[context][client][osName]}' + ' {json[context][client][osVersion]};' + ' {json[context][client][gl]}) gzip'), + 'X-YouTube-Client-Name': '{_id}', + 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', + }, + 'params': { + 'key': 'AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw', + }, + }, + # 4k with HDR + # Some videos block this client, may also require embedding enabled + # Limited subtitle availability + 'android_youtube_tv': { + '_id': 29, + '_query_subtitles': True, + 'json': { + 'params': '2AMBCgIQBg', + 'context': { + 'client': { + 'clientName': 'ANDROID_UNPLUGGED', + 'clientVersion': '6.36', + 'androidSdkVersion': '29', + 'osName': 'Android', + 'osVersion': '10', + 'platform': 'MOBILE', + }, + }, + }, + 'headers': { + 'User-Agent': ('com.google.android.apps.youtube.unplugged/' + '{json[context][client][clientVersion]}' + ' (Linux; U; {json[context][client][osName]}' + ' {json[context][client][osVersion]};' + ' {json[context][client][gl]}) gzip'), + 'X-YouTube-Client-Name': '{_id}', + 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', + }, + 'params': { + 'key': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', + }, + }, + 'ios': { + '_id': 5, + 'json': { + 'context': { + 'client': { + 'clientName': 'IOS', + 'clientVersion': '17.33.2', + 'deviceModel': 'iPhone14,3', + 'osName': 'iOS', + 'osVersion': '15_6', + 'platform': 'MOBILE', + }, + }, + }, + 'headers': { + 'User-Agent': ('com.google.ios.youtube/' + '{json[context][client][clientVersion]}' + ' ({json[context][client][deviceModel]};' + ' U; CPU {json[context][client][osName]}' + ' {json[context][client][osVersion]}' + ' like Mac OS X)'), + 'X-YouTube-Client-Name': '{_id}', + 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', + }, + 'params': { + 'key': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc', + }, + }, + # Used to requests captions for clients that don't provide them + # Requires handling of nsig to overcome throttling (TODO) + 'smarttv_embedded': { + '_id': 85, + 'json': { + 'params': '2AMBCgIQBg', + 'context': { + 'client': { + 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', + 'clientScreen': 'WATCH', + 'clientVersion': '2.0', + }, + }, + 'thirdParty': { + 'embedUrl': 'https://www.youtube.com', + }, + }, + # Headers from a 2022 Samsung Tizen 6.5 based Smart TV + 'headers': { + 'User-Agent': ('Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' 85.0.4183.93/6.5 TV Safari/537.36'), + }, + 'params': { + 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', + }, + }, + # Used for misc api requests by default + # Requires handling of nsig to overcome throttling (TODO) + 'web': { + '_id': 1, + 'json': { + 'context': { + 'client': { + 'clientName': 'WEB', + 'clientVersion': '2.20220801.00.00', + }, + }, + }, + # Headers for a "Galaxy S20 Ultra" from Chrome dev tools device + # emulation + 'headers': { + 'User-Agent': ('Mozilla/5.0 (Linux; Android 10; SM-G981B)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/80.0.3987.162 Mobile Safari/537.36'), + 'Referer': 'https://www.youtube.com/watch?v={json[videoId]}' + }, + 'params': { + 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', + }, + }, + '_common': { + '_access_token': None, + 'json': { + 'contentCheckOk': True, + 'context': { + 'client': { + 'gl': None, + 'hl': None, + }, + }, + 'playbackContext': { + 'contentPlaybackContext': { + 'html5Preference': 'HTML5_PREF_WANTS', + }, + }, + 'racyCheckOk': True, + 'thirdParty': {}, + 'user': { + 'lockedSafetyMode': False + }, + 'videoId': None, + }, + 'headers': { + 'Origin': 'https://www.youtube.com', + 'Referer': 'https://www.youtube.com/watch?v={json[videoId]}', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.5', + 'Authorization': 'Bearer {_access_token}', + }, + 'params': { + 'key': None, + 'prettyPrint': 'false' + }, + }, + } + + def __init__(self, context): + super(YouTubeRequestClient, self).__init__( + context=context, exc_type=YouTubeException + ) + + @staticmethod + def json_traverse(json_data, path): + if not json_data or not path: + return None + + result = json_data + for keys in path: + is_dict = isinstance(result, dict) + if not is_dict and not isinstance(result, list): + return None + + if not isinstance(keys, (list, tuple)): + keys = [keys] + for key in keys: + if is_dict: + if key not in result: + continue + elif not isinstance(key, int) or len(result) <= key: + continue + result = result[key] + break + else: + return None + + if result == json_data: + return None + return result + + def build_client(self, client_name, auth_header=False): + def _merge_dicts(item1, item2, _=Ellipsis): + if not isinstance(item1, dict) or not isinstance(item2, dict): + return item1 if item2 is _ else item2 + new = {} + keys = set(item1) + keys.update(item2) + for key in keys: + value = _merge_dicts(item1.get(key, _), item2.get(key, _)) + if value is _: + continue + if isinstance(value, str) and '{' in value: + _format['{0}.{1}'.format(id(new), key)] = (new, key, value) + new[key] = value + return new or _ + _format = {} + + client = (self.CLIENTS.get(client_name) or self.CLIENTS['web']).copy() + client = _merge_dicts(self.CLIENTS['_common'], client) + + client['json']['videoId'] = self.video_id + if auth_header and self._access_token: + client['_access_token'] = self._access_token + client['params'] = None + elif 'Authorization' in client['headers']: + del client['headers']['Authorization'] + + for values, key, value in _format.values(): + if key in values: + values[key] = value.format(**client) + + return client diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index ee64358aa..03a959ad3 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -24,13 +24,13 @@ class YouTube(LoginClient): - def __init__(self, config=None, language='en-US', region='US', items_per_page=50, access_token='', access_token_tv=''): - if config is None: - config = {} - LoginClient.__init__(self, config=config, language=language, region=region, access_token=access_token, - access_token_tv=access_token_tv) + def __init__(self, **kwargs): + if not kwargs.get('config'): + kwargs['config'] = {} + if 'items_per_page' in kwargs: + self._max_results = kwargs.pop('items_per_page') - self._max_results = items_per_page + super(YouTube, self).__init__(**kwargs) def get_max_results(self): return self._max_results @@ -86,8 +86,8 @@ def update_watch_history(self, video_id, url): if self._access_token: params['access_token'] = self._access_token - self._request(url, params=params, headers=headers, - error_msg='Failed to update watch history') + self.request(url, params=params, headers=headers, + error_msg='Failed to update watch history') def get_video_streams(self, context, video_id): video_info = VideoInfo(context, access_token=self._access_token_tv, @@ -819,7 +819,7 @@ def _perform(_page_token, _offset, _result): responses = [] def fetch_xml(_url, _responses): - _response = self._request(_url, headers=headers) + _response = self.request(_url, headers=headers) if _response: _responses.append(_response) @@ -1059,7 +1059,7 @@ def perform_v3_request(self, method='GET', headers=None, path=None, log_params = None _context.log_debug('[data] v3 request: |{0}| path: |{1}| params: |{2}| post_data: |{3}|'.format(method, path, log_params, post_data)) - result = self._request(_url, method=method, headers=_headers, json=post_data, params=_params) + result = self.request(_url, method=method, headers=_headers, json=post_data, params=_params) if result is None: return {} @@ -1111,7 +1111,7 @@ def perform_v1_tv_request(self, method='GET', headers=None, path=None, log_params = None _context.log_debug('[data] v1 request: |{0}| path: |{1}| params: |{2}| post_data: |{3}|'.format(method, path, log_params, post_data)) - result = self._request(_url, method=method, headers=_headers, json=post_data, params=_params) + result = self.request(_url, method=method, headers=_headers, json=post_data, params=_params) if result is None: return {} diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index f57a5cd2d..32f52eb54 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -24,9 +24,9 @@ urljoin, ) -import requests import xbmcvfs +from ..client.request_client import YouTubeRequestClient from ...kodion.utils import is_httpd_live, make_dirs, DataCache from ..youtube_exceptions import YouTubeException from .subtitles import Subtitles @@ -34,7 +34,7 @@ from .signature.cipher import Cipher -class VideoInfo(object): +class VideoInfo(YouTubeRequestClient): FORMAT = { # === Non-DASH === '5': {'container': 'flv', @@ -584,256 +584,12 @@ class VideoInfo(object): 'video': {'height': 0, 'encoding': ''}} } - CLIENTS = { - # 4k no VP9 HDR - # Limited subtitle availability - 'android_testsuite': { - '_id': 30, - '_query_subtitles': True, - 'json': { - 'params': '2AMBCgIQBg', - 'context': { - 'client': { - 'clientName': 'ANDROID_TESTSUITE', - 'clientVersion': '1.9', - 'androidSdkVersion': '29', - 'osName': 'Android', - 'osVersion': '10', - 'platform': 'MOBILE', - }, - }, - }, - 'headers': { - 'User-Agent': ('com.google.android.youtube/' - '{json[context][client][clientVersion]}' - ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), - 'X-YouTube-Client-Name': '{_id}', - 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', - }, - 'params': { - 'key': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', - }, - }, - 'android': { - '_id': 3, - 'json': { - 'params': '2AMBCgIQBg', - 'context': { - 'client': { - 'clientName': 'ANDROID', - 'clientVersion': '17.31.35', - 'androidSdkVersion': '30', - 'osName': 'Android', - 'osVersion': '11', - 'platform': 'MOBILE', - }, - }, - }, - 'headers': { - 'User-Agent': ('com.google.android.youtube/' - '{json[context][client][clientVersion]}' - ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), - 'X-YouTube-Client-Name': '{_id}', - 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', - }, - 'params': { - 'key': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', - }, - }, - # Only for videos that allow embedding - # Limited to 720p on some videos - 'android_embedded': { - '_id': 55, - 'json': { - 'params': '2AMBCgIQBg', - 'context': { - 'client': { - 'clientName': 'ANDROID_EMBEDDED_PLAYER', - 'clientVersion': '17.36.4', - 'clientScreen': 'EMBED', - 'androidSdkVersion': '29', - 'osName': 'Android', - 'osVersion': '10', - 'platform': 'MOBILE', - }, - }, - 'thirdParty': { - 'embedUrl': 'https://www.youtube.com/embed/{json[videoId]}', - }, - }, - 'headers': { - 'User-Agent': ('com.google.android.youtube/' - '{json[context][client][clientVersion]}' - ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), - 'X-YouTube-Client-Name': '{_id}', - 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', - }, - 'params': { - 'key': 'AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw', - }, - }, - # 4k with HDR - # Some videos block this client, may also require embedding enabled - # Limited subtitle availability - 'android_youtube_tv': { - '_id': 29, - '_query_subtitles': True, - 'json': { - 'params': '2AMBCgIQBg', - 'context': { - 'client': { - 'clientName': 'ANDROID_UNPLUGGED', - 'clientVersion': '6.36', - 'androidSdkVersion': '29', - 'osName': 'Android', - 'osVersion': '10', - 'platform': 'MOBILE', - }, - }, - }, - 'headers': { - 'User-Agent': ('com.google.android.apps.youtube.unplugged/' - '{json[context][client][clientVersion]}' - ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), - 'X-YouTube-Client-Name': '{_id}', - 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', - }, - 'params': { - 'key': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', - }, - }, - 'ios': { - '_id': 5, - 'json': { - 'context': { - 'client': { - 'clientName': 'IOS', - 'clientVersion': '17.33.2', - 'deviceModel': 'iPhone14,3', - 'osName': 'iOS', - 'osVersion': '15_6', - 'platform': 'MOBILE', - }, - }, - }, - 'headers': { - 'User-Agent': ('com.google.ios.youtube/' - '{json[context][client][clientVersion]}' - ' ({json[context][client][deviceModel]};' - ' U; CPU {json[context][client][osName]}' - ' {json[context][client][osVersion]}' - ' like Mac OS X)'), - 'X-YouTube-Client-Name': '{_id}', - 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', - }, - 'params': { - 'key': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc', - }, - }, - # Used to requests captions for clients that don't provide them - # Requires handling of nsig to overcome throttling (TODO) - 'smarttv_embedded': { - '_id': 85, - 'json': { - 'params': '2AMBCgIQBg', - 'context': { - 'client': { - 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', - 'clientScreen': 'WATCH', - 'clientVersion': '2.0', - }, - }, - 'thirdParty': { - 'embedUrl': 'https://www.youtube.com', - }, - }, - # Headers from a 2022 Samsung Tizen 6.5 based Smart TV - 'headers': { - 'User-Agent': ('Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5)' - ' AppleWebKit/537.36 (KHTML, like Gecko)' - ' 85.0.4183.93/6.5 TV Safari/537.36'), - }, - 'params': { - 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', - }, - }, - # Used for misc api requests by default - # Requires handling of nsig to overcome throttling (TODO) - 'web': { - '_id': 1, - 'json': { - 'context': { - 'client': { - 'clientName': 'WEB', - 'clientVersion': '2.20220801.00.00', - }, - }, - }, - # Headers for a "Galaxy S20 Ultra" from Chrome dev tools device - # emulation - 'headers': { - 'User-Agent': ('Mozilla/5.0 (Linux; Android 10; SM-G981B)' - ' AppleWebKit/537.36 (KHTML, like Gecko)' - ' Chrome/80.0.3987.162 Mobile Safari/537.36'), - 'Referer': 'https://www.youtube.com/watch?v={json[videoId]}' - }, - 'params': { - 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', - }, - }, - '_common': { - '_access_token': None, - 'json': { - 'contentCheckOk': True, - 'context': { - 'client': { - 'gl': None, - 'hl': None, - }, - }, - 'playbackContext': { - 'contentPlaybackContext': { - 'html5Preference': 'HTML5_PREF_WANTS', - }, - }, - 'racyCheckOk': True, - 'thirdParty': {}, - 'user': { - 'lockedSafetyMode': False - }, - 'videoId': None, - }, - 'headers': { - 'Origin': 'https://www.youtube.com', - 'Referer': 'https://www.youtube.com/watch?v={json[videoId]}', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', - 'Accept': '*/*', - 'Accept-Language': 'en-US,en;q=0.5', - 'Authorization': 'Bearer {_access_token}', - }, - 'params': { - 'key': None, - 'prettyPrint': 'false' - }, - }, - } - def __init__(self, context, access_token='', language='en-US'): settings = context.get_settings() self.video_id = None self._context = context self._data_cache = self._context.get_data_cache() - self._verify = settings.verify_ssl() self._language = (settings.get_string('youtube.language', language) .replace('-', '_')) self._language_base = self._language[0:2] @@ -887,6 +643,8 @@ def __init__(self, context, access_token='', language='en-US'): 'gl': settings.get_string('youtube.region', 'US'), } + super(VideoInfo, self).__init__(context=context) + @staticmethod def _generate_cpn(): # https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L1381 @@ -902,73 +660,15 @@ def load_stream_infos(self, video_id): self.video_id = video_id return self._get_video_info() - def _build_client(self, client_name, auth_header=False): - def _merge_dicts(item1, item2, _=Ellipsis): - if not isinstance(item1, dict) or not isinstance(item2, dict): - return item1 if item2 is _ else item2 - new = {} - keys = set(item1) - keys.update(item2) - for key in keys: - value = _merge_dicts(item1.get(key, _), item2.get(key, _)) - if value is _: - continue - if isinstance(value, str) and '{' in value: - _format['{0}.{1}'.format(id(new), key)] = (new, key, value) - new[key] = value - return new or _ - _format = {} - - client = (self.CLIENTS.get(client_name) or self.CLIENTS['web']).copy() - client = _merge_dicts(self.CLIENTS['_common'], client) - - client['json']['videoId'] = self.video_id - if auth_header and self._access_token: - client['_access_token'] = self._access_token - client['params'] = None - elif 'Authorization' in client['headers']: - del client['headers']['Authorization'] - - for values, key, value in _format.values(): - if key in values: - values[key] = value.format(**client) - - return client - - def _request(self, url, method='GET', - cookies=None, data=None, headers=None, json=None, params=None, - error_msg=None, raise_error=False, timeout=(3.05, 27), **_): - try: - result = requests.request(method, url, - verify=self._verify, - allow_redirects=True, - timeout=timeout, - cookies=cookies, - data=data, - headers=headers, - json=json, - params=params) - result.raise_for_status() - except requests.exceptions.RequestException as error: - response = error.response and error.response.text - self._context.log_debug('Response: {0}'.format(response)) - self._context.log_error('{0}\n{1}'.format( - error_msg or 'Request failed', traceback.format_exc() - )) - if raise_error: - raise YouTubeException(error_msg) - return None - return result - def _get_player_page(self, client='web', embed=False): - client = self._build_client(client) + client = self.build_client(client) if embed: url = 'https://www.youtube.com/embed/{0}'.format(self.video_id) else: url = 'https://www.youtube.com/watch?v={0}'.format(self.video_id) cookies = {'CONSENT': 'YES+cb.20210615-14-p0.en+FX+294'} - result = self._request( + result = self.request( url, cookies=cookies, headers=client['headers'], error_msg=('Failed to get player html for video_id: {0}' .format(self.video_id)) @@ -1045,8 +745,8 @@ def _get_player_js(self): if cached_js: return cached_js - client = self._build_client('web') - result = self._request( + client = self.build_client('web') + result = self.request( js_url, headers=client['headers'], error_msg=('Failed to get player js for video_id: {0}' .format(self.video_id)) @@ -1093,10 +793,10 @@ def _load_hls_manifest(self, url, live_type=None, meta_info=None, if 'Authorization' in headers: del headers['Authorization'] else: - headers = self._build_client('web')['headers'] + headers = self.build_client('web')['headers'] curl_headers = self._make_curl_headers(headers, cookies=None) - result = self._request( + result = self.request( url, headers=headers, error_msg=('Failed to get manifest for video_id: {0}' .format(self.video_id)) @@ -1120,7 +820,7 @@ def _load_hls_manifest(self, url, live_type=None, meta_info=None, yt_format = self.FORMAT['9995'] elif live_type == 'isa_hls': yt_format = self.FORMAT['9996'] - + if yt_format: stream = {'url': url, 'meta': meta_info, @@ -1161,7 +861,7 @@ def _create_stream_list(self, streams, meta_info=None, headers=None, playback_st if 'Authorization' in headers: del headers['Authorization'] else: - headers = self._build_client('web')['headers'] + headers = self.build_client('web')['headers'] curl_headers = self._make_curl_headers(headers, cookies=None) if meta_info is None: @@ -1341,9 +1041,9 @@ def _get_video_info(self): playability_status = status = reason = None for _ in range(2): for client_name in self._prioritised_clients: - client = self._build_client(client_name, auth_header) + client = self.build_client(client_name, auth_header) - result = self._request( + result = self.request( video_info_url, 'POST', error_msg=( 'Player response failed for video_id: {0},' @@ -1377,7 +1077,7 @@ def _get_video_info(self): # This is used to check for error like: # "The following content is not available on this app." # Text will vary depending on Accept-Language and client hl - # Youtube support url is checked instead + # YouTube support url is checked instead url = self._get_error_details( playability_status, details=( @@ -1444,11 +1144,11 @@ def _get_video_info(self): if captions: captions['headers'] = client['headers'] elif client.get('_query_subtitles'): - result = self._request( + result = self.request( video_info_url, 'POST', error_msg=('Caption request failed to get player response for' 'video_id: {0}'.format(self.video_id)), - **self._build_client('smarttv_embedded', True) + **self.build_client('smarttv_embedded', True) ) response = result.json() diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 34bdd8433..6af39ead8 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -357,7 +357,8 @@ def get_client(self, context): refresh_tokens = refresh_tokens.split('|') context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) - client = YouTube(language=language, + client = YouTube(context=context, + language=language, region=region, items_per_page=items_per_page, config=dev_keys if dev_keys else youtube_config) From e2c4b62d5c6cfb16ea01e2108f1a9c37b4f138fa Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 1 Nov 2023 21:29:11 +1100 Subject: [PATCH 022/141] Use super() rather than hardcoded parent class --- resources/lib/youtube_plugin/kodion/impl/abstract_settings.py | 2 +- resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py | 2 +- .../lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py | 2 +- resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py | 2 +- .../lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py | 2 +- .../youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py | 2 +- .../youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py | 2 +- .../kodion/impl/xbmc/xbmc_progress_dialog_bg.py | 2 +- resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py | 2 +- resources/lib/youtube_plugin/kodion/items/audio_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/directory_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/favorites_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/image_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/new_search_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/next_page_item.py | 2 +- .../lib/youtube_plugin/kodion/items/search_history_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/search_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/uri_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/video_item.py | 2 +- resources/lib/youtube_plugin/kodion/items/watch_later_item.py | 2 +- resources/lib/youtube_plugin/kodion/json_store/api_keys.py | 2 +- .../lib/youtube_plugin/kodion/json_store/login_tokens.py | 2 +- resources/lib/youtube_plugin/kodion/utils/data_cache.py | 2 +- resources/lib/youtube_plugin/kodion/utils/favorite_list.py | 2 +- resources/lib/youtube_plugin/kodion/utils/function_cache.py | 2 +- resources/lib/youtube_plugin/kodion/utils/http_server.py | 2 +- resources/lib/youtube_plugin/kodion/utils/playback_history.py | 2 +- resources/lib/youtube_plugin/kodion/utils/search_history.py | 2 +- resources/lib/youtube_plugin/kodion/utils/watch_later_list.py | 2 +- resources/lib/youtube_plugin/youtube/helper/url_resolver.py | 4 ++-- resources/lib/youtube_plugin/youtube/provider.py | 2 +- 31 files changed, 32 insertions(+), 32 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py index 2dd651f39..61e42fa56 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py @@ -16,7 +16,7 @@ class AbstractSettings(object): def __init__(self): - object.__init__(self) + super(AbstractSettings, self).__init__() def get_string(self, setting_id, default_value=None): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py index 3fc817fb1..3844277b4 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py @@ -35,7 +35,7 @@ class XbmcContext(AbstractContext): def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override=True): - AbstractContext.__init__(self, path, params, plugin_name, plugin_id) + super(XbmcContext, self).__init__(path, params, plugin_name, plugin_id) if plugin_id: self._addon = xbmcaddon.Addon(id=plugin_id) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py index 4f1e50aee..618fc241d 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py @@ -20,7 +20,7 @@ class XbmcContextUI(AbstractContextUI): def __init__(self, xbmc_addon, context): - AbstractContextUI.__init__(self) + super(XbmcContextUI, self).__init__() self._xbmc_addon = xbmc_addon diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py index 36268e494..4b44cfaa7 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py @@ -14,7 +14,7 @@ class XbmcPlayer(AbstractPlayer): def __init__(self, player_type, context): - AbstractPlayer.__init__(self) + super(XbmcPlayer, self).__init__() self._player_type = player_type if player_type == 'audio': diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py index b21130c86..e7c6edc12 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py @@ -17,7 +17,7 @@ class XbmcPlaylist(AbstractPlaylist): def __init__(self, playlist_type, context): - AbstractPlaylist.__init__(self) + super(XbmcPlaylist, self).__init__() self._context = context self._playlist = None diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py index 3970d9c00..6e5632690 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py @@ -13,7 +13,7 @@ class XbmcPluginSettings(AbstractSettings): def __init__(self, xbmc_addon): - AbstractSettings.__init__(self) + super(XbmcPluginSettings, self).__init__() self._xbmc_addon = xbmc_addon diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py index 5aa6535d4..a1f061b99 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py @@ -14,7 +14,7 @@ class XbmcProgressDialog(AbstractProgressDialog): def __init__(self, heading, text): - AbstractProgressDialog.__init__(self, 100) + super(XbmcProgressDialog, self).__init__(100) self._dialog = xbmcgui.DialogProgress() self._dialog.create(heading, text) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog_bg.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog_bg.py index be9b4d0ae..222cde7f7 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog_bg.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog_bg.py @@ -14,7 +14,7 @@ class XbmcProgressDialogBG(AbstractProgressDialog): def __init__(self, heading, text): - AbstractProgressDialog.__init__(self, 100) + super(XbmcProgressDialogBG, self).__init__(100) self._dialog = xbmcgui.DialogProgressBG() self._dialog.create(heading, text) diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py index 454ab1d11..0719e5de3 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py @@ -21,7 +21,7 @@ class XbmcRunner(AbstractProviderRunner): def __init__(self): - AbstractProviderRunner.__init__(self) + super(XbmcRunner, self).__init__() self.handle = None self.settings = None diff --git a/resources/lib/youtube_plugin/kodion/items/audio_item.py b/resources/lib/youtube_plugin/kodion/items/audio_item.py index 0d4b012ff..bd0c674e5 100644 --- a/resources/lib/youtube_plugin/kodion/items/audio_item.py +++ b/resources/lib/youtube_plugin/kodion/items/audio_item.py @@ -15,7 +15,7 @@ class AudioItem(BaseItem): def __init__(self, name, uri, image='', fanart=''): - BaseItem.__init__(self, name, uri, image, fanart) + super(AudioItem, self).__init__(name, uri, image, fanart) self._duration = None self._track_number = None self._year = None diff --git a/resources/lib/youtube_plugin/kodion/items/directory_item.py b/resources/lib/youtube_plugin/kodion/items/directory_item.py index 96072485b..14b1702f8 100644 --- a/resources/lib/youtube_plugin/kodion/items/directory_item.py +++ b/resources/lib/youtube_plugin/kodion/items/directory_item.py @@ -13,7 +13,7 @@ class DirectoryItem(BaseItem): def __init__(self, name, uri, image='', fanart=''): - BaseItem.__init__(self, name, uri, image, fanart) + super(DirectoryItem, self).__init__(name, uri, image, fanart) self._plot = self.get_name() self._is_action = False self._channel_subscription_id = None diff --git a/resources/lib/youtube_plugin/kodion/items/favorites_item.py b/resources/lib/youtube_plugin/kodion/items/favorites_item.py index 462c89d5c..acb939436 100644 --- a/resources/lib/youtube_plugin/kodion/items/favorites_item.py +++ b/resources/lib/youtube_plugin/kodion/items/favorites_item.py @@ -21,7 +21,7 @@ def __init__(self, context, alt_name=None, image=None, fanart=None): if image is None: image = context.create_resource_path('media/favorites.png') - DirectoryItem.__init__(self, name, context.create_uri([constants.paths.FAVORITES, 'list']), image=image) + super(FavoritesItem, self).__init__(name, context.create_uri([constants.paths.FAVORITES, 'list']), image=image) if fanart: self.set_fanart(fanart) else: diff --git a/resources/lib/youtube_plugin/kodion/items/image_item.py b/resources/lib/youtube_plugin/kodion/items/image_item.py index 3909a48d6..2a1217135 100644 --- a/resources/lib/youtube_plugin/kodion/items/image_item.py +++ b/resources/lib/youtube_plugin/kodion/items/image_item.py @@ -13,7 +13,7 @@ class ImageItem(BaseItem): def __init__(self, name, uri, image='', fanart=''): - BaseItem.__init__(self, name, uri, image, fanart) + super(ImageItem, self).__init__(name, uri, image, fanart) self._title = None def set_title(self, title): diff --git a/resources/lib/youtube_plugin/kodion/items/new_search_item.py b/resources/lib/youtube_plugin/kodion/items/new_search_item.py index 8661f300f..5499e598c 100644 --- a/resources/lib/youtube_plugin/kodion/items/new_search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/new_search_item.py @@ -31,7 +31,7 @@ def __init__(self, context, alt_name=None, image=None, fanart=None, incognito=Fa if location: item_params.update({'location': location}) - DirectoryItem.__init__(self, name, context.create_uri([constants.paths.SEARCH, 'input'], params=item_params), image=image) + super(NewSearchItem, self).__init__(name, context.create_uri([constants.paths.SEARCH, 'input'], params=item_params), image=image) if fanart: self.set_fanart(fanart) else: diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index 5b54b7868..4aa83a91b 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -21,7 +21,7 @@ def __init__(self, context, current_page=1, image=None, fanart=None): if name.find('%d') != -1: name %= current_page + 1 - DirectoryItem.__init__(self, name, context.create_uri(context.get_path(), new_params), image=image) + super(NextPageItem, self).__init__(name, context.create_uri(context.get_path(), new_params), image=image) if fanart: self.set_fanart(fanart) else: diff --git a/resources/lib/youtube_plugin/kodion/items/search_history_item.py b/resources/lib/youtube_plugin/kodion/items/search_history_item.py index 28242c436..10b8c7646 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_history_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_history_item.py @@ -21,7 +21,7 @@ def __init__(self, context, query, image=None, fanart=None, location=False): if location: params['location'] = location - DirectoryItem.__init__(self, query, context.create_uri([constants.paths.SEARCH, 'query'], params=params), image=image) + super(SearchHistoryItem, self).__init__(query, context.create_uri([constants.paths.SEARCH, 'query'], params=params), image=image) if fanart: self.set_fanart(fanart) else: diff --git a/resources/lib/youtube_plugin/kodion/items/search_item.py b/resources/lib/youtube_plugin/kodion/items/search_item.py index f04e3ed6e..51e292dd2 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_item.py @@ -23,7 +23,7 @@ def __init__(self, context, alt_name=None, image=None, fanart=None, location=Fal params = {'location': location} if location else {} - DirectoryItem.__init__(self, name, context.create_uri([constants.paths.SEARCH, 'list'], params=params), image=image) + super(SearchItem, self).__init__(name, context.create_uri([constants.paths.SEARCH, 'list'], params=params), image=image) if fanart: self.set_fanart(fanart) else: diff --git a/resources/lib/youtube_plugin/kodion/items/uri_item.py b/resources/lib/youtube_plugin/kodion/items/uri_item.py index 9ba74cc9b..a3a80b702 100644 --- a/resources/lib/youtube_plugin/kodion/items/uri_item.py +++ b/resources/lib/youtube_plugin/kodion/items/uri_item.py @@ -13,4 +13,4 @@ class UriItem(BaseItem): def __init__(self, uri): - BaseItem.__init__(self, name='', uri=uri) + super(UriItem, self).__init__(name='', uri=uri) diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 35a819be9..595bf90f6 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -20,7 +20,7 @@ class VideoItem(BaseItem): def __init__(self, name, uri, image='', fanart=''): - BaseItem.__init__(self, name, uri, image, fanart) + super(VideoItem, self).__init__(name, uri, image, fanart) self._genre = None self._aired = None self._aired_utc = None diff --git a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py index b896fdecb..0a816c277 100644 --- a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py +++ b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py @@ -21,7 +21,7 @@ def __init__(self, context, alt_name=None, image=None, fanart=None): if image is None: image = context.create_resource_path('media/watch_later.png') - DirectoryItem.__init__(self, name, context.create_uri([constants.paths.WATCH_LATER, 'list']), image=image) + super(WatchLaterItem, self).__init__(name, context.create_uri([constants.paths.WATCH_LATER, 'list']), image=image) if fanart: self.set_fanart(fanart) else: diff --git a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py index 0faf0aa37..e5a4b8533 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py +++ b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py @@ -12,7 +12,7 @@ class APIKeyStore(JSONStore): def __init__(self): - JSONStore.__init__(self, 'api_keys.json') + super(APIKeyStore, self).__init__('api_keys.json') def set_defaults(self): data = self.get_data() diff --git a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py index 58d586f9c..3f691e1ca 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py +++ b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py @@ -14,7 +14,7 @@ # noinspection PyTypeChecker class LoginTokenStore(JSONStore): def __init__(self): - JSONStore.__init__(self, 'access_manager.json') + super(LoginTokenStore, self).__init__('access_manager.json') def set_defaults(self): data = self.get_data() diff --git a/resources/lib/youtube_plugin/kodion/utils/data_cache.py b/resources/lib/youtube_plugin/kodion/utils/data_cache.py index abee69e65..2e6fd89ec 100644 --- a/resources/lib/youtube_plugin/kodion/utils/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/utils/data_cache.py @@ -27,7 +27,7 @@ class DataCache(Storage): def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 - Storage.__init__(self, filename, max_file_size_kb=max_file_size_kb) + super(DataCache, self).__init__(filename, max_file_size_kb=max_file_size_kb) def is_empty(self): return self._is_empty() diff --git a/resources/lib/youtube_plugin/kodion/utils/favorite_list.py b/resources/lib/youtube_plugin/kodion/utils/favorite_list.py index c75c2755c..a4031b918 100644 --- a/resources/lib/youtube_plugin/kodion/utils/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/utils/favorite_list.py @@ -14,7 +14,7 @@ class FavoriteList(Storage): def __init__(self, filename): - Storage.__init__(self, filename) + super(FavoriteList, self).__init__(filename) def clear(self): self._clear() diff --git a/resources/lib/youtube_plugin/kodion/utils/function_cache.py b/resources/lib/youtube_plugin/kodion/utils/function_cache.py index cdd4ed4e3..7218a8ba8 100644 --- a/resources/lib/youtube_plugin/kodion/utils/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/utils/function_cache.py @@ -23,7 +23,7 @@ class FunctionCache(Storage): def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 - Storage.__init__(self, filename, max_file_size_kb=max_file_size_kb) + super(FunctionCache, self).__init__(filename, max_file_size_kb=max_file_size_kb) self._enabled = True diff --git a/resources/lib/youtube_plugin/kodion/utils/http_server.py b/resources/lib/youtube_plugin/kodion/utils/http_server.py index 909ab0034..076255718 100644 --- a/resources/lib/youtube_plugin/kodion/utils/http_server.py +++ b/resources/lib/youtube_plugin/kodion/utils/http_server.py @@ -42,7 +42,7 @@ def __init__(self, request, client_address, server): self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id).decode('utf-8') except AttributeError: self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id) - BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, server) + super(YouTubeRequestHandler, self).__init__(request, client_address, server) def connection_allowed(self): client_ip = self.client_address[0] diff --git a/resources/lib/youtube_plugin/kodion/utils/playback_history.py b/resources/lib/youtube_plugin/kodion/utils/playback_history.py index 723f3b160..db54100bf 100644 --- a/resources/lib/youtube_plugin/kodion/utils/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/utils/playback_history.py @@ -16,7 +16,7 @@ class PlaybackHistory(Storage): def __init__(self, filename): - Storage.__init__(self, filename) + super(PlaybackHistory, self).__init__(filename) def is_empty(self): return self._is_empty() diff --git a/resources/lib/youtube_plugin/kodion/utils/search_history.py b/resources/lib/youtube_plugin/kodion/utils/search_history.py index 726fcc6a9..f94d7bfb0 100644 --- a/resources/lib/youtube_plugin/kodion/utils/search_history.py +++ b/resources/lib/youtube_plugin/kodion/utils/search_history.py @@ -16,7 +16,7 @@ class SearchHistory(Storage): def __init__(self, filename, max_items=10): - Storage.__init__(self, filename, max_item_count=max_items) + super(SearchHistory, self).__init__(filename, max_item_count=max_items) def is_empty(self): return self._is_empty() diff --git a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py b/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py index 38fc222cb..04e21cada 100644 --- a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py @@ -16,7 +16,7 @@ class WatchLaterList(Storage): def __init__(self, filename): - Storage.__init__(self, filename) + super(WatchLaterList, self).__init__(filename) def clear(self): self._clear() diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index 1b4ea4ff0..fe6750c34 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -35,7 +35,7 @@ class YouTubeResolver(AbstractResolver): RE_USER_NAME = re.compile(r'http(s)?://(www.)?youtube.com/(?P[a-zA-Z0-9]+)$') def __init__(self): - AbstractResolver.__init__(self) + super(YouTubeResolver, self).__init__() def supports_url(self, url, url_components): if url_components.hostname == 'www.youtube.com' or url_components.hostname == 'youtube.com': @@ -91,7 +91,7 @@ def _load_page(_url): class CommonResolver(AbstractResolver, list): def __init__(self): - AbstractResolver.__init__(self) + super(CommonResolver, self).__init__() def supports_url(self, url, url_components): return True diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 6af39ead8..d2a4c7a85 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -203,7 +203,7 @@ class Provider(kodion.AbstractProvider): } def __init__(self): - kodion.AbstractProvider.__init__(self) + super(Provider, self).__init__() self._resource_manager = None self._client = None From f131501b8e71baf5bd8cbbba0fb29962733f9d75 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 1 Nov 2023 21:40:12 +1100 Subject: [PATCH 023/141] Locator as sub-class of BaseRequestsClass --- resources/lib/youtube_plugin/kodion/utils/ip_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/ip_api.py b/resources/lib/youtube_plugin/kodion/utils/ip_api.py index f6873e253..6f5761d77 100644 --- a/resources/lib/youtube_plugin/kodion/utils/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/utils/ip_api.py @@ -7,22 +7,24 @@ See LICENSES/GPL-2.0-only for more information. """ -import requests +from .requests import BaseRequestsClass -class Locator(object): +class Locator(BaseRequestsClass): def __init__(self, context): self._base_url = 'http://ip-api.com' self._response = {} self._context = context + super(Locator, self).__init__(context=context) + def response(self): return self._response def locate_requester(self): request_url = '/'.join([self._base_url, 'json']) - response = requests.get(request_url) + response = self.request(request_url) self._response = response.json() def success(self): From 978312d69c6fcfa2632e939e2dd87ee85fac42d2 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:30:44 +1100 Subject: [PATCH 024/141] Re-org to prevent issues with circular imports - Move http_server, ip_api, and requests to kodion.network module from kodion.utils - Split kodion.impl to - kodion.context - kodion.player - kodion.plugin - kodion.settings - kodion.ui - Remove context parameter from BaseRequestsClass, requests now imports Settings directly and can be imported by other modules in kodion - Update BaseRequestsClass.request to use same params as parent class --- resources/lib/youtube_authentication.py | 2 +- .../lib/youtube_plugin/kodion/__init__.py | 2 +- .../youtube_plugin/kodion/context/__init__.py | 13 ++ .../{impl => context}/abstract_context.py | 0 .../{impl => context}/xbmc/xbmc_context.py | 9 +- .../youtube_plugin/kodion/impl/__init__.py | 17 --- .../youtube_plugin/kodion/network/__init__.py | 21 +++ .../kodion/{utils => network}/http_server.py | 61 ++++---- .../kodion/{utils => network}/ip_api.py | 16 +-- .../kodion/{utils => network}/requests.py | 57 +++++--- .../youtube_plugin/kodion/player/__init__.py | 14 ++ .../{impl => player}/abstract_player.py | 0 .../{impl => player}/abstract_playlist.py | 0 .../{impl => player}/xbmc/xbmc_player.py | 0 .../{impl => player}/xbmc/xbmc_playlist.py | 2 +- .../kodion/{impl/xbmc => plugin}/__init__.py | 8 +- .../abstract_provider_runner.py | 2 +- .../{impl => plugin}/xbmc/xbmc_runner.py | 5 +- resources/lib/youtube_plugin/kodion/runner.py | 6 +- .../lib/youtube_plugin/kodion/service.py | 5 +- .../kodion/settings/__init__.py | 13 ++ .../{impl => settings}/abstract_settings.py | 0 .../xbmc/xbmc_plugin_settings.py | 0 .../lib/youtube_plugin/kodion/ui/__init__.py | 13 ++ .../{impl => ui}/abstract_context_ui.py | 0 .../{impl => ui}/abstract_progress_dialog.py | 0 .../kodion/{impl => ui}/xbmc/info_labels.py | 0 .../{impl => ui}/xbmc/xbmc_context_ui.py | 2 +- .../kodion/{impl => ui}/xbmc/xbmc_items.py | 0 .../{impl => ui}/xbmc/xbmc_progress_dialog.py | 0 .../xbmc/xbmc_progress_dialog_bg.py | 0 .../youtube_plugin/kodion/utils/__init__.py | 30 +++- .../youtube_plugin/kodion/utils/monitor.py | 3 +- .../youtube/client/login_client.py | 25 ++-- .../youtube/client/request_client.py | 8 +- .../lib/youtube_plugin/youtube/helper/tv.py | 11 +- .../youtube_plugin/youtube/helper/utils.py | 14 +- .../youtube/helper/video_info.py | 5 +- .../youtube_plugin/youtube/helper/yt_play.py | 2 +- .../youtube/helper/yt_setup_wizard.py | 7 +- .../lib/youtube_plugin/youtube/provider.py | 130 +++++++++--------- resources/lib/youtube_registration.py | 2 +- resources/lib/youtube_requests.py | 2 +- resources/lib/youtube_resolver.py | 2 +- 44 files changed, 301 insertions(+), 208 deletions(-) create mode 100644 resources/lib/youtube_plugin/kodion/context/__init__.py rename resources/lib/youtube_plugin/kodion/{impl => context}/abstract_context.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => context}/xbmc/xbmc_context.py (98%) delete mode 100644 resources/lib/youtube_plugin/kodion/impl/__init__.py create mode 100644 resources/lib/youtube_plugin/kodion/network/__init__.py rename resources/lib/youtube_plugin/kodion/{utils => network}/http_server.py (90%) rename resources/lib/youtube_plugin/kodion/{utils => network}/ip_api.py (68%) rename resources/lib/youtube_plugin/kodion/{utils => network}/requests.py (64%) create mode 100644 resources/lib/youtube_plugin/kodion/player/__init__.py rename resources/lib/youtube_plugin/kodion/{impl => player}/abstract_player.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => player}/abstract_playlist.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => player}/xbmc/xbmc_player.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => player}/xbmc/xbmc_playlist.py (98%) rename resources/lib/youtube_plugin/kodion/{impl/xbmc => plugin}/__init__.py (50%) rename resources/lib/youtube_plugin/kodion/{impl => plugin}/abstract_provider_runner.py (89%) rename resources/lib/youtube_plugin/kodion/{impl => plugin}/xbmc/xbmc_runner.py (98%) create mode 100644 resources/lib/youtube_plugin/kodion/settings/__init__.py rename resources/lib/youtube_plugin/kodion/{impl => settings}/abstract_settings.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => settings}/xbmc/xbmc_plugin_settings.py (100%) create mode 100644 resources/lib/youtube_plugin/kodion/ui/__init__.py rename resources/lib/youtube_plugin/kodion/{impl => ui}/abstract_context_ui.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => ui}/abstract_progress_dialog.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => ui}/xbmc/info_labels.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => ui}/xbmc/xbmc_context_ui.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => ui}/xbmc/xbmc_items.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => ui}/xbmc/xbmc_progress_dialog.py (100%) rename resources/lib/youtube_plugin/kodion/{impl => ui}/xbmc/xbmc_progress_dialog_bg.py (100%) diff --git a/resources/lib/youtube_authentication.py b/resources/lib/youtube_authentication.py index e3f373f35..08abf56c9 100644 --- a/resources/lib/youtube_authentication.py +++ b/resources/lib/youtube_authentication.py @@ -8,7 +8,7 @@ """ from youtube_plugin.youtube.provider import Provider -from youtube_plugin.kodion.impl import Context +from youtube_plugin.kodion.context import Context from youtube_plugin.youtube.helper import yt_login # noinspection PyUnresolvedReferences diff --git a/resources/lib/youtube_plugin/kodion/__init__.py b/resources/lib/youtube_plugin/kodion/__init__.py index be1e324a8..869364a9b 100644 --- a/resources/lib/youtube_plugin/kodion/__init__.py +++ b/resources/lib/youtube_plugin/kodion/__init__.py @@ -18,7 +18,7 @@ from .abstract_provider import AbstractProvider # import specialized implementation into the kodion namespace -from .impl import Context +from .context import Context from . import logger diff --git a/resources/lib/youtube_plugin/kodion/context/__init__.py b/resources/lib/youtube_plugin/kodion/context/__init__.py new file mode 100644 index 000000000..9b2b1d889 --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/context/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from .xbmc.xbmc_context import XbmcContext as Context + + +__all__ = ('Context', ) \ No newline at end of file diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/abstract_context.py rename to resources/lib/youtube_plugin/kodion/context/abstract_context.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py similarity index 98% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py rename to resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 3844277b4..7f77c785a 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -21,12 +21,13 @@ import xbmcvfs from ..abstract_context import AbstractContext -from .xbmc_plugin_settings import XbmcPluginSettings -from .xbmc_context_ui import XbmcContextUI -from .xbmc_playlist import XbmcPlaylist -from .xbmc_player import XbmcPlayer +from ...player.xbmc.xbmc_playlist import XbmcPlaylist +from ...player.xbmc.xbmc_player import XbmcPlayer +from ...settings.xbmc.xbmc_plugin_settings import XbmcPluginSettings +from ...ui.xbmc.xbmc_context_ui import XbmcContextUI from ... import utils + try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: diff --git a/resources/lib/youtube_plugin/kodion/impl/__init__.py b/resources/lib/youtube_plugin/kodion/impl/__init__.py deleted file mode 100644 index ddb3e0b93..000000000 --- a/resources/lib/youtube_plugin/kodion/impl/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from .xbmc.xbmc_plugin_settings import XbmcPluginSettings as Settings -from .xbmc.xbmc_context import XbmcContext as Context -from .xbmc.xbmc_context_ui import XbmcContextUI as ContextUI -from .xbmc.xbmc_runner import XbmcRunner as Runner - - -__all__ = ['Settings', 'Context', 'ContextUI', 'Runner'] diff --git a/resources/lib/youtube_plugin/kodion/network/__init__.py b/resources/lib/youtube_plugin/kodion/network/__init__.py new file mode 100644 index 000000000..8e1c3dccf --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/network/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from .http_server import get_client_ip_address, get_http_server, is_httpd_live +from .ip_api import Locator +from .requests import BaseRequestsClass + + +__all__ = ( + 'get_client_ip_address', + 'get_http_server', + 'is_httpd_live', + 'BaseRequestsClass', + 'Locator', +) diff --git a/resources/lib/youtube_plugin/kodion/utils/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py similarity index 90% rename from resources/lib/youtube_plugin/kodion/utils/http_server.py rename to resources/lib/youtube_plugin/kodion/network/http_server.py index 076255718..591810d84 100644 --- a/resources/lib/youtube_plugin/kodion/utils/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -17,32 +17,36 @@ from urllib.parse import urlparse import xbmc -import xbmcaddon import xbmcgui import xbmcvfs +from xbmcaddon import Addon + +from ..logger import log_debug +from ..settings import Settings -from .. import logger try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: pass -class YouTubeRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): +_addon_id = 'plugin.video.youtube' +_settings = Settings(Addon(id=_addon_id)) + +class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def __init__(self, request, client_address, server): - self.addon_id = 'plugin.video.youtube' - addon = xbmcaddon.Addon(self.addon_id) + addon = Addon(_addon_id) whitelist_ips = addon.getSetting('kodion.http.ip.whitelist') whitelist_ips = ''.join(whitelist_ips.split()) self.whitelist_ips = whitelist_ips.split(',') self.local_ranges = ('10.', '172.16.', '192.168.', '127.0.0.1', 'localhost', '::1') self.chunk_size = 1024 * 64 try: - self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id).decode('utf-8') + self.base_path = xbmc.translatePath('special://temp/%s' % _addon_id).decode('utf-8') except AttributeError: - self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id) - super(YouTubeRequestHandler, self).__init__(request, client_address, server) + self.base_path = xbmc.translatePath('special://temp/%s' % _addon_id) + super(YouTubeProxyRequestHandler, self).__init__(request, client_address, server) def connection_allowed(self): client_ip = self.client_address[0] @@ -54,14 +58,14 @@ def connection_allowed(self): log_lines.append('Whitelisted: |%s|' % str(conn_allowed)) if not conn_allowed: - logger.log_debug('HTTPServer: Connection from |%s| not allowed' % client_ip) + log_debug('HTTPServer: Connection from |%s| not allowed' % client_ip) elif self.path != '/ping': - logger.log_debug(' '.join(log_lines)) + log_debug(' '.join(log_lines)) return conn_allowed # noinspection PyPep8Naming def do_GET(self): - addon = xbmcaddon.Addon('plugin.video.youtube') + addon = Addon('plugin.video.youtube') mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true' api_config_enabled = addon.getSetting('youtube.api.config.page') == 'true' @@ -77,14 +81,14 @@ def do_GET(self): self.wfile.write(client_json.encode('utf-8')) if stripped_path != '/ping': - logger.log_debug('HTTPServer: GET Request uri path |{proxy_path}|'.format(proxy_path=self.path)) + log_debug('HTTPServer: GET Request uri path |{proxy_path}|'.format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) elif mpd_proxy_enabled and self.path.endswith('.mpd'): file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) file_chunk = True - logger.log_debug('HTTPServer: Request file path |{file_path}|'.format(file_path=file_path.encode('utf-8'))) + log_debug('HTTPServer: Request file path |{file_path}|'.format(file_path=file_path.encode('utf-8'))) try: with open(file_path, 'rb') as f: self.send_response(200) @@ -108,7 +112,7 @@ def do_GET(self): for chunk in self.get_chunks(html): self.wfile.write(chunk) elif api_config_enabled and stripped_path.startswith('/api_submit'): - addon = xbmcaddon.Addon('plugin.video.youtube') + addon = Addon('plugin.video.youtube') i18n = addon.getLocalizedString xbmc.executebuiltin('Dialog.Close(addonsettings,true)') old_api_key = addon.getSetting('youtube.api.key') @@ -160,12 +164,12 @@ def do_GET(self): # noinspection PyPep8Naming def do_HEAD(self): - logger.log_debug('HTTPServer: HEAD Request uri path |{proxy_path}|'.format(proxy_path=self.path)) + log_debug('HTTPServer: HEAD Request uri path |{proxy_path}|'.format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) else: - addon = xbmcaddon.Addon('plugin.video.youtube') + addon = Addon('plugin.video.youtube') mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true' if mpd_proxy_enabled and self.path.endswith('.mpd'): file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) @@ -182,7 +186,7 @@ def do_HEAD(self): # noinspection PyPep8Naming def do_POST(self): - logger.log_debug('HTTPServer: Request uri path |{proxy_path}|'.format(proxy_path=self.path)) + log_debug('HTTPServer: Request uri path |{proxy_path}|'.format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) @@ -220,7 +224,7 @@ def do_POST(self): match = re.search(r'^Authorized-Format-Types:\s*(?P.+?)\r*$', response_header, re.MULTILINE) if match: authorized_types = match.group('authorized_types').split(',') - logger.log_debug('HTTPServer: Found authorized formats |{authorized_fmts}|'.format(authorized_fmts=authorized_types)) + log_debug('HTTPServer: Found authorized formats |{authorized_fmts}|'.format(authorized_fmts=authorized_types)) fmt_to_px = {'SD': (1280 * 528) - 1, 'HD720': 1280 * 720, 'HD': 7680 * 4320} if 'HD' in authorized_types: @@ -259,7 +263,7 @@ def get_chunks(self, data): @staticmethod def api_config_page(): - addon = xbmcaddon.Addon('plugin.video.youtube') + addon = Addon('plugin.video.youtube') i18n = addon.getLocalizedString api_key = addon.getSetting('youtube.api.key') api_id = addon.getSetting('youtube.api.id') @@ -273,7 +277,7 @@ def api_config_page(): @staticmethod def api_submit_page(updated_keys, enabled, footer): - addon = xbmcaddon.Addon('plugin.video.youtube') + addon = Addon('plugin.video.youtube') i18n = addon.getLocalizedString html = Pages().api_submit.get('html') css = Pages().api_submit.get('css') @@ -454,16 +458,15 @@ class Pages(object): def get_http_server(address=None, port=None): - addon_id = 'plugin.video.youtube' - addon = xbmcaddon.Addon(addon_id) + addon = Addon(_addon_id) address = address if address else addon.getSetting('kodion.http.listen') address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '0.0.0.0' port = int(port) if port else 50152 try: - server = BaseHTTPServer.HTTPServer((address, port), YouTubeRequestHandler) + server = BaseHTTPServer.HTTPServer((address, port), YouTubeProxyRequestHandler) return server except socket.error as e: - logger.log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|'.format(address=address, port=port, response=str(e))) + log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|'.format(address=address, port=port, response=str(e))) xbmcgui.Dialog().notification(addon.getAddonInfo('name'), str(e), addon.getAddonInfo('icon'), 5000, False) @@ -471,8 +474,7 @@ def get_http_server(address=None, port=None): def is_httpd_live(address=None, port=None): - addon_id = 'plugin.video.youtube' - addon = xbmcaddon.Addon(addon_id) + addon = Addon(_addon_id) address = address if address else addon.getSetting('kodion.http.listen') address = '127.0.0.1' if address == '0.0.0.0' else address address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '127.0.0.1' @@ -482,16 +484,15 @@ def is_httpd_live(address=None, port=None): response = requests.get(url) result = response.status_code == 204 if not result: - logger.log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response=response.status_code)) + log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response=response.status_code)) return result except: - logger.log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response='failed')) + log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response='failed')) return False def get_client_ip_address(address=None, port=None): - addon_id = 'plugin.video.youtube' - addon = xbmcaddon.Addon(addon_id) + addon = Addon(_addon_id) address = address if address else addon.getSetting('kodion.http.listen') address = '127.0.0.1' if address == '0.0.0.0' else address address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '127.0.0.1' diff --git a/resources/lib/youtube_plugin/kodion/utils/ip_api.py b/resources/lib/youtube_plugin/kodion/network/ip_api.py similarity index 68% rename from resources/lib/youtube_plugin/kodion/utils/ip_api.py rename to resources/lib/youtube_plugin/kodion/network/ip_api.py index 6f5761d77..79892c6fe 100644 --- a/resources/lib/youtube_plugin/kodion/utils/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/network/ip_api.py @@ -8,16 +8,16 @@ """ from .requests import BaseRequestsClass +from .. import logger class Locator(BaseRequestsClass): - def __init__(self, context): + def __init__(self): self._base_url = 'http://ip-api.com' self._response = {} - self._context = context - super(Locator, self).__init__(context=context) + super(Locator, self).__init__() def response(self): return self._response @@ -30,9 +30,9 @@ def locate_requester(self): def success(self): successful = self.response().get('status', 'fail') == 'success' if successful: - self._context.log_debug('Location request was successful') + logger.log_debug('Location request was successful') else: - self._context.log_error(self.response().get('message', 'Location request failed with no error message')) + logger.log_error(self.response().get('message', 'Location request failed with no error message')) return successful def coordinates(self): @@ -42,7 +42,7 @@ def coordinates(self): lat = self._response.get('lat') lon = self._response.get('lon') if lat is None or lon is None: - self._context.log_error('No coordinates returned') + logger.log_error('No coordinates returned') return None - self._context.log_debug('Coordinates found') - return lat, lon + logger.log_debug('Coordinates found') + return {'lat': lat, 'lon': lon} diff --git a/resources/lib/youtube_plugin/kodion/utils/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py similarity index 64% rename from resources/lib/youtube_plugin/kodion/utils/requests.py rename to resources/lib/youtube_plugin/kodion/network/requests.py index 64594907e..ad83e1f44 100644 --- a/resources/lib/youtube_plugin/kodion/utils/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -15,6 +15,14 @@ from requests.adapters import HTTPAdapter, Retry from requests.exceptions import RequestException +from ..logger import log_error +from ..settings import Settings + +from xbmcaddon import Addon + + +_settings = Settings(Addon(id='plugin.video.youtube')) + class BaseRequestsClass(object): http_adapter = HTTPAdapter( @@ -28,10 +36,9 @@ class BaseRequestsClass(object): ) ) - def __init__(self, context, exc_type=RequestException): - self._context = context - self._verify = self._context.get_settings().verify_ssl() - self._timeout = self._context.get_settings().get_timeout() + def __init__(self, exc_type=RequestException): + self._verify = _settings.verify_ssl() + self._timeout = _settings.get_timeout() self._default_exc = exc_type self._session = Session() @@ -46,20 +53,38 @@ def __exit__(self, exc_type, exc_value, traceback): self._session.close() def request(self, url, method='GET', - cookies=None, data=None, headers=None, json=None, params=None, + params=None, data=None, headers=None, cookies=None, files=None, + auth=None, timeout=None, allow_redirects=None, proxies=None, + hooks=None, stream=None, verify=None, cert=None, json=None, + # Custom event hook implementation + # See _login_json_hook and _login_error_hook in login_client.py + # for example usage response_hook=None, error_hook=None, error_title=None, error_info=None, raise_exc=False, **_): + if timeout is None: + timeout = self._timeout + if verify is None: + verify = self._verify + if allow_redirects is None: + allow_redirects = True + response = None try: response = self._session.request(method, url, - verify=self._verify, - allow_redirects=True, - timeout=self._timeout, - cookies=cookies, + params=params, data=data, headers=headers, - json=json, - params=params) + cookies=cookies, + files=files, + auth=auth, + timeout=timeout, + allow_redirects=allow_redirects, + proxies=proxies, + hooks=hooks, + stream=stream, + verify=verify, + cert=cert, + json=json,) if response_hook: response = response_hook(response) else: @@ -106,15 +131,15 @@ def request(self, url, method='GET', ) ) - self._context.log_error('\n'.join([part for part in [ + log_error('\n'.join([part for part in [ error_title, error_info, response_text, stack_trace, exc_tb ] if part])) if raise_exc: if isinstance(raise_exc, BaseException): - raise raise_exc from exc - if not callable(raise_exc): - raise self._default_exc(error_title) from exc - raise raise_exc(error_title) from exc + raise raise_exc(exc) + if callable(raise_exc): + raise raise_exc(error_title)(exc) + raise self._default_exc(error_title)(exc) return response diff --git a/resources/lib/youtube_plugin/kodion/player/__init__.py b/resources/lib/youtube_plugin/kodion/player/__init__.py new file mode 100644 index 000000000..705b491c8 --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/player/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from .xbmc.xbmc_player import XbmcPlayer as Player +from .xbmc.xbmc_playlist import XbmcPlaylist as Playlist + + +__all__ = ('Player', 'Playlist', ) \ No newline at end of file diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_player.py b/resources/lib/youtube_plugin/kodion/player/abstract_player.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/abstract_player.py rename to resources/lib/youtube_plugin/kodion/player/abstract_player.py diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_playlist.py b/resources/lib/youtube_plugin/kodion/player/abstract_playlist.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/abstract_playlist.py rename to resources/lib/youtube_plugin/kodion/player/abstract_playlist.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_player.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_player.py rename to resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_player.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py similarity index 98% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py rename to resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py index e7c6edc12..7eb8a5a1e 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py @@ -12,7 +12,7 @@ import xbmc from ..abstract_playlist import AbstractPlaylist -from . import xbmc_items +from ...ui.xbmc import xbmc_items class XbmcPlaylist(AbstractPlaylist): diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/__init__.py b/resources/lib/youtube_plugin/kodion/plugin/__init__.py similarity index 50% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/__init__.py rename to resources/lib/youtube_plugin/kodion/plugin/__init__.py index 86fe5e5c0..37fe21cbe 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/__init__.py +++ b/resources/lib/youtube_plugin/kodion/plugin/__init__.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- """ - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube + Copyright (C) 2023-present plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. """ -__all__ = [] +from .xbmc.xbmc_runner import XbmcRunner as Runner + + +__all__ = ('Runner', ) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_provider_runner.py b/resources/lib/youtube_plugin/kodion/plugin/abstract_provider_runner.py similarity index 89% rename from resources/lib/youtube_plugin/kodion/impl/abstract_provider_runner.py rename to resources/lib/youtube_plugin/kodion/plugin/abstract_provider_runner.py index 515cf4544..d1aed3959 100644 --- a/resources/lib/youtube_plugin/kodion/impl/abstract_provider_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/abstract_provider_runner.py @@ -13,5 +13,5 @@ class AbstractProviderRunner(object): def __init__(self): pass - def run(self, provider, context=None): + def run(self, provider, context): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py similarity index 98% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py rename to resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index 0719e5de3..b44a8fed8 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -15,8 +15,7 @@ from ...exceptions import KodionException from ...items import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem from ... import AbstractProvider -from . import info_labels -from . import xbmc_items +from ...ui.xbmc import info_labels, xbmc_items class XbmcRunner(AbstractProviderRunner): @@ -25,7 +24,7 @@ def __init__(self): self.handle = None self.settings = None - def run(self, provider, context=None): + def run(self, provider, context): self.handle = context.get_handle() diff --git a/resources/lib/youtube_plugin/kodion/runner.py b/resources/lib/youtube_plugin/kodion/runner.py index 32104852c..19067c112 100644 --- a/resources/lib/youtube_plugin/kodion/runner.py +++ b/resources/lib/youtube_plugin/kodion/runner.py @@ -11,10 +11,10 @@ import copy import timeit -from .impl import Runner -from .impl import Context - from . import debug +from .context import Context +from .plugin import Runner + __all__ = ['run'] diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index a1e3fefc0..163f1375c 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -11,10 +11,9 @@ from datetime import datetime import time -from .impl import Context +from .context import Context +from .utils import YouTubeMonitor, YouTubePlayer from ..youtube.provider import Provider -from .utils import YouTubeMonitor -from .utils import YouTubePlayer def strptime(stamp, stamp_fmt): diff --git a/resources/lib/youtube_plugin/kodion/settings/__init__.py b/resources/lib/youtube_plugin/kodion/settings/__init__.py new file mode 100644 index 000000000..61a1c1394 --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/settings/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from .xbmc.xbmc_plugin_settings import XbmcPluginSettings as Settings + + +__all__ = ('Settings', ) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/abstract_settings.py rename to resources/lib/youtube_plugin/kodion/settings/abstract_settings.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_plugin_settings.py rename to resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py diff --git a/resources/lib/youtube_plugin/kodion/ui/__init__.py b/resources/lib/youtube_plugin/kodion/ui/__init__.py new file mode 100644 index 000000000..20669bc59 --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/ui/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from .xbmc.xbmc_context_ui import XbmcContextUI as ContextUI + + +__all__ = ('ContextUI', ) diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/abstract_context_ui.py rename to resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py diff --git a/resources/lib/youtube_plugin/kodion/impl/abstract_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/abstract_progress_dialog.py rename to resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/info_labels.py rename to resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py rename to resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 618fc241d..b8cdb0185 100644 --- a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -11,9 +11,9 @@ import xbmc import xbmcgui -from ..abstract_context_ui import AbstractContextUI from .xbmc_progress_dialog import XbmcProgressDialog from .xbmc_progress_dialog_bg import XbmcProgressDialogBG +from ..abstract_context_ui import AbstractContextUI from ... import constants from ... import utils diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_items.py rename to resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog.py rename to resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py diff --git a/resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog_bg.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog_bg.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/impl/xbmc/xbmc_progress_dialog_bg.py rename to resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog_bg.py diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 5365ec8f6..f74045045 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -26,17 +26,33 @@ from .watch_later_list import WatchLaterList from .function_cache import FunctionCache from .access_manager import AccessManager -from .http_server import get_http_server, is_httpd_live, get_client_ip_address from .monitor import YouTubeMonitor from .player import YouTubePlayer from .playback_history import PlaybackHistory from .data_cache import DataCache from .system_version import SystemVersion -from . import ip_api -__all__ = ['SearchHistory', 'FavoriteList', 'WatchLaterList', 'FunctionCache', 'AccessManager', - 'strip_html_from_text', 'create_path', 'create_uri_path', 'find_best_fit', 'to_unicode', 'to_utf8', - 'datetime_parser', 'select_stream', 'get_http_server', 'is_httpd_live', 'YouTubeMonitor', - 'make_dirs', 'loose_version', 'ip_api', 'PlaybackHistory', 'DataCache', 'get_client_ip_address', - 'SystemVersion', 'find_video_id', 'YouTubePlayer'] +__all__ = [ + 'create_path', + 'create_uri_path', + 'datetime_parser', + 'find_best_fit', + 'find_video_id', + 'loose_version', + 'make_dirs', + 'select_stream', + 'strip_html_from_text', + 'to_unicode', + 'to_utf8', + 'AccessManager', + 'DataCache', + 'FavoriteList', + 'FunctionCache', + 'PlaybackHistory', + 'SearchHistory', + 'SystemVersion', + 'WatchLaterList', + 'YouTubeMonitor', + 'YouTubePlayer' +] diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index 3c86eda88..363109250 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -17,9 +17,10 @@ import xbmcaddon import xbmcvfs -from ..utils import get_http_server, is_httpd_live +from ..network import get_http_server, is_httpd_live from .. import logger + try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 0524027e9..f713e16ea 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -13,18 +13,19 @@ from requests.exceptions import InvalidJSONError -from .request_client import YouTubeRequestClient -from ...youtube.youtube_exceptions import ( - InvalidGrant, - LoginException, - YouTubeException, -) from .__config__ import ( api, developer_keys, keys_changed, youtube_tv, ) +from .request_client import YouTubeRequestClient +from ...kodion.logger import log_debug +from ...youtube.youtube_exceptions import ( + InvalidGrant, + LoginException, + YouTubeException, +) class LoginClient(YouTubeRequestClient): @@ -46,10 +47,8 @@ class LoginClient(YouTubeRequestClient): 'developer': developer_keys } - def __init__(self, context, config=None, language='en-US', region='', + def __init__(self, config=None, language='en-US', region='', access_token='', access_token_tv=''): - self._context = context - self._config = self.CONFIGS['main'] if config is None else config self._config_tv = self.CONFIGS['youtube-tv'] # the default language is always en_US (like YouTube on the WEB) @@ -64,7 +63,7 @@ def __init__(self, context, config=None, language='en-US', region='', self._log_error_callback = None - super(LoginClient, self).__init__(context=context) + super(LoginClient, self).__init__() @staticmethod def _login_json_hook(response): @@ -151,7 +150,7 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): ' client_id: |', client_id[:5], '...|', ' client_secret: |', client_secret[:5], '...|)' ]) - self._context.log_debug('Refresh token for ' + client_summary) + log_debug('Refresh token for ' + client_summary) json_data = self.request('https://www.googleapis.com/oauth2/v4/token', method='POST', data=post_data, headers=headers, @@ -192,7 +191,7 @@ def request_access_token(self, code, client_id='', client_secret=''): ' client_id: |', client_id[:5], '...|', ' client_secret: |', client_secret[:5], '...|)' ]) - self._context.log_debug('Requesting access token for ' + client_summary) + log_debug('Requesting access token for ' + client_summary) json_data = self.request('https://www.googleapis.com/oauth2/v4/token', method='POST', data=post_data, headers=headers, @@ -223,7 +222,7 @@ def request_device_and_user_code(self, client_id=''): '(config_type: |', config_type, '|', ' client_id: |', client_id[:5], '...|)', ]) - self._context.log_debug('Requesting device and user code for ' + client_summary) + log_debug('Requesting device and user code for ' + client_summary) json_data = self.request('https://accounts.google.com/o/oauth2/device/code', method='POST', data=post_data, headers=headers, diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 7b2c482fd..0bfb2771d 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -7,7 +7,7 @@ See LICENSES/GPL-2.0-only for more information. """ -from ...kodion.utils.requests import BaseRequestsClass +from ...kodion.network import BaseRequestsClass from ...youtube.youtube_exceptions import YouTubeException @@ -255,10 +255,8 @@ class YouTubeRequestClient(BaseRequestsClass): }, } - def __init__(self, context): - super(YouTubeRequestClient, self).__init__( - context=context, exc_type=YouTubeException - ) + def __init__(self): + super(YouTubeRequestClient, self).__init__(exc_type=YouTubeException) @staticmethod def json_traverse(json_data, path): diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index c9818f924..94a9795af 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -8,9 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ -from ... import kodion +from ...kodion.items import DirectoryItem, NextPageItem, VideoItem from ...youtube.helper import utils -from ...kodion.items.video_item import VideoItem def my_subscriptions_to_items(provider, context, json_data, do_filter=False): @@ -65,7 +64,7 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): new_context = context.clone(new_params=new_params) current_page = int(new_context.get_param('page', 1)) - next_page_item = kodion.items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) return result @@ -111,7 +110,7 @@ def tv_videos_to_items(provider, context, json_data): new_context = context.clone(new_params=new_params) current_page = int(new_context.get_param('page', 1)) - next_page_item = kodion.items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) return result @@ -140,7 +139,7 @@ def saved_playlists_to_items(provider, context, json_data): else: item_uri = context.create_uri(['playlist', playlist_id], item_params) - playlist_item = kodion.items.DirectoryItem(title, item_uri, image=image) + playlist_item = DirectoryItem(title, item_uri, image=image) playlist_item.set_fanart(provider.get_fanart(context)) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_item @@ -162,7 +161,7 @@ def saved_playlists_to_items(provider, context, json_data): new_context = context.clone(new_params=new_params) current_page = int(new_context.get_param('page', 1)) - next_page_item = kodion.items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) return result diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 30ca5f284..78392b4af 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -11,8 +11,8 @@ import re import time -from ... import kodion from ...kodion import utils +from ...kodion.items import DirectoryItem from ...youtube.helper import yt_context_menu try: @@ -44,8 +44,8 @@ def get_thumb_timestamp(minutes=15): def make_comment_item(context, provider, snippet, uri, total_replies=0): - author = '[B]{}[/B]'.format(kodion.utils.to_str(snippet['authorDisplayName'])) - body = kodion.utils.to_str(snippet['textOriginal']) + author = '[B]{}[/B]'.format(utils.to_str(snippet['authorDisplayName'])) + body = utils.to_str(snippet['textOriginal']) label_props = None plot_props = None @@ -86,7 +86,7 @@ def make_comment_item(context, provider, snippet, uri, total_replies=0): else: plot = '{author}{edited}[CR][CR]{body}'.format(author=author, edited=edited, body=body) - comment_item = kodion.items.DirectoryItem(label, uri) + comment_item = DirectoryItem(label, uri) comment_item.set_plot(plot) comment_item.set_date_from_datetime(utils.datetime_parser.parse(snippet['publishedAt'])) if not uri: @@ -346,7 +346,7 @@ def update_video_infos(provider, context, video_id_dict, # plot channel_name = snippet.get('channelTitle', '') - description = kodion.utils.strip_html_from_text(snippet['description']) + description = utils.strip_html_from_text(snippet['description']) if show_channel_name and channel_name: description = '%s[CR][CR]%s' % (ui.uppercase(ui.bold(channel_name)), description) video_item.set_studio(channel_name) @@ -438,7 +438,7 @@ def update_video_infos(provider, context, video_id_dict, # got to [CHANNEL], only if we are not directly in the channel provide a jump to the channel if (channel_id and channel_name and - kodion.utils.create_path('channel', channel_id) != context.get_path()): + utils.create_path('channel', channel_id) != context.get_path()): video_item.set_channel_id(channel_id) yt_context_menu.append_go_to_channel(context_menu, provider, context, channel_id, channel_name) @@ -581,7 +581,7 @@ def update_play_info(provider, context, video_id, video_item, video_stream, use_ # plot channel_name = snippet.get('channelTitle', '') - description = kodion.utils.strip_html_from_text(snippet['description']) + description = utils.strip_html_from_text(snippet['description']) if channel_name and settings.get_bool('youtube.view.description.show_channel_name', True): description = '%s[CR][CR]%s' % (ui.uppercase(ui.bold(channel_name)), description) video_item.set_studio(channel_name) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 32f52eb54..914c3ff21 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -27,7 +27,8 @@ import xbmcvfs from ..client.request_client import YouTubeRequestClient -from ...kodion.utils import is_httpd_live, make_dirs, DataCache +from ...kodion.network import is_httpd_live +from ...kodion.utils import make_dirs, DataCache from ..youtube_exceptions import YouTubeException from .subtitles import Subtitles from .ratebypass import ratebypass @@ -643,7 +644,7 @@ def __init__(self, context, access_token='', language='en-US'): 'gl': settings.get_string('youtube.region', 'US'), } - super(VideoInfo, self).__init__(context=context) + super(VideoInfo, self).__init__() @staticmethod def _generate_cpn(): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index e4e2ec096..eceed96c4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -18,7 +18,7 @@ from ... import kodion from ...kodion import constants from ...kodion.items import VideoItem -from ...kodion.impl.xbmc.xbmc_items import to_playback_item +from ...kodion.ui.xbmc.xbmc_items import to_playback_item from ...youtube.youtube_exceptions import YouTubeException from ...youtube.helper import utils, v3 diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 559cba4d2..c6fe1d609 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -8,7 +8,7 @@ See LICENSES/GPL-2.0-only for more information. """ -from ...kodion.utils import ip_api +from ...kodion.network import Locator DEFAULT_LANGUAGES = {'items': [{'snippet': {'name': 'Afrikaans', 'hl': 'af'}, 'id': 'af'}, {'snippet': {'name': 'Azerbaijani', 'hl': 'az'}, 'id': 'az'}, {'snippet': {'name': 'Indonesian', 'hl': 'id'}, 'id': 'id'}, {'snippet': {'name': 'Malay', 'hl': 'ms'}, 'id': 'ms'}, @@ -106,15 +106,14 @@ def _process_language(provider, context): def _process_geo_location(provider, context): - settings = context.get_settings() if not context.get_ui().on_yes_no_input(context.get_name(), context.localize(provider.LOCAL_MAP['youtube.perform.geolocation'])): return - locator = ip_api.Locator(context) + locator = Locator() locator.locate_requester() coordinates = locator.coordinates() if coordinates: - settings.set_location('{lat},{lon}'.format(lat=coordinates[0], lon=coordinates[1])) + context.get_settings().set_location('{0[lat]},{0[lon]}'.format(coordinates)) def process(provider, context): diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index d2a4c7a85..09d47d879 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -15,17 +15,6 @@ import socket from base64 import b64decode -from ..youtube.helper import yt_subscriptions -from .. import kodion -from ..kodion.utils import ( - find_video_id, - get_client_ip_address, - is_httpd_live, - strip_html_from_text, - FunctionCache, -) -from ..kodion.items import DirectoryItem -from ..youtube.client import YouTube from .helper import ( v3, yt_context_menu, @@ -41,6 +30,16 @@ UrlToItemConverter, ) from .youtube_exceptions import InvalidGrant, LoginException +from ..kodion import ( + constants, + AbstractProvider, + RegisterProviderPath, +) +from ..youtube.client import YouTube +from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem +from ..kodion.network import get_client_ip_address, is_httpd_live +from ..kodion.utils import find_video_id, strip_html_from_text, FunctionCache +from ..youtube.helper import yt_subscriptions import xbmc import xbmcaddon @@ -55,7 +54,7 @@ pass -class Provider(kodion.AbstractProvider): +class Provider(AbstractProvider): LOCAL_MAP = {'youtube.search': 30102, 'youtube.next_page': 30106, 'youtube.watch_later': 30107, @@ -357,8 +356,7 @@ def get_client(self, context): refresh_tokens = refresh_tokens.split('|') context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) - client = YouTube(context=context, - language=language, + client = YouTube(language=language, region=region, items_per_page=items_per_page, config=dev_keys if dev_keys else youtube_config) @@ -424,7 +422,7 @@ def get_fanart(context): return context.create_resource_path('media', 'fanart.jpg') # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/uri2addon/$') + @RegisterProviderPath('^/uri2addon/$') def on_uri2addon(self, context, re_match): uri = context.get_param('uri', '') if not uri: @@ -440,9 +438,9 @@ def on_uri2addon(self, context, re_match): return False - @kodion.RegisterProviderPath('^/playlist/(?P[^/]+)/$') + @RegisterProviderPath('^/playlist/(?P[^/]+)/$') def _on_playlist(self, context, re_match): - self.set_content_type(context, kodion.constants.content_type.VIDEOS) + self.set_content_type(context, constants.content_type.VIDEOS) result = [] @@ -464,9 +462,9 @@ def _on_playlist(self, context, re_match): playlist_id: """ - @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/playlist/(?P[^/]+)/$') + @RegisterProviderPath('^/channel/(?P[^/]+)/playlist/(?P[^/]+)/$') def _on_channel_playlist(self, context, re_match): - self.set_content_type(context, kodion.constants.content_type.VIDEOS) + self.set_content_type(context, constants.content_type.VIDEOS) client = self.get_client(context) result = [] @@ -487,9 +485,9 @@ def _on_channel_playlist(self, context, re_match): channel_id: """ - @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/playlists/$') + @RegisterProviderPath('^/channel/(?P[^/]+)/playlists/$') def _on_channel_playlists(self, context, re_match): - self.set_content_type(context, kodion.constants.content_type.FILES) + self.set_content_type(context, constants.content_type.FILES) result = [] channel_id = re_match.group('channel_id') @@ -528,9 +526,9 @@ def _on_channel_playlists(self, context, re_match): channel_id: """ - @kodion.RegisterProviderPath('^/channel/(?P[^/]+)/live/$') + @RegisterProviderPath('^/channel/(?P[^/]+)/live/$') def _on_channel_live(self, context, re_match): - self.set_content_type(context, kodion.constants.content_type.VIDEOS) + self.set_content_type(context, constants.content_type.VIDEOS) result = [] channel_id = re_match.group('channel_id') @@ -551,7 +549,7 @@ def _on_channel_live(self, context, re_match): channel_id: """ - @kodion.RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$') + @RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$') def _on_channel(self, context, re_match): listitem_channel_id = context.get_ui().get_info_label('Container.ListItem(0).Property(channel_id)') @@ -565,7 +563,7 @@ def _on_channel(self, context, re_match): if method == 'channel' and not channel_id: return False - self.set_content_type(context, kodion.constants.content_type.VIDEOS) + self.set_content_type(context, constants.content_type.VIDEOS) resource_manager = self.get_resource_manager(context) @@ -623,9 +621,9 @@ def _on_channel(self, context, re_match): search_live_id = mine_id if mine_id else channel_id if not hide_search: - search_item = kodion.items.NewSearchItem(context, alt_name=context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.search'])), - image=context.create_resource_path('media', 'search.png'), - fanart=self.get_fanart(context), channel_id=search_live_id, incognito=incognito, addon_id=addon_id) + search_item = NewSearchItem(context, alt_name=context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.search'])), + image=context.create_resource_path('media', 'search.png'), + fanart=self.get_fanart(context), channel_id=search_live_id, incognito=incognito, addon_id=addon_id) search_item.set_fanart(self.get_fanart(context)) result.append(search_item) @@ -651,15 +649,15 @@ def _on_channel(self, context, re_match): return result # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/location/mine/$') + @RegisterProviderPath('^/location/mine/$') def _on_my_location(self, context, re_match): - self.set_content_type(context, kodion.constants.content_type.FILES) + self.set_content_type(context, constants.content_type.FILES) settings = context.get_settings() result = [] # search - search_item = kodion.items.SearchItem(context, image=context.create_resource_path('media', 'search.png'), + search_item = SearchItem(context, image=context.create_resource_path('media', 'search.png'), fanart=self.get_fanart(context), location=True) result.append(search_item) @@ -702,7 +700,7 @@ def _on_my_location(self, context, re_match): """ # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/play/$') + @RegisterProviderPath('^/play/$') def on_play(self, context, re_match): listitem_path = context.get_ui().get_info_label('Container.ListItem(0).FileNameAndPath') @@ -779,25 +777,25 @@ def on_play(self, context, re_match): return yt_play.play_channel_live(self, context) return False - @kodion.RegisterProviderPath('^/video/(?P[^/]+)/$') + @RegisterProviderPath('^/video/(?P[^/]+)/$') def _on_video_x(self, context, re_match): method = re_match.group('method') return yt_video.process(method, self, context, re_match) - @kodion.RegisterProviderPath('^/playlist/(?P[^/]+)/(?P[^/]+)/$') + @RegisterProviderPath('^/playlist/(?P[^/]+)/(?P[^/]+)/$') def _on_playlist_x(self, context, re_match): method = re_match.group('method') category = re_match.group('category') return yt_playlist.process(method, category, self, context) - @kodion.RegisterProviderPath('^/subscriptions/(?P[^/]+)/$') + @RegisterProviderPath('^/subscriptions/(?P[^/]+)/$') def _on_subscriptions(self, context, re_match): method = re_match.group('method') resource_manager = self.get_resource_manager(context) subscriptions = yt_subscriptions.process(method, self, context) if method == 'list': - self.set_content_type(context, kodion.constants.content_type.FILES) + self.set_content_type(context, constants.content_type.FILES) channel_ids = [] for subscription in subscriptions: channel_ids.append(subscription.get_channel_id()) @@ -808,20 +806,20 @@ def _on_subscriptions(self, context, re_match): return subscriptions - @kodion.RegisterProviderPath('^/special/(?P[^/]+)/$') + @RegisterProviderPath('^/special/(?P[^/]+)/$') def _on_yt_specials(self, context, re_match): category = re_match.group('category') return yt_specials.process(category, self, context) # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/history/clear/$') + @RegisterProviderPath('^/history/clear/$') def _on_yt_clear_history(self, context, re_match): if context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.clear_history_confirmation'])): json_data = self.get_client(context).clear_watch_history() if 'error' not in json_data: context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) - @kodion.RegisterProviderPath('^/users/(?P[^/]+)/$') + @RegisterProviderPath('^/users/(?P[^/]+)/$') def _on_users(self, context, re_match): action = re_match.group('action') refresh = context.get_param('refresh', 'true').lower() == 'true' @@ -972,7 +970,7 @@ def switch_to_user(_user): return True - @kodion.RegisterProviderPath('^/sign/(?P[^/]+)/$') + @RegisterProviderPath('^/sign/(?P[^/]+)/$') def _on_sign(self, context, re_match): sign_out_confirmed = context.get_param('confirmed', '').lower() == 'true' mode = re_match.group('mode') @@ -989,7 +987,7 @@ def _on_sign(self, context, re_match): yt_login.process(mode, self, context) return False - @kodion.RegisterProviderPath('^/search/$') + @RegisterProviderPath('^/search/$') def endpoint_search(self, context, re_match): query = context.get_param('q', '') if not query: @@ -1031,9 +1029,9 @@ def on_search(self, search_text, context, re_match): context.set_param('q', search_text) if search_type == 'video': - self.set_content_type(context, kodion.constants.content_type.VIDEOS) + self.set_content_type(context, constants.content_type.VIDEOS) else: - self.set_content_type(context, kodion.constants.content_type.FILES) + self.set_content_type(context, constants.content_type.FILES) if page == 1 and search_type == 'video' and not event_type and not hide_folders: if not channel_id and not location: @@ -1075,7 +1073,7 @@ def on_search(self, search_text, context, re_match): result.extend(v3.response_to_items(self, context, json_data)) return result - @kodion.RegisterProviderPath('^/config/(?P[^/]+)/$') + @RegisterProviderPath('^/config/(?P[^/]+)/$') def configure_addon(self, context, re_match): switch = re_match.group('switch') settings = context.get_settings() @@ -1122,7 +1120,7 @@ def configure_addon(self, context, re_match): return False # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/my_subscriptions/filter/$') + @RegisterProviderPath('^/my_subscriptions/filter/$') def manage_my_subscription_filter(self, context, re_match): params = context.get_params() action = params.get('action') @@ -1160,7 +1158,7 @@ def manage_my_subscription_filter(self, context, re_match): context.get_ui().show_notification(message=message) context.get_ui().refresh_container() - @kodion.RegisterProviderPath('^/maintain/(?P[^/]+)/(?P[^/]+)/$') + @RegisterProviderPath('^/maintain/(?P[^/]+)/(?P[^/]+)/$') def maintenance_actions(self, context, re_match): maint_type = re_match.group('maint_type') action = re_match.group('action') @@ -1251,7 +1249,7 @@ def maintenance_actions(self, context, re_match): context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.requires.krypton'])) # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/api/update/$') + @RegisterProviderPath('^/api/update/$') def api_key_update(self, context, re_match): settings = context.get_settings() params = context.get_params() @@ -1301,7 +1299,7 @@ def api_key_update(self, context, re_match): context.log_debug('Failed to enable personal API keys. Missing: %s' % ', '.join(log_list)) # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/show_client_ip/$') + @RegisterProviderPath('^/show_client_ip/$') def show_client_ip(self, context, re_match): port = context.get_settings().httpd_port() @@ -1315,7 +1313,7 @@ def show_client_ip(self, context, re_match): context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.httpd.not.running'])) # noinspection PyUnusedLocal - @kodion.RegisterProviderPath('^/playback_history/$') + @RegisterProviderPath('^/playback_history/$') def on_playback_history(self, context, re_match): params = context.get_params() video_id = params.get('video_id') @@ -1359,7 +1357,7 @@ def on_root(self, context, re_match): settings = context.get_settings() _ = self.get_client(context) # required for self.is_logged_in() - self.set_content_type(context, kodion.constants.content_type.FILES) + self.set_content_type(context, constants.content_type.FILES) result = [] @@ -1420,22 +1418,20 @@ def on_root(self, context, re_match): # search if settings.get_bool('youtube.folder.search.show', True): - search_item = kodion.items.SearchItem(context, image=context.create_resource_path('media', 'search.png'), - fanart=self.get_fanart(context)) + search_item = SearchItem(context, image=context.create_resource_path('media', 'search.png'), + fanart=self.get_fanart(context)) result.append(search_item) if settings.get_bool('youtube.folder.quick_search.show', True): - quick_search_item = kodion.items.NewSearchItem(context, - alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search']), - fanart=self.get_fanart(context)) + quick_search_item = NewSearchItem(context, alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search']), + fanart=self.get_fanart(context)) result.append(quick_search_item) if settings.get_bool('youtube.folder.quick_search_incognito.show', True): - quick_search_incognito_item = kodion.items.NewSearchItem(context, - alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search.incognito']), - image=context.create_resource_path('media', 'search.png'), - fanart=self.get_fanart(context), - incognito=True) + quick_search_incognito_item = NewSearchItem(context, alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search.incognito']), + image=context.create_resource_path('media', 'search.png'), + fanart=self.get_fanart(context), + incognito=True) result.append(quick_search_incognito_item) # my location @@ -1594,13 +1590,13 @@ def on_root(self, context, re_match): @staticmethod def set_content_type(context, content_type): context.set_content_type(content_type) - if content_type == kodion.constants.content_type.VIDEOS: - context.add_sort_method(kodion.constants.sort_method.UNSORTED, - kodion.constants.sort_method.VIDEO_RUNTIME, - kodion.constants.sort_method.DATEADDED, - kodion.constants.sort_method.TRACKNUM, - kodion.constants.sort_method.VIDEO_TITLE, - kodion.constants.sort_method.DATE) + if content_type == constants.content_type.VIDEOS: + context.add_sort_method(constants.sort_method.UNSORTED, + constants.sort_method.VIDEO_RUNTIME, + constants.sort_method.DATEADDED, + constants.sort_method.TRACKNUM, + constants.sort_method.VIDEO_TITLE, + constants.sort_method.DATE) def handle_exception(self, context, exception_to_handle): if isinstance(exception_to_handle, (InvalidGrant, LoginException)): diff --git a/resources/lib/youtube_registration.py b/resources/lib/youtube_registration.py index ab64f9ad4..429420ba0 100644 --- a/resources/lib/youtube_registration.py +++ b/resources/lib/youtube_registration.py @@ -9,7 +9,7 @@ from base64 import b64encode from youtube_plugin.kodion.json_store import APIKeyStore -from youtube_plugin.kodion.impl import Context +from youtube_plugin.kodion.context import Context def register_api_keys(addon_id, api_key, client_id, client_secret): diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index 513546a64..7f427348e 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -10,7 +10,7 @@ import re from youtube_plugin.youtube.provider import Provider -from youtube_plugin.kodion.impl import Context +from youtube_plugin.kodion.context import Context def __get_core_components(addon_id=None): diff --git a/resources/lib/youtube_resolver.py b/resources/lib/youtube_resolver.py index 28eb03256..69d5d411c 100644 --- a/resources/lib/youtube_resolver.py +++ b/resources/lib/youtube_resolver.py @@ -10,7 +10,7 @@ import re from youtube_plugin.youtube.provider import Provider -from youtube_plugin.kodion.impl import Context +from youtube_plugin.kodion.context import Context def _get_core_components(addon_id=None): From a0d7c1b90ea4d98cbddba032cdfd648d7784a9cb Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 2 Nov 2023 23:35:34 +1100 Subject: [PATCH 025/141] Fix completed live streams playing at max 720p - Fix #530 - Partially fix #540 - MPEG-DASH and HLS manifests are not provided for completed live streams --- .../youtube/helper/video_info.py | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 914c3ff21..4f66dac61 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1264,26 +1264,32 @@ def _get_video_info(self): self._player_js = self._get_player_js() self._cipher = Cipher(self._context, javascript=self._player_js) - manifest_url = None - if is_live: - live_type = _settings.get_live_stream_type() - if live_type == 'isa_mpd': - manifest_url = streaming_data.get('dashManifestUrl', '') - else: - stream_list.extend(self._load_hls_manifest( - streaming_data.get('hlsManifestUrl'), - live_type, meta_info, client['headers'], playback_stats - )) - elif httpd_is_live and adaptive_fmts: + manifest_url = main_stream = None + live_type = is_live and _settings.get_live_stream_type() + + if live_type == 'isa_mpd' and 'dashManifestUrl' in streaming_data: + manifest_url = streaming_data['dashManifestUrl'] + elif 'hlsManifestUrl' in streaming_data: + stream_list.extend(self._load_hls_manifest( + streaming_data['hlsManifestUrl'], + live_type, meta_info, client['headers'], playback_stats + )) + else: + live_type = None + + # extract adaptive streams and create MPEG-DASH manifest + if not manifest_url and httpd_is_live and adaptive_fmts: video_data, audio_data = self._process_stream_data( adaptive_fmts, default_lang['code'] ) manifest_url, main_stream = self._generate_mpd_manifest( video_data, audio_data, license_info.get('url') ) - stream_list.extend(self._load_hls_manifest( - streaming_data.get('hlsManifestUrl'), - None, meta_info, client['headers'], playback_stats + + # extract non-adaptive streams + if all_fmts: + stream_list.extend(self._create_stream_list( + all_fmts, meta_info, client['headers'], playback_stats )) if manifest_url: @@ -1295,7 +1301,7 @@ def _get_video_info(self): 'playback_stats': playback_stats } - if is_live: + if live_type: # MPD structure has segments with additional attributes # and url has changed from using a query string to using url params # This breaks the InputStream.Adaptive partial manifest update @@ -1306,7 +1312,7 @@ def _get_video_info(self): else: video_stream['url'] = manifest_url + '/mpd_version/5' details = self.FORMAT.get('9998') - else: + elif main_stream: details = self.FORMAT.get('9999').copy() video_info = main_stream['video'] @@ -1336,13 +1342,6 @@ def _get_video_info(self): video_stream.update(details) stream_list.append(video_stream) - # extract streams from map - if all_fmts: - stream_list.extend(self._create_stream_list( - all_fmts, meta_info, client['headers'], playback_stats - )) - - # last fallback if not stream_list: raise YouTubeException('No streams found') From 6d1301eaa04a04727b0c983fec096d3b20b5bcdf Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:13:16 +1100 Subject: [PATCH 026/141] Add setting methods to get/set API details - Also add some basic validation --- .../kodion/constants/const_settings.py | 3 + .../kodion/settings/abstract_settings.py | 67 +++++++++++++++---- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 58ac464a6..1f13d7468 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -50,5 +50,8 @@ HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (string) API_CONFIG_PAGE = 'youtube.api.config.page' # (bool) +API_KEY = 'youtube.api.key' # (string) +API_ID = 'youtube.api.id' # (string) +API_SECRET = 'youtube.api.secret' # (string) CLIENT_SELECTION = 'youtube.client.selection' # (int) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 61e42fa56..b4d975f45 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -136,7 +136,7 @@ def verify_ssl(self): def get_timeout(self): connect_timeout = self.get_int(SETTINGS.CONNECT_TIMEOUT, 9) + 0.5 read_timout = self.get_int(SETTINGS.READ_TIMEOUT, 27) - return (connect_timeout, read_timout) + return connect_timeout, read_timout def allow_dev_keys(self): return self.get_bool(SETTINGS.ALLOW_DEV_KEYS, False) @@ -170,30 +170,71 @@ def use_mpd_live_streams(self): return self.get_int(SETTINGS.LIVE_STREAMS + '.1', 0) == 3 return False - def httpd_port(self): - return self.get_int(SETTINGS.HTTPD_PORT, 50152) + def httpd_port(self, port=None): + default_port = 50152 + + if not port: + port = self.get_int(SETTINGS.HTTPD_PORT, default_port) - def httpd_listen(self, default='0.0.0.0', for_request=False): - ip_address = self.get_string(SETTINGS.HTTPD_LISTEN, default) try: - ip_address = ip_address.strip() - except AttributeError: - pass + port = int(port) + except ValueError: + return default_port + return port + + def httpd_listen(self, for_request=False, ip_address=None): + default_address = '0.0.0.0' + default_octets = [0, 0, 0, 0,] + if not ip_address: - ip_address = default - if for_request and ip_address == default: - ip_address = '127.0.0.1' - return ip_address + ip_address = self.get_string(SETTINGS.HTTPD_LISTEN, + default_address) + + try: + octets = [octet for octet in map(int, ip_address.split('.')) + if 0 <= octet <= 255] + if len(octets) != 4: + raise ValueError + except ValueError: + octets = default_octets + + if for_request and octets == default_octets: + return '127.0.0.1' + return '.'.join(map(str, octets)) def set_httpd_listen(self, value): return self.set_string(SETTINGS.HTTPD_LISTEN, value) def httpd_whitelist(self): - return self.get_string(SETTINGS.HTTPD_WHITELIST, '') + allow_list = self.get_string(SETTINGS.HTTPD_WHITELIST, '') + allow_list = ''.join(allow_list.split()).split(',') + allow_list = [ + self.httpd_listen(for_request=True, ip_address=ip_address) + for ip_address in allow_list + ] + return allow_list def api_config_page(self): return self.get_bool(SETTINGS.API_CONFIG_PAGE, False) + def api_id(self, new_id=None): + if new_id is not None: + self.set_string(SETTINGS.API_ID, new_id) + return new_id + return self.get_string(SETTINGS.API_ID) + + def api_key(self, new_key=None): + if new_key is not None: + self.set_string(SETTINGS.API_KEY, new_key) + return new_key + return self.get_string(SETTINGS.API_KEY) + + def api_secret(self, new_secret=None): + if new_secret is not None: + self.set_string(SETTINGS.API_SECRET, new_secret) + return new_secret + return self.get_string(SETTINGS.API_SECRET) + def get_location(self): location = self.get_string(SETTINGS.LOCATION, '').replace(' ', '').strip() coords = location.split(',') From c56234d88c7206964a7adf06a59aeaf77c61d175 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 4 Nov 2023 17:04:29 +1100 Subject: [PATCH 027/141] Fix local resuming not working after d873bbd - Also enable for non MPEG-DASH videos - Also respect user choice from Kodi resume prompt --- .../youtube_plugin/kodion/context/xbmc/xbmc_context.py | 9 +++++++-- resources/lib/youtube_plugin/youtube/helper/yt_play.py | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 7f77c785a..17441ff2d 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -48,6 +48,8 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override sys parameters and re-build our clean uri. Also we extract the path and parameters - man, that would be so simple with the normal url-parsing routines. """ + num_args = len(sys.argv) + # first the path of the uri if override: self._uri = sys.argv[0] @@ -55,18 +57,21 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override self._path = unquote(comps.path) # after that try to get the params - if len(sys.argv) > 2: + if num_args > 2: params = sys.argv[2][1:] if params: self._uri = '?'.join([self._uri, params]) self._params = dict(parse_qsl(params)) + if num_args > 3 and sys.argv[3].lower() == 'resume:true': + self._params['resume'] = True + self._ui = None self._video_playlist = None self._audio_playlist = None self._video_player = None self._audio_player = None - self._plugin_handle = int(sys.argv[1]) if len(sys.argv) > 1 else None + self._plugin_handle = int(sys.argv[1]) if num_args > 1 else -1 self._plugin_id = plugin_id or self._addon.getAddonInfo('id') self._plugin_name = plugin_name or self._addon.getAddonInfo('name') self._version = self._addon.getAddonInfo('version') diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index eceed96c4..d735d400d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -85,7 +85,6 @@ def play_video(provider, context): use_remote_history = use_history and settings.use_remote_history() use_play_data = use_history and settings.use_local_history() - video_item = utils.update_play_info(provider, context, video_id, video_item, video_stream, use_play_data=use_play_data) @@ -93,9 +92,10 @@ def play_video(provider, context): play_count = 0 playback_stats = video_stream.get('playback_stats') - if use_remote_history: - if video_item.get_start_time() and video_item.use_mpd_video(): - seek_time = video_item.get_start_time() + if use_play_data: + seek = video_item.get_start_time() + if seek and context.get_param('resume'): + seek_time = start_time play_count = video_item.get_play_count() if video_item.get_play_count() is not None else '0' item = to_playback_item(context, video_item) From 4362bbcdcefa76172db19ace2fd8d08d3ff0d371 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 Nov 2023 23:24:47 +1100 Subject: [PATCH 028/141] Use correct types for context params and play_data --- .../kodion/abstract_provider.py | 6 +- .../kodion/context/abstract_context.py | 116 +++++++++++++++--- .../kodion/context/xbmc/xbmc_context.py | 9 +- .../kodion/items/next_page_item.py | 2 +- .../youtube_plugin/kodion/items/video_item.py | 4 +- resources/lib/youtube_plugin/kodion/runner.py | 2 +- .../kodion/ui/xbmc/xbmc_items.py | 8 +- .../kodion/utils/playback_history.py | 27 ++-- .../lib/youtube_plugin/kodion/utils/player.py | 4 +- .../lib/youtube_plugin/youtube/helper/tv.py | 12 +- .../youtube/helper/url_to_item_converter.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 8 +- .../lib/youtube_plugin/youtube/helper/v3.py | 4 +- .../youtube_plugin/youtube/helper/yt_play.py | 12 +- .../youtube/helper/yt_specials.py | 26 ++-- .../youtube_plugin/youtube/helper/yt_video.py | 2 +- .../lib/youtube_plugin/youtube/provider.py | 79 ++++++------ 17 files changed, 208 insertions(+), 115 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index edd9efc40..8c466e846 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -249,7 +249,7 @@ def _internal_search(self, context, re_match): if not query: return False - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) channel_id = context.get_param('channel_id', '') query = to_utf8(query) @@ -270,7 +270,7 @@ def _internal_search(self, context, re_match): return self.on_search(query, context, re_match) if command == 'query': - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) channel_id = context.get_param('channel_id', '') query = params['q'] query = to_unicode(query) @@ -287,7 +287,7 @@ def _internal_search(self, context, re_match): context.set_content_type(constants.content_type.FILES) result = [] - location = str(context.get_param('location', False)).lower() == 'true' + location = context.get_param('location', False) # 'New Search...' new_search_item = NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 321087b66..8d3f56dd0 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -16,7 +16,6 @@ from ..utils import ( create_path, create_uri_path, - to_utf8, AccessManager, DataCache, FavoriteList, @@ -29,6 +28,63 @@ class AbstractContext(object): + _BOOL_PARAMS = { + 'ask_for_quality', + 'audio_only', + 'confirmed', + 'enable', + 'hide_folders', + 'hide_live', + 'hide_playlists', + 'hide_search', + 'incognito', + 'location', + 'logged_in', + 'play', + 'prompt_for_subtitles', + 'refresh', + 'refresh_container' + 'resume', + 'screensaver', + 'strm', + } + _INT_PARAMS = { + 'live', + 'offset', + 'page', + } + _FLOAT_PARAMS = { + 'seek', + } + _LIST_PARAMS = { + 'channel_ids', + 'playlist_ids', + } + _STRING_PARAMS = { + 'api_key', + 'action', # deprecated + 'addon_id', + 'channel_id', + 'channel_name', + 'client_id', + 'client_secret', + 'event_type', + 'item', + 'next_page_token', + 'page_token', + 'parent_id', + 'playlist', # deprecated + 'playlist_id', + 'playlist_name', + 'q', + 'rating', + 'search_type', + 'subscription_id', + 'videoid', # deprecated + 'video_id', + 'uri', + } + def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): if not params: params = {} @@ -55,6 +111,7 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): self._view_mode = None # create valid uri + self.parse_params() self._uri = self.create_uri(self._path, self._params) def format_date_short(self, date_obj): @@ -90,7 +147,7 @@ def get_data_cache(self): if max_cache_size_mb <= 0: max_cache_size_mb = 5 else: - max_cache_size_mb = max_cache_size_mb / 2.0 + max_cache_size_mb /= 2.0 self._data_cache = DataCache(os.path.join(self.get_cache_path(), 'data_cache'), max_file_size_mb=max_cache_size_mb) return self._data_cache @@ -101,7 +158,7 @@ def get_function_cache(self): if max_cache_size_mb <= 0: max_cache_size_mb = 5 else: - max_cache_size_mb = max_cache_size_mb / 2.0 + max_cache_size_mb /= 2.0 self._function_cache = FunctionCache(os.path.join(self.get_cache_path(), 'cache'), max_file_size_mb=max_cache_size_mb) return self._function_cache @@ -160,17 +217,7 @@ def create_uri(self, path='/', params=None): uri = "%s://%s/" % ('plugin', str(self._plugin_id)) if params: - # make a copy of the map - uri_params = {} - uri_params.update(params) - - # encode in utf-8 - for param in uri_params: - if isinstance(params[param], int): - params[param] = str(params[param]) - - uri_params[param] = to_utf8(params[param]) - uri = '?'.join([uri, urlencode(uri_params)]) + uri = '?'.join([uri, urlencode(params, encoding='utf-8')]) return uri @@ -184,10 +231,47 @@ def get_params(self): return self._params def get_param(self, name, default=None): - return self.get_params().get(name, default) + return self._params.get(name, default) + + def parse_params(self, params=None): + if not params: + params = self._params + to_delete = [] + + for param, value in params.items(): + try: + if param in self._BOOL_PARAMS: + parsed_value = str(value).lower() in ('true', '1') + elif param in self._INT_PARAMS: + parsed_value = int(value) + elif param in self._FLOAT_PARAMS: + parsed_value = float(value) + elif param in self._LIST_PARAMS: + parsed_value = [ + val for val in value.split(',') if val + ] + elif param in self._STRING_PARAMS: + parsed_value = str(value) + else: + self.log_debug('Unknown parameter - |{0}: {1}|'.format( + param, value + )) + to_delete.append(param) + continue + except (TypeError, ValueError): + self.log_error('Invalid parameter value - |{0}: {1}|'.format( + param, value + )) + to_delete.append(param) + continue + + self._params[param] = parsed_value + + for param in to_delete: + del params[param] def set_param(self, name, value): - self._params[name] = value + self.parse_params({name: value}) def get_data_path(self): """ diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 17441ff2d..e5bdd7ac4 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -53,15 +53,15 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override # first the path of the uri if override: self._uri = sys.argv[0] - comps = urlparse(self._uri) - self._path = unquote(comps.path) + parsed_url = urlparse(self._uri) + self._path = unquote(parsed_url.path) # after that try to get the params if num_args > 2: params = sys.argv[2][1:] if params: self._uri = '?'.join([self._uri, params]) - self._params = dict(parse_qsl(params)) + self.parse_params(dict(parse_qsl(params))) if num_args > 3 and sys.argv[3].lower() == 'resume:true': self._params['resume'] = True @@ -332,7 +332,8 @@ def inputstream_adaptive_capabilities(self, capability=None): version = self._ISA_CAPABILITIES.get(capability) return version is True or version and isa_loose_version >= utils.loose_version(version) - def inputstream_adaptive_auto_stream_selection(self): + @staticmethod + def inputstream_adaptive_auto_stream_selection(): try: return xbmcaddon.Addon('inputstream.adaptive').getSetting('STREAMSELECTION') == '0' except RuntimeError: diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index 4aa83a91b..364baa36c 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -16,7 +16,7 @@ class NextPageItem(DirectoryItem): def __init__(self, context, current_page=1, image=None, fanart=None): new_params = {} new_params.update(context.get_params()) - new_params['page'] = str(current_page + 1) + new_params['page'] = current_page + 1 name = context.localize(constants.localize.NEXT_PAGE, 'Next Page') if name.find('%d') != -1: name %= current_page + 1 diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 595bf90f6..65e3579ef 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -286,13 +286,13 @@ def get_last_played(self): return self._last_played def set_start_percent(self, start_percent): - self._start_percent = start_percent or '' + self._start_percent = start_percent or 0 def get_start_percent(self): return self._start_percent def set_start_time(self, start_time): - self._start_time = start_time or '' + self._start_time = start_time or 0 def get_start_time(self): return self._start_time diff --git a/resources/lib/youtube_plugin/kodion/runner.py b/resources/lib/youtube_plugin/kodion/runner.py index 19067c112..63ae7b367 100644 --- a/resources/lib/youtube_plugin/kodion/runner.py +++ b/resources/lib/youtube_plugin/kodion/runner.py @@ -55,7 +55,7 @@ def run(provider, context=None): context.log_notice('Running: %s (%s) on %s with %s\n\tPath: %s\n\tParams: %s' % (name, addon_version, version, python_version, - context.get_path(), str(context_params))) + context.get_path(), context_params)) __RUNNER__.run(provider, context) provider.tear_down(context) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index a23ce255b..2cbbe6324 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -109,10 +109,10 @@ def to_play_item(context, play_item): if not is_strm: if play_item.get_play_count() == 0: if play_item.get_start_percent(): - list_item.setProperty('StartPercent', play_item.get_start_percent()) + list_item.setProperty('StartPercent', str(play_item.get_start_percent())) if play_item.get_start_time(): - list_item.setProperty('StartOffset', play_item.get_start_time()) + list_item.setProperty('StartOffset', str(play_item.get_start_time())) if play_item.subtitles: list_item.setSubtitles(play_item.subtitles) @@ -165,10 +165,10 @@ def to_video_item(context, video_item): if video_item.get_play_count() == 0: if video_item.get_start_percent(): - item.setProperty('StartPercent', video_item.get_start_percent()) + item.setProperty('StartPercent', str(video_item.get_start_percent())) if video_item.get_start_time(): - item.setProperty('StartOffset', video_item.get_start_time()) + item.setProperty('StartOffset', str(video_item.get_start_time())) # This should work for all versions of XBMC/KODI. if 'duration' in _info_labels: diff --git a/resources/lib/youtube_plugin/kodion/utils/playback_history.py b/resources/lib/youtube_plugin/kodion/utils/playback_history.py index db54100bf..e61779755 100644 --- a/resources/lib/youtube_plugin/kodion/utils/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/utils/playback_history.py @@ -26,30 +26,37 @@ def _decode(obj): return pickle.loads(obj) self._open() - placeholders = ','.join(['?' for _ in keys]) - keys = [str(item) for item in keys] + placeholders = ','.join(['?'] * len(keys)) query = 'SELECT * FROM %s WHERE key IN (%s)' % (self._table_name, placeholders) - query_result = self._execute(False, query, keys) + query_result = self._execute(False, query, list(keys)) + data_keys = ['play_count', 'total_time' 'played_time', 'played_percent'] result = {} if query_result: for item in query_result: values = _decode(item[2]).split(',') - result[str(item[0])] = {'play_count': values[0], 'total_time': values[1], - 'played_time': values[2], 'played_percent': values[3], - 'last_played': item[1]} + result[item[0]] = { + 'play_count': int(values[0]), + 'total_time': float(values[1]), + 'played_time': float(values[2]), + 'played_percent': int(values[3]), + 'last_played': item[1], + } self._close() return result def get_item(self, key): - key = str(key) query_result = self._get(key) result = {} if query_result: values = query_result[0].split(',') - result[key] = {'play_count': values[0], 'total_time': values[1], - 'played_time': values[2], 'played_percent': values[3], - 'last_played': query_result[1]} + result[key] = { + 'play_count': int(values[0]), + 'total_time': float(values[1]), + 'played_time': float(values[2]), + 'played_percent': int(values[3]), + 'last_played': query_result[1], + } return result def clear(self): diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index 0bdccf1c1..0374d98da 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -71,8 +71,6 @@ def run(self): if playback_stats is None: playback_stats = {} - play_count = str(play_count) - played_time = -1.0 state = 'playing' @@ -266,7 +264,7 @@ def run(self): is_logged_in = self.provider.is_logged_in() if self.percent_complete >= settings.get_play_count_min_percent(): - play_count = '1' + play_count += 1 self.current_time = 0.0 if is_logged_in and report_url and use_remote_history: client.update_watch_history(self.video_id, report_url diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index 94a9795af..a78b39dde 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -16,7 +16,7 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): result = [] video_id_dict = {} - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) filter_list = [] black_list = False @@ -63,7 +63,7 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): new_context = context.clone(new_params=new_params) - current_page = int(new_context.get_param('page', 1)) + current_page = new_context.get_param('page', 1) next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) @@ -74,7 +74,7 @@ def tv_videos_to_items(provider, context, json_data): result = [] video_id_dict = {} - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) items = json_data.get('items', []) for item in items: @@ -109,7 +109,7 @@ def tv_videos_to_items(provider, context, json_data): new_context = context.clone(new_params=new_params) - current_page = int(new_context.get_param('page', 1)) + current_page = new_context.get_param('page', 1) next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) @@ -120,7 +120,7 @@ def saved_playlists_to_items(provider, context, json_data): result = [] playlist_id_dict = {} - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) thumb_size = context.get_settings().use_thumbnail_size() items = json_data.get('items', []) @@ -160,7 +160,7 @@ def saved_playlists_to_items(provider, context, json_data): new_context = context.clone(new_params=new_params) - current_page = int(new_context.get_param('page', 1)) + current_page = new_context.get_param('page', 1) next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index a8e3c2d69..068d853b0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -121,7 +121,7 @@ def get_items(self, provider, context, title_required=True): return result def get_video_items(self, provider, context, title_required=True): - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) use_play_data = not incognito if not self._video_items: diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 78392b4af..70f6e688b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -287,7 +287,7 @@ def update_video_infos(provider, context, video_id_dict, # duration if not video_item.live and play_data and 'total_time' in play_data: - duration = float(play_data['total_time'] or 0) + duration = play_data['total_time'] else: duration = yt_item.get('contentDetails', {}).get('duration') if duration: @@ -448,12 +448,12 @@ def update_video_infos(provider, context, video_id_dict, yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name) if not video_item.live and play_data: - if play_data.get('play_count') is None or int(play_data.get('play_count')) == 0: + if not play_data.get('play_count'): yt_context_menu.append_mark_watched(context_menu, provider, context, video_id) else: yt_context_menu.append_mark_unwatched(context_menu, provider, context, video_id) - if int(play_data.get('played_percent', '0')) > 0 or float(play_data.get('played_time', '0.0')) > 0.0: + if play_data.get('played_percent', 0) > 0 or play_data.get('played_time', 0) > 0: yt_context_menu.append_reset_resume_point(context_menu, provider, context, video_id) # more... @@ -557,7 +557,7 @@ def update_play_info(provider, context, video_id, video_item, video_stream, use_ # duration if not video_item.live and play_data and 'total_time' in play_data: - duration = float(play_data['total_time'] or 0) + duration = play_data['total_time'] else: duration = yt_item.get('contentDetails', {}).get('duration') if duration: diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 186a60c33..3a5c1a7b3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -31,7 +31,7 @@ def _process_list_response(provider, context, json_data): context.log_warning('List of search result is empty') return result - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) addon_id = context.get_param('addon_id', '') for yt_item in yt_items: @@ -325,7 +325,7 @@ def response_to_items(provider, context, json_data, sort=None, reverse_sort=Fals new_context = context.clone(new_params=new_params) - current_page = int(new_context.get_param('page', 1)) + current_page = new_context.get_param('page', 1) next_page_item = items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index d735d400d..a2c126327 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -36,7 +36,7 @@ def play_video(provider, context): context.get_ui().clear_home_window_property('ask_for_quality') screensaver = False - if context.get_param('screensaver', None) and str(context.get_param('screensaver')).lower() == 'true': + if context.get_param('screensaver'): ask_for_quality = False screensaver = True @@ -80,7 +80,7 @@ def play_video(provider, context): title = metadata.get('video', {}).get('title', '') video_item = VideoItem(title, video_stream['url']) - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) use_history = not is_live and not screensaver and not incognito use_remote_history = use_history and settings.use_remote_history() use_play_data = use_history and settings.use_local_history() @@ -96,7 +96,7 @@ def play_video(provider, context): seek = video_item.get_start_time() if seek and context.get_param('resume'): seek_time = start_time - play_count = video_item.get_play_count() if video_item.get_play_count() is not None else '0' + play_count = video_item.get_play_count() or 0 item = to_playback_item(context, video_item) item.setPath(video_item.get_uri()) @@ -223,10 +223,10 @@ def _load_videos(_page_token='', _progress_dialog=None): if progress_dialog: progress_dialog.close() - if (context.get_param('play', '') == '1') and (context.get_handle() == -1): + if context.get_param('play') and context.get_handle() == -1: player.play(playlist_index=playlist_position) return False - if context.get_param('play', '') == '1': + if context.get_param('play'): return videos[playlist_position] return True @@ -234,7 +234,7 @@ def _load_videos(_page_token='', _progress_dialog=None): def play_channel_live(provider, context): channel_id = context.get_param('channel_id') - index = int(context.get_param('live')) - 1 + index = context.get_param('live') - 1 if index < 0: index = 0 json_data = provider.get_client(context).search(q='', search_type='video', event_type='live', channel_id=channel_id, safe_search=False) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 8a9c0e114..5cd5935da 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -127,7 +127,7 @@ def _sort(x): # TODO: cache result page_token = context.get_param('page_token', '') - location = str(context.get_param('location', False)).lower() == 'true' + location = context.get_param('location', False) json_data = provider.get_client(context).get_live_events(event_type=event_type, page_token=page_token, location=location) if not v3.handle_error(provider, context, json_data): @@ -138,7 +138,7 @@ def _sort(x): def _process_description_links(provider, context): - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) addon_id = context.get_param('addon_id', '') def _extract_urls(_video_id): @@ -250,17 +250,13 @@ def _display_playlists(_playlist_ids): if video_id: return _extract_urls(video_id) - channel_ids = context.get_param('channel_ids', '') + channel_ids = context.get_param('channel_ids', []) if channel_ids: - channel_ids = channel_ids.split(',') - if channel_ids: - return _display_channels(channel_ids) + return _display_channels(channel_ids) - playlist_ids = context.get_param('playlist_ids', '') + playlist_ids = context.get_param('playlist_ids', []) if playlist_ids: - playlist_ids = playlist_ids.split(',') - if playlist_ids: - return _display_playlists(playlist_ids) + return _display_playlists(playlist_ids) context.log_error('Missing video_id or playlist_ids for description links') @@ -272,7 +268,7 @@ def _process_saved_playlists_tv(provider, context): result = [] next_page_token = context.get_param('next_page_token', '') - offset = int(context.get_param('offset', 0)) + offset = context.get_param('offset', 0) json_data = provider.get_client(context).get_saved_playlists(page_token=next_page_token, offset=offset) result.extend(tv.saved_playlists_to_items(provider, context, json_data)) @@ -284,7 +280,7 @@ def _process_watch_history_tv(provider, context): result = [] next_page_token = context.get_param('next_page_token', '') - offset = int(context.get_param('offset', 0)) + offset = context.get_param('offset', 0) json_data = provider.get_client(context).get_watch_history(page_token=next_page_token, offset=offset) result.extend(tv.tv_videos_to_items(provider, context, json_data)) @@ -296,7 +292,7 @@ def _process_purchases_tv(provider, context): result = [] next_page_token = context.get_param('next_page_token', '') - offset = int(context.get_param('offset', 0)) + offset = context.get_param('offset', 0) json_data = provider.get_client(context).get_purchases(page_token=next_page_token, offset=offset) result.extend(tv.tv_videos_to_items(provider, context, json_data)) @@ -308,7 +304,7 @@ def _process_new_uploaded_videos_tv(provider, context): result = [] next_page_token = context.get_param('next_page_token', '') - offset = int(context.get_param('offset', 0)) + offset = context.get_param('offset', 0) json_data = provider.get_client(context).get_my_subscriptions(page_token=next_page_token, offset=offset) result.extend(tv.my_subscriptions_to_items(provider, context, json_data)) @@ -320,7 +316,7 @@ def _process_new_uploaded_videos_tv_filtered(provider, context): result = [] next_page_token = context.get_param('next_page_token', '') - offset = int(context.get_param('offset', 0)) + offset = context.get_param('offset', 0) json_data = provider.get_client(context).get_my_subscriptions(page_token=next_page_token, offset=offset) result.extend(tv.my_subscriptions_to_items(provider, context, json_data, do_filter=True)) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 16cece658..81728600e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -67,7 +67,7 @@ def _process_rate_video(provider, context, re_match): elif response.get('status_code') == 204: # this will be set if we are in the 'Liked Video' playlist - if context.get_param('refresh_container', '0') == '1': + if context.get_param('refresh_container'): context.get_ui().refresh_container() if result == 'none': diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 09d47d879..21367aed2 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -496,7 +496,7 @@ def _on_channel_playlists(self, context, re_match): resource_manager = self.get_resource_manager(context) item_params = {} - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) addon_id = context.get_param('addon_id', '') if incognito: item_params.update({'incognito': incognito}) @@ -595,9 +595,9 @@ def _on_channel(self, context, re_match): return False channel_fanarts = resource_manager.get_fanarts([channel_id]) - page = int(context.get_param('page', 1)) + page = context.get_param('page', 1) page_token = context.get_param('page_token', '') - incognito = str(context.get_param('incognito', False)).lower() == 'true' + incognito = context.get_param('incognito', False) addon_id = context.get_param('addon_id', '') item_params = {} if incognito: @@ -605,12 +605,12 @@ def _on_channel(self, context, re_match): if addon_id: item_params.update({'addon_id': addon_id}) - hide_folders = str(context.get_param('hide_folders', False)).lower() == 'true' + hide_folders = context.get_param('hide_folders', False) if page == 1 and not hide_folders: - hide_playlists = str(context.get_param('hide_playlists', False)).lower() == 'true' - hide_search = str(context.get_param('hide_search', False)).lower() == 'true' - hide_live = str(context.get_param('hide_live', False)).lower() == 'true' + hide_playlists = context.get_param('hide_playlists', False) + hide_search = context.get_param('hide_search', False) + hide_live = context.get_param('hide_live', False) if not hide_playlists: playlists_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.playlists'])), @@ -729,7 +729,7 @@ def on_play(self, context, re_match): context.get_ui().clear_home_window_property('ask_for_quality') if 'prompt_for_subtitles' in params: - prompt_subtitles = params['prompt_for_subtitles'] == '1' + prompt_subtitles = params['prompt_for_subtitles'] del params['prompt_for_subtitles'] if prompt_subtitles and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems @@ -738,7 +738,7 @@ def on_play(self, context, re_match): redirect = True elif 'audio_only' in params: - audio_only = params['audio_only'] == '1' + audio_only = params['audio_only'] del params['audio_only'] if audio_only and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems @@ -747,7 +747,7 @@ def on_play(self, context, re_match): redirect = True elif 'ask_for_quality' in params: - ask_for_quality = params['ask_for_quality'] == '1' + ask_for_quality = params['ask_for_quality'] del params['ask_for_quality'] if ask_for_quality and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems @@ -773,7 +773,7 @@ def on_play(self, context, re_match): return yt_play.play_video(self, context) if 'playlist_id' in params: return yt_play.play_playlist(self, context) - if 'channel_id' in params and 'live' in params and int(params['live']) > 0: + if 'channel_id' in params and 'live' in params and params['live'] > 0: return yt_play.play_channel_live(self, context) return False @@ -822,7 +822,8 @@ def _on_yt_clear_history(self, context, re_match): @RegisterProviderPath('^/users/(?P[^/]+)/$') def _on_users(self, context, re_match): action = re_match.group('action') - refresh = context.get_param('refresh', 'true').lower() == 'true' + refresh = context.get_param('refresh') + access_manager = context.get_access_manager() ui = context.get_ui() @@ -972,7 +973,7 @@ def switch_to_user(_user): @RegisterProviderPath('^/sign/(?P[^/]+)/$') def _on_sign(self, context, re_match): - sign_out_confirmed = context.get_param('confirmed', '').lower() == 'true' + sign_out_confirmed = context.get_param('confirmed') mode = re_match.group('mode') if (mode == 'in') and context.get_access_manager().has_refresh_token(): yt_login.process('out', self, context, sign_out_refresh=False) @@ -1018,9 +1019,9 @@ def on_search(self, search_text, context, re_match): channel_id = context.get_param('channel_id', '') event_type = context.get_param('event_type', '') - hide_folders = str(context.get_param('hide_folders', False)).lower() == 'true' - location = str(context.get_param('location', False)).lower() == 'true' - page = int(context.get_param('page', 1)) + hide_folders = context.get_param('hide_folders', False) + location = context.get_param('location', False) + page = context.get_param('page', 1) page_token = context.get_param('page_token', '') search_type = context.get_param('search_type', 'video') @@ -1256,7 +1257,7 @@ def api_key_update(self, context, re_match): client_id = params.get('client_id') client_secret = params.get('client_secret') api_key = params.get('api_key') - enable = params.get('enable', '').lower() == 'true' + enable = params.get('enable') updated_list = [] log_list = [] @@ -1321,28 +1322,34 @@ def on_playback_history(self, context, re_match): if not video_id or not action: return True playback_history = context.get_playback_history() - items = playback_history.get_items([video_id]) - if not items or not items.get(video_id): - item_dict = {'play_count': '0', 'total_time': '0.0', - 'played_time': '0.0', 'played_percent': '0'} - else: - item_dict = items.get(video_id) + play_data = playback_history.get_items([video_id]).get(video_id) + if not play_data: + play_data = { + 'play_count': 0, + 'total_time': 0, + 'played_time': 0, + 'played_percent': 0 + } + if action == 'mark_unwatched': - if int(item_dict.get('play_count', 0)) > 0: - item_dict['play_count'] = '0' - item_dict['played_time'] = '0.0' - item_dict['played_percent'] = '0' + if play_data.get('play_count', 0) > 0: + play_data['play_count'] = 0 + play_data['played_time'] = 0 + play_data['played_percent'] = 0 + elif action == 'mark_watched': - if int(item_dict.get('play_count', 0)) == 0: - item_dict['play_count'] = '1' + if not play_data.get('play_count', 0): + play_data['play_count'] = 1 + elif action == 'reset_resume': - item_dict['played_time'] = '0.0' - item_dict['played_percent'] = '0' - item_dict['play_count'] = item_dict.get('play_count', '0') - item_dict['total_time'] = item_dict.get('total_time', '0.0') - item_dict['played_time'] = item_dict.get('played_time', '0.0') - item_dict['played_percent'] = item_dict.get('played_percent', '0') - playback_history.update(video_id, item_dict['play_count'], item_dict['total_time'], item_dict['played_time'], item_dict['played_percent']) + play_data['played_time'] = 0 + play_data['played_percent'] = 0 + + playback_history.update(video_id, + play_data.get('play_count', 0), + play_data.get('total_time', 0), + play_data.get('played_time', 0), + play_data.get('played_percent', 0)) context.get_ui().refresh_container() return True From 1e2e5f074115024f7d91f83eab8a0fd260d20c85 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 Nov 2023 23:37:21 +1100 Subject: [PATCH 029/141] Add lisitems to directory as complete listing --- .../kodion/plugin/xbmc/xbmc_runner.py | 100 ++++++++++-------- 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index b44a8fed8..dddeca507 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -35,7 +35,7 @@ def run(self, provider, context): context.log_error(ex.__str__()) xbmcgui.Dialog().ok("Exception in ContentProvider", ex.__str__()) xbmcplugin.endOfDirectory(self.handle, succeeded=False) - return + return False self.settings = context.get_settings() @@ -45,35 +45,45 @@ def run(self, provider, context): if isinstance(result, bool) and not result: xbmcplugin.endOfDirectory(self.handle, succeeded=False) - elif isinstance(result, (VideoItem, AudioItem, UriItem)): - self._set_resolved_url(context, result) - elif isinstance(result, DirectoryItem): - self._add_directory(context, result) + return False + + if isinstance(result, (VideoItem, AudioItem, UriItem)): + return self._set_resolved_url(context, result) + + show_fanart = self.settings.show_fanart() + + if isinstance(result, DirectoryItem): + item_count = 1 + items = [self._add_directory(result, show_fanart)] elif isinstance(result, list): item_count = len(result) - for item in result: - if isinstance(item, DirectoryItem): - self._add_directory(context, item, item_count) - elif isinstance(item, VideoItem): - self._add_video(context, item, item_count) - elif isinstance(item, AudioItem): - self._add_audio(context, item, item_count) - elif isinstance(item, ImageItem): - self._add_image(context, item, item_count) - - xbmcplugin.endOfDirectory( - self.handle, succeeded=True, - updateListing=options.get(AbstractProvider.RESULT_UPDATE_LISTING, False), - cacheToDisc=options.get(AbstractProvider.RESULT_CACHE_TO_DISC, True)) + items = [ + self._add_directory(item, show_fanart) if isinstance(item, DirectoryItem) + else self._add_video(context, item) if isinstance(item, VideoItem) + else self._add_audio(context, item) if isinstance(item, AudioItem) + else self._add_image(item, show_fanart) if isinstance(item, ImageItem) + else None + for item in result + ] else: # handle exception - pass + return False + + succeeded = xbmcplugin.addDirectoryItems( + self.handle, items, item_count + ) + xbmcplugin.endOfDirectory( + self.handle, + succeeded=succeeded, + updateListing=options.get(AbstractProvider.RESULT_UPDATE_LISTING, False), + cacheToDisc=options.get(AbstractProvider.RESULT_CACHE_TO_DISC, True) + ) + return succeeded def _set_resolved_url(self, context, base_item, succeeded=True): item = xbmc_items.to_playback_item(context, base_item) item.setPath(base_item.get_uri()) xbmcplugin.setResolvedUrl(self.handle, succeeded=succeeded, listitem=item) - """ # just to be sure :) if not isLiveStream: @@ -85,8 +95,10 @@ def _set_resolved_url(self, context, base_item, succeeded=True): break tries-=1 """ + return succeeded - def _add_directory(self, context, directory_item, item_count=0): + @staticmethod + def _add_directory(directory_item, show_fanart=False): art = {'icon': 'DefaultFolder.png', 'thumb': directory_item.get_image()} @@ -95,9 +107,10 @@ def _add_directory(self, context, directory_item, item_count=0): info_tag = xbmc_items.ListItemInfoTag(item, tag_type='video') # only set fanart is enabled - if directory_item.get_fanart() and self.settings.show_fanart(): - art['fanart'] = directory_item.get_fanart() - + if show_fanart: + fanart = directory_item.get_fanart() + if fanart: + art['fanart'] = fanart item.setArt(art) @@ -119,28 +132,26 @@ def _add_directory(self, context, directory_item, item_count=0): if directory_item.get_channel_subscription_id(): # make channel_subscription_id property available for keymapping item.setProperty('channel_subscription_id', directory_item.get_channel_subscription_id()) - xbmcplugin.addDirectoryItem(handle=self.handle, - url=directory_item.get_uri(), - listitem=item, - isFolder=is_folder, - totalItems=item_count) + return directory_item.get_uri(), item, is_folder - def _add_video(self, context, video_item, item_count=0): + @staticmethod + def _add_video(context, video_item): item = xbmc_items.to_video_item(context, video_item) item.setPath(video_item.get_uri()) - xbmcplugin.addDirectoryItem(handle=self.handle, - url=video_item.get_uri(), - listitem=item, - totalItems=item_count) + return video_item.get_uri(), item, False - def _add_image(self, context, image_item, item_count): + @staticmethod + def _add_image(image_item, show_fanart=False): art = {'icon': 'DefaultPicture.png', 'thumb': image_item.get_image()} item = xbmcgui.ListItem(label=image_item.get_name(), offscreen=True) - if image_item.get_fanart() and self.settings.show_fanart(): - art['fanart'] = image_item.get_fanart() + # only set fanart is enabled + if show_fanart: + fanart = image_item.get_fanart() + if fanart: + art['fanart'] = fanart item.setArt(art) @@ -150,15 +161,10 @@ def _add_image(self, context, image_item, item_count): item.setInfo(type='picture', infoLabels=info_labels.create_from_item(image_item)) item.setPath(image_item.get_uri()) - xbmcplugin.addDirectoryItem(handle=self.handle, - url=image_item.get_uri(), - listitem=item, - totalItems=item_count) + return image_item.get_uri(), item, False - def _add_audio(self, context, audio_item, item_count): + @staticmethod + def _add_audio(context, audio_item): item = xbmc_items.to_audio_item(context, audio_item) item.setPath(audio_item.get_uri()) - xbmcplugin.addDirectoryItem(handle=self.handle, - url=audio_item.get_uri(), - listitem=item, - totalItems=item_count) + return audio_item.get_uri(), item, False From e210bb8a944c18afe9ce6ef026dc69d517dc8f01 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 9 Nov 2023 00:12:48 +1100 Subject: [PATCH 030/141] Update sqlite based modules - Faster and maybe more rebust - Deduplicate code in subclasses, move to Storage - Fix issue where timestamp was stored as datetime string --- .../kodion/abstract_provider.py | 12 +- .../kodion/context/abstract_context.py | 2 +- .../youtube_plugin/kodion/utils/data_cache.py | 88 ++--- .../kodion/utils/favorite_list.py | 17 +- .../kodion/utils/playback_history.py | 73 ++--- .../kodion/utils/search_history.py | 22 +- .../youtube_plugin/kodion/utils/storage.py | 301 +++++++++++------- .../kodion/utils/watch_later_list.py | 20 +- .../youtube_plugin/youtube/client/youtube.py | 6 +- .../youtube/helper/resource_manager.py | 6 +- .../youtube/helper/video_info.py | 6 +- .../lib/youtube_plugin/youtube/provider.py | 3 +- 12 files changed, 267 insertions(+), 289 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 8c466e846..0a21812eb 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -159,7 +159,7 @@ def _internal_favorite(context, re_match): context.get_ui().refresh_container() return None if command == 'list': - directory_items = context.get_favorite_list().list() + directory_items = context.get_favorite_list().get_items() for directory_item in directory_items: context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), @@ -186,7 +186,7 @@ def _internal_watch_later(self, context, re_match): context.get_ui().refresh_container() return None if command == 'list': - video_items = context.get_watch_later_list().list() + video_items = context.get_watch_later_list().get_items() for video_item in video_items: context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), @@ -207,6 +207,8 @@ def data_cache(self, context): self._data_cache = context.get_data_cache() def _internal_search(self, context, re_match): + context.add_sort_method(constants.sort_method.UNSORTED) + params = context.get_params() command = re_match.group('command') @@ -254,10 +256,10 @@ def _internal_search(self, context, re_match): query = to_utf8(query) try: - self._data_cache.set('search_query', json.dumps({'query': quote(query)})) + encoded = json.dumps({'query': quote(query)}) except KeyError: encoded = json.dumps({'query': quote(query.encode('utf8'))}) - self._data_cache.set('search_query', encoded) + self._data_cache.set_item('search_query', encoded) if not incognito and not channel_id: try: @@ -293,7 +295,7 @@ def _internal_search(self, context, re_match): new_search_item = NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) result.append(new_search_item) - for search in search_history.list(): + for search in search_history.get_items(): # little fallback for old history entries if isinstance(search, DirectoryItem): search = search.get_name() diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 8d3f56dd0..fc2595f18 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -167,7 +167,7 @@ def get_search_history(self): if not self._search_history: max_search_history_items = self.get_settings().get_int(constants.setting.SEARCH_SIZE, 50) self._search_history = SearchHistory(os.path.join(self.get_cache_path(), 'search'), - max_search_history_items) + max_item_count=max_search_history_items) return self._search_history def get_favorite_list(self): diff --git a/resources/lib/youtube_plugin/kodion/utils/data_cache.py b/resources/lib/youtube_plugin/kodion/utils/data_cache.py index 2e6fd89ec..7e986beeb 100644 --- a/resources/lib/youtube_plugin/kodion/utils/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/utils/data_cache.py @@ -9,13 +9,9 @@ """ import json -import pickle -import sqlite3 - -from datetime import datetime, timedelta +from datetime import datetime from .storage import Storage -from .. import logger class DataCache(Storage): @@ -33,55 +29,35 @@ def is_empty(self): return self._is_empty() def get_items(self, seconds, content_ids): - def _decode(obj): - return pickle.loads(obj) + query_result = self._get_by_ids(content_ids, process=json.loads) + if not query_result: + return {} current_time = datetime.now() - placeholders = ','.join(['?' for _ in content_ids]) - keys = [str(item) for item in content_ids] - query = 'SELECT * FROM %s WHERE key IN (%s)' % (self._table_name, placeholders) - - self._open() - - query_result = self._execute(False, query, keys) - result = {} - if query_result: - for item in query_result: - cached_time = item[1] - if cached_time is None: - logger.log_error('Data Cache [get_items]: cached_time is None while getting {content_id}'.format( - content_id=item[0] - )) - cached_time = current_time - # this is so stupid, but we have the function 'total_seconds' only starting with python 2.7 - diff_seconds = self.get_seconds_diff(cached_time) - if diff_seconds <= seconds: - result[str(item[0])] = json.loads(_decode(item[2])) - - self._close() + result = { + item[0]: item[2] + for item in query_result + if self.get_seconds_diff(item[1] or current_time) <= seconds + } return result def get_item(self, seconds, content_id): content_id = str(content_id) query_result = self._get(content_id) - result = {} - if query_result: - current_time = datetime.now() - cached_time = query_result[1] - if cached_time is None: - logger.log_error('Data Cache [get]: cached_time is None while getting {content_id}'.format(content_id=content_id)) - cached_time = current_time - # this is so stupid, but we have the function 'total_seconds' only starting with python 2.7 - diff_seconds = self.get_seconds_diff(cached_time) - if diff_seconds <= seconds: - result[content_id] = json.loads(query_result[0]) + if not query_result: + return {} + + current_time = datetime.now() + if self.get_seconds_diff(query_result[1] or current_time) > seconds: + return {} + result = {content_id: json.loads(query_result[0])} return result - def set(self, content_id, item): + def set_item(self, content_id, item): self._set(content_id, item) - def set_all(self, items): + def set_items(self, items): self._set_all(items) def clear(self): @@ -95,31 +71,3 @@ def update(self, content_id, item): def _optimize_item_count(self): pass - - def _set(self, content_id, item): - def _encode(obj): - return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)) - - current_time = datetime.now() + timedelta(microseconds=1) - query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name - - self._open() - self._execute(True, query, values=[content_id, current_time, _encode(item)]) - self._close() - - def _set_all(self, items): - def _encode(obj): - return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)) - - needs_commit = True - current_time = datetime.now() + timedelta(microseconds=1) - - query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name - - self._open() - - for key, item in items.items(): - self._execute(needs_commit, query, values=[key, current_time, _encode(json.dumps(item))]) - needs_commit = False - - self._close() diff --git a/resources/lib/youtube_plugin/kodion/utils/favorite_list.py b/resources/lib/youtube_plugin/kodion/utils/favorite_list.py index a4031b918..d18a013f6 100644 --- a/resources/lib/youtube_plugin/kodion/utils/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/utils/favorite_list.py @@ -19,18 +19,13 @@ def __init__(self, filename): def clear(self): self._clear() - def list(self): - result = [] + @staticmethod + def _sort_item(_item): + return _item[2].get_name().upper() - for key in self._get_ids(): - data = self._get(key) - item = items.from_json(data[0]) - result.append(item) - - def _sort(_item): - return _item.get_name().upper() - - return sorted(result, key=_sort, reverse=False) + def get_items(self): + result = self._get_by_ids(process=items.from_json) + return sorted(result, key=self._sort_item, reverse=False) def add(self, base_item): item_json_data = items.to_json(base_item) diff --git a/resources/lib/youtube_plugin/kodion/utils/playback_history.py b/resources/lib/youtube_plugin/kodion/utils/playback_history.py index e61779755..3acdb6400 100644 --- a/resources/lib/youtube_plugin/kodion/utils/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/utils/playback_history.py @@ -7,10 +7,6 @@ See LICENSES/GPL-2.0-only for more information. """ -import datetime -import pickle -import sqlite3 - from .storage import Storage @@ -21,42 +17,39 @@ def __init__(self, filename): def is_empty(self): return self._is_empty() + @staticmethod + def _process_item(item): + return item.split(',') + def get_items(self, keys): - def _decode(obj): - return pickle.loads(obj) - - self._open() - placeholders = ','.join(['?'] * len(keys)) - query = 'SELECT * FROM %s WHERE key IN (%s)' % (self._table_name, placeholders) - query_result = self._execute(False, query, list(keys)) - data_keys = ['play_count', 'total_time' 'played_time', 'played_percent'] - result = {} - if query_result: - for item in query_result: - values = _decode(item[2]).split(',') - result[item[0]] = { - 'play_count': int(values[0]), - 'total_time': float(values[1]), - 'played_time': float(values[2]), - 'played_percent': int(values[3]), - 'last_played': item[1], - } - - self._close() + query_result = self._get_by_ids(keys, process=self._process_item) + if not query_result: + return {} + + result = { + item[0]: { + 'play_count': int(item[2][0]), + 'total_time': float(item[2][1]), + 'played_time': float(item[2][2]), + 'played_percent': int(item[2][3]), + 'last_played': str(item[1]), + } for item in query_result + } return result def get_item(self, key): query_result = self._get(key) - result = {} - if query_result: - values = query_result[0].split(',') - result[key] = { - 'play_count': int(values[0]), - 'total_time': float(values[1]), - 'played_time': float(values[2]), - 'played_percent': int(values[3]), - 'last_played': query_result[1], - } + if not query_result: + return {} + + values = query_result[0].split(',') + result = {key: { + 'play_count': int(values[0]), + 'total_time': float(values[1]), + 'played_time': float(values[2]), + 'played_percent': int(values[3]), + 'last_played': str(query_result[1]), + }} return result def clear(self): @@ -69,16 +62,6 @@ def update(self, video_id, play_count, total_time, played_time, played_percent): item = ','.join([str(play_count), str(total_time), str(played_time), str(played_percent)]) self._set(str(video_id), item) - def _set(self, item_id, item): - def _encode(obj): - return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)) - - self._open() - now = datetime.datetime.now() + datetime.timedelta(microseconds=1) # add 1 microsecond, required for dbapi2 - query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name - self._execute(True, query, values=[item_id, now, _encode(item)]) - self._close() - def _optimize_item_count(self): pass diff --git a/resources/lib/youtube_plugin/kodion/utils/search_history.py b/resources/lib/youtube_plugin/kodion/utils/search_history.py index f94d7bfb0..9eae48318 100644 --- a/resources/lib/youtube_plugin/kodion/utils/search_history.py +++ b/resources/lib/youtube_plugin/kodion/utils/search_history.py @@ -15,25 +15,17 @@ class SearchHistory(Storage): - def __init__(self, filename, max_items=10): - super(SearchHistory, self).__init__(filename, max_item_count=max_items) + def __init__(self, filename, max_item_count=10): + super(SearchHistory, self).__init__(filename, + max_item_count=max_item_count) def is_empty(self): return self._is_empty() - def list(self): - result = [] - - keys = self._get_ids(oldest_first=False) - for i, key in enumerate(keys): - if i >= self._max_item_count: - break - item = self._get(key) - - if item: - result.append(item[0]) - - return result + def get_items(self): + result = self._get_by_ids(oldest_first=False, + limit=self._max_item_count) + return [item[2] for item in result] def clear(self): self._clear() diff --git a/resources/lib/youtube_plugin/kodion/utils/storage.py b/resources/lib/youtube_plugin/kodion/utils/storage.py index e764274bd..00efd4983 100644 --- a/resources/lib/youtube_plugin/kodion/utils/storage.py +++ b/resources/lib/youtube_plugin/kodion/utils/storage.py @@ -9,6 +9,7 @@ """ import datetime +import json import os import pickle import sqlite3 @@ -19,12 +20,24 @@ class Storage(object): - def __init__(self, filename, max_item_count=0, max_file_size_kb=-1): - self._table_name = 'storage' + _table_name = 'storage' + _clear_query = 'DELETE FROM %s' % _table_name + _create_table_query = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % _table_name + _get_query = 'SELECT * FROM %s WHERE key = ?' % _table_name + _get_by_query = 'SELECT * FROM %s WHERE key in ({0})' % _table_name + _get_all_asc_query = 'SELECT * FROM %s ORDER BY time ASC LIMIT {0}' % _table_name + _get_all_desc_query = 'SELECT * FROM %s ORDER BY time DESC LIMIT {0}' % _table_name + _is_empty_query = 'SELECT EXISTS(SELECT 1 FROM %s LIMIT 1)' % _table_name + _optimize_item_query = 'SELECT key FROM %s ORDER BY time DESC LIMIT -1 OFFSET {0}' % _table_name + _remove_query = 'DELETE FROM %s WHERE key = ?' % _table_name + _remove_all_query = 'DELETE FROM %s WHERE key in ({0})' % _table_name + _set_query = 'REPLACE INTO %s (key, time, value) VALUES(?, ?, ?)' % _table_name + + def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._filename = filename if not self._filename.endswith('.sqlite'): self._filename = ''.join([self._filename, '.sqlite']) - self._file = None + self._db = None self._cursor = None self._max_item_count = max_item_count self._max_file_size_kb = max_file_size_kb @@ -32,6 +45,8 @@ def __init__(self, filename, max_item_count=0, max_file_size_kb=-1): self._table_created = False self._needs_commit = False + sqlite3.register_converter('timestamp', self._convert_timestamp) + def set_max_item_count(self, max_item_count): self._max_item_count = max_item_count @@ -42,51 +57,63 @@ def __del__(self): self._close() def _open(self): - if self._file is None: - self._optimize_file_size() - - path = os.path.dirname(self._filename) - if not os.path.exists(path): - os.makedirs(path) + if self._db: + return - self._file = sqlite3.connect(self._filename, check_same_thread=False, - detect_types=0, timeout=1) + self._optimize_file_size() - self._file.isolation_level = None - self._cursor = self._file.cursor() - self._cursor.execute('PRAGMA journal_mode=MEMORY') - self._cursor.execute('PRAGMA busy_timeout=20000') - # self._cursor.execute('PRAGMA synchronous=OFF') - self._create_table() + path = os.path.dirname(self._filename) + if not os.path.exists(path): + os.makedirs(path) + + db = sqlite3.connect(self._filename, check_same_thread=False, + detect_types=sqlite3.PARSE_DECLTYPES, + timeout=1, isolation_level=None) + db.row_factory = sqlite3.Row + cursor = db.cursor() + # cursor.execute('PRAGMA journal_mode=MEMORY') + cursor.execute('PRAGMA journal_mode=WAL') + cursor.execute('PRAGMA busy_timeout=20000') + cursor.execute('PRAGMA read_uncommitted=TRUE') + cursor.execute('PRAGMA temp_store=MEMORY') + # cursor.execute('PRAGMA synchronous=OFF') + cursor.execute('PRAGMA synchronous=NORMAL') + cursor.arraysize = 100 + self._db = db + self._cursor = cursor + self._create_table() - def _execute(self, needs_commit, query, values=None): + def _execute(self, needs_commit, query, values=None, many=False): if values is None: - values = [] + values = () if not self._needs_commit and needs_commit: self._needs_commit = True self._cursor.execute('BEGIN') """ - Tests revealed that sqlite has problems to release the database in time. This happens no so often, but just to - be sure, we try at least 3 times to execute out statement. + Tests revealed that sqlite has problems to release the database in time + This happens no so often, but just to be sure, we try at least 3 times + to execute our statement. """ for _ in range(3): try: + if many: + return self._cursor.executemany(query, values) return self._cursor.execute(query, values) except TypeError: - return None + return [] except: time.sleep(0.1) - return None + return [] def _close(self): - if self._file is not None: - self.sync() - self._file.commit() + if self._db: + self._sync() + self._db.commit() self._cursor.close() self._cursor = None - self._file.close() - self._file = None + self._db.close() + self._db = None def _optimize_file_size(self): # do nothing - only we have given a size @@ -109,109 +136,126 @@ def _optimize_file_size(self): pass def _create_table(self): - self._open() - if not self._table_created: - query = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % self._table_name - self._execute(True, query) - self._table_created = True + if self._table_created: + return + self._execute(True, self._create_table_query) + self._table_created = True - def sync(self): - if not self._cursor or not self._needs_commit: + def _sync(self): + if not self._needs_commit: return None self._needs_commit = False return self._execute(False, 'COMMIT') def _set(self, item_id, item): - def _encode(obj): - return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)) + # add 1 microsecond, required for dbapi2 + now = datetime.datetime.now().timestamp() + 0.000001 + self._open() + self._execute(True, self._set_query, values=[item_id, + now, + self._encode(item)]) + self._close() + self._optimize_item_count() - if self._max_file_size_kb < 1 and self._max_item_count < 1: - self._optimize_item_count() - else: - self._open() - now = datetime.datetime.now() + datetime.timedelta(microseconds=1) # add 1 microsecond, required for dbapi2 - query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name - self._execute(True, query, values=[item_id, now, _encode(item)]) - self._close() - self._optimize_item_count() + def _set_all(self, items): + # add 1 microsecond, required for dbapi2 + now = datetime.datetime.now().timestamp() + 0.000001 + self._open() + self._execute(True, self._set_query, + values=[(key, now, self._encode(json.dumps(item))) + for key, item in items.items()], + many=True) + self._close() + self._optimize_item_count() def _optimize_item_count(self): - if self._max_item_count < 1: + if not self._max_item_count: if not self._is_empty(): self._clear() - else: - self._open() - query = 'SELECT key FROM %s ORDER BY time DESC LIMIT -1 OFFSET %d' % (self._table_name, self._max_item_count) - result = self._execute(False, query) - if result is not None: - for item in result: - self._remove(item[0]) - self._close() + return + if self._max_item_count < 0: + return + query = self._optimize_item_query.format(self._max_item_count) + self._open() + item_ids = self._execute(False, query) + item_ids = [item_id['key'] for item_id in item_ids] + if item_ids: + self._remove_all(item_ids) + self._close() def _clear(self): self._open() - query = 'DELETE FROM %s' % self._table_name - self._execute(True, query) + self._execute(True, self._clear_query) self._create_table() - self._close() - self._open() + self._sync() self._execute(False, 'VACUUM') self._close() def _is_empty(self): self._open() - query = 'SELECT exists(SELECT 1 FROM %s LIMIT 1);' % self._table_name - result = self._execute(False, query) - is_empty = True - if result is not None: - for item in result: - is_empty = item[0] == 0 - break + result = self._execute(False, self._is_empty_query) + for item in result: + is_empty = item[0] == 0 + break + else: + is_empty = True self._close() return is_empty - def _get_ids(self, oldest_first=True): - self._open() - # self.sync() - query = 'SELECT key FROM %s' % self._table_name - if oldest_first: - query = '%s ORDER BY time ASC' % query - else: - query = '%s ORDER BY time DESC' % query - - query_result = self._execute(False, query) - - result = [] - if query_result: - for item in query_result: - result.append(item[0]) + @staticmethod + def _decode(obj, process=None): + decoded_obj = pickle.loads(obj, encoding='utf-8') + if process: + return process(decoded_obj) + return decoded_obj - self._close() - return result + @staticmethod + def _encode(obj): + return sqlite3.Binary(pickle.dumps( + obj, protocol=pickle.HIGHEST_PROTOCOL + )) def _get(self, item_id): - def _decode(obj): - return pickle.loads(obj, encoding='utf-8') - self._open() - query = 'SELECT time, value FROM %s WHERE key=?' % self._table_name - result = self._execute(False, query, [item_id]) - if result is None: - self._close() - return None + result = self._execute(False, self._get_query, [item_id]) + if result: + result = result.fetchone() + self._close() + if result: + return self._decode(result['value']), result['time'] + return None - item = result.fetchone() - if item is None: - self._close() - return None + def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, + process=None): + if not item_ids: + if oldest_first: + query = self._get_all_asc_query + else: + query = self._get_all_desc_query + query = query.format(limit) + else: + num_ids = len(item_ids) + query = self._get_by_query.format(('?,' * (num_ids - 1)) + '?') + item_ids = tuple(item_ids) + self._open() + result = self._execute(False, query, item_ids) + result = [ + (item['key'], item['time'], self._decode(item['value'], process)) + for item in result + ] self._close() - return _decode(item[1]), item[0] + return result def _remove(self, item_id): self._open() - query = 'DELETE FROM %s WHERE key = ?' % self._table_name - self._execute(True, query, [item_id]) + self._execute(True, self._remove_query, [item_id]) + + def _remove_all(self, item_ids): + num_ids = len(item_ids) + query = self._remove_all_query.format(('?,' * (num_ids - 1)) + '?') + self._open() + self._execute(True, query, tuple(item_ids)) @staticmethod def strptime(stamp, stamp_fmt): @@ -223,27 +267,50 @@ def strptime(stamp, stamp_fmt): pass return time.strptime(stamp, stamp_fmt) + @classmethod + def _convert_timestamp(cls, val): + val = val.decode('utf-8') + if '-' in val or ':' in val: + return cls._parse_datetime_string(val) + return datetime.datetime.fromtimestamp(float(val)) + + @classmethod + def _parse_datetime_string(cls, current_stamp): + for stamp_format in ['%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S']: + try: + stamp_datetime = datetime.datetime( + *(cls.strptime(current_stamp, stamp_format)[0:6]) + ) + break + except ValueError: # current_stamp has no microseconds + continue + except TypeError: + logger.log_error('Exception while parsing timestamp:\n' + 'current_stamp |{cs}|{cst}|\n' + 'stamp_format |{sf}|{sft}|\n{tb}' + .format(cs=current_stamp, + cst=type(current_stamp), + sf=stamp_format, + sft=type(stamp_format), + tb=traceback.print_exc())) + else: + return None + return stamp_datetime + def get_seconds_diff(self, current_stamp): - stamp_format = '%Y-%m-%d %H:%M:%S.%f' - current_datetime = datetime.datetime.now() if not current_stamp: return 86400 # 24 hrs - try: - stamp_datetime = datetime.datetime(*(self.strptime(current_stamp, stamp_format)[0:6])) - except ValueError: # current_stamp has no microseconds - stamp_format = '%Y-%m-%d %H:%M:%S' - stamp_datetime = datetime.datetime(*(self.strptime(current_stamp, stamp_format)[0:6])) - except TypeError: - logger.log_error('Exception while calculating timestamp difference: ' - 'current_stamp |{cs}|{cst}| stamp_format |{sf}|{sft}| \n{tb}' - .format(cs=current_stamp, cst=type(current_stamp), - sf=stamp_format, sft=type(stamp_format), - tb=traceback.print_exc()) - ) - return 604800 # one week + current_datetime = datetime.datetime.now() + if isinstance(current_stamp, datetime.datetime): + time_delta = current_datetime - current_stamp + return time_delta.total_seconds() + + if isinstance(current_stamp, (float, int)): + return current_datetime.timestamp() - current_stamp + + stamp_datetime = self._parse_datetime_string(current_stamp) + if not stamp_datetime: + return 604800 # one week time_delta = current_datetime - stamp_datetime - total_seconds = 0 - if time_delta: - total_seconds = ((time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) // (10 ** 6) - return total_seconds + return time_delta.total_seconds() diff --git a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py b/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py index 04e21cada..e6a570960 100644 --- a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py @@ -21,21 +21,13 @@ def __init__(self, filename): def clear(self): self._clear() - def list(self): - result = [] + @staticmethod + def _sort_item(_item): + return _item[2].get_date() - for key in self._get_ids(): - data = self._get(key) - item = items.from_json(data[0]) - result.append(item) - - def _sort(video_item): - return video_item.get_date() - - self.sync() - - sorted_list = sorted(result, key=_sort, reverse=False) - return sorted_list + def get_items(self): + result = self._get_by_ids(process=items.from_json) + return sorted(result, key=self._sort_item, reverse=False) def add(self, base_item): now = datetime.datetime.now() diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 03a959ad3..aa03648d4 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -362,7 +362,7 @@ def helper(video_id, responses): # Truncate items to keep it manageable, and cache items = items[:500] - cache.set(cache_items_key, json.dumps(items)) + cache.set_item(cache_items_key, json.dumps(items)) # Build the result set items.sort( @@ -421,7 +421,7 @@ def helper(video_id, responses): } """ # Update cache - cache.set(cache_home_key, json.dumps(payload)) + cache.set_item(cache_home_key, json.dumps(payload)) # If there are no sorted_items we fall back to default API behaviour return payload @@ -867,7 +867,7 @@ def _sort_by_date_time(e): _result['items'].sort(reverse=True, key=_sort_by_date_time) # Update cache - cache.set(cache_items_key, json.dumps(_result['items'])) + cache.set_item(cache_items_key, json.dumps(_result['items'])) """ no cache, get uploads data from web """ # trim result diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index ee62dad50..ca1edcf95 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -82,7 +82,7 @@ def _update_channels(self, channel_ids): if yt_item } result.update(channel_data) - data_cache.set_all(channel_data) + data_cache.set_items(channel_data) self._context.log_debug('Cached data for channels |%s|' % ', '.join(channel_data)) if self.handle_error(json_data): @@ -112,7 +112,7 @@ def _update_videos(self, video_ids, live_details=False, suppress_errors=False): if yt_item } result.update(video_data) - data_cache.set_all(video_data) + data_cache.set_items(video_data) self._context.log_debug('Cached data for videos |%s|' % ', '.join(video_data)) if self._context.get_settings().use_local_history(): @@ -163,7 +163,7 @@ def _update_playlists(self, playlists_ids): if yt_item } result.update(playlist_data) - data_cache.set_all(playlist_data) + data_cache.set_items(playlist_data) self._context.log_debug('Cached data for playlists |%s|' % ', '.join(playlist_data)) if self.handle_error(json_data): diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 4f66dac61..9b46657ee 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -737,7 +737,7 @@ def _get_player_js(self): return '' js_url = self._normalize_url(js_url) - self._data_cache.set('player_js_url', json_dumps({'url': js_url})) + self._data_cache.set_item('player_js_url', json_dumps({'url': js_url})) cache_key = quote(js_url) cached_js = self._data_cache.get_item( @@ -756,7 +756,7 @@ def _get_player_js(self): return '' javascript = result.text - self._data_cache.set(cache_key, json_dumps({'js': javascript})) + self._data_cache.set_item(cache_key, json_dumps({'js': javascript})) return javascript @staticmethod @@ -942,7 +942,7 @@ def _process_signature_cipher(self, stream_map): 'Failed to extract URL from signatureCipher' ) return None - self._data_cache.set( + self._data_cache.set_item( encrypted_signature, json_dumps({'sig': signature}) ) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 21367aed2..f92b017ca 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1382,8 +1382,7 @@ def on_root(self, context, re_match): #clear cache cache = context.get_data_cache() - cache_items_key = 'my-subscriptions-items' - cache.set(cache_items_key, '[]') + cache.set_item('my-subscriptions-items', '[]') my_subscriptions_item = DirectoryItem( context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.my_subscriptions'])), From b748a9fadd57736eb716d508dfa60b07c2d36035 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:20:01 +1100 Subject: [PATCH 031/141] Update listitem creation to fix date sorting - Fixes #411 - Workaround issue with infotagger until updated - Fix resume properties - Update ISA properties --- .../kodion/plugin/xbmc/xbmc_runner.py | 4 +- .../kodion/ui/xbmc/info_labels.py | 8 +- .../kodion/ui/xbmc/xbmc_items.py | 283 ++++++++++-------- 3 files changed, 162 insertions(+), 133 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index dddeca507..4b84d1cab 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -104,7 +104,8 @@ def _add_directory(directory_item, show_fanart=False): item = xbmcgui.ListItem(label=directory_item.get_name(), offscreen=True) - info_tag = xbmc_items.ListItemInfoTag(item, tag_type='video') + info = info_labels.create_from_item(directory_item) + xbmc_items.set_info_tag(item, info, 'video') # only set fanart is enabled if show_fanart: @@ -118,7 +119,6 @@ def _add_directory(directory_item, show_fanart=False): item.addContextMenuItems(directory_item.get_context_menu(), replaceItems=directory_item.replace_context_menu()) - info_tag.set_info(info_labels.create_from_item(directory_item)) item.setPath(directory_item.get_uri()) is_folder = True diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index 86f8f9427..9f779b9aa 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -15,8 +15,7 @@ def _process_date(info_labels, param): if param: datetime = utils.datetime_parser.parse(param) - datetime = '%02d.%02d.%04d' % (datetime.day, datetime.month, datetime.year) - info_labels['date'] = datetime + info_labels['date'] = datetime.isoformat() def _process_int_value(info_labels, name, param): @@ -68,8 +67,7 @@ def _process_video_rating(info_labels, param): def _process_date_value(info_labels, name, param): if param: date = utils.datetime_parser.parse(param) - date = '%04d-%02d-%02d' % (date.year, date.month, date.day) - info_labels[name] = date + info_labels[name] = date.isoformat() def _process_list_value(info_labels, name, param): @@ -92,7 +90,7 @@ def _process_last_played(info_labels, name, param): def create_from_item(base_item): info_labels = {} - # 'date' = '09.03.1982' + # 'date' = '1982-03-09' _process_date(info_labels, base_item.get_date()) # Directory diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index 2cbbe6324..b938c4250 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -8,216 +8,247 @@ See LICENSES/GPL-2.0-only for more information. """ -import xbmcgui +from xbmcgui import ListItem try: - from infotagger.listitem import ListItemInfoTag + from infotagger.listitem import set_info_tag except ImportError: + def set_info_tag(listitem, info, tag_type, *_args, **_kwargs): + listitem.setInfo(tag_type, info) + return ListItemInfoTag(listitem, tag_type) + class ListItemInfoTag(object): - __slots__ = ('__li__', '__type__') + __slots__ = ('__li__', ) - def __init__(self, list_item, tag_type): - self.__li__ = list_item - self.__type__ = tag_type + def __init__(self, listitem, *_args, **_kwargs): + self.__li__ = listitem def add_stream_info(self, *args, **kwargs): return self.__li__.addStreamInfo(*args, **kwargs) - def set_info(self, *args, **kwargs): - return self.__li__.setInfo(self.__type__, *args, **kwargs) - + def set_resume_point(self, *_args, **_kwargs): + pass -from ...items import VideoItem, AudioItem, UriItem -from ... import utils from . import info_labels +from ...items import VideoItem, AudioItem, UriItem +from ...utils import datetime_parser def to_play_item(context, play_item): uri = play_item.get_uri() context.log_debug('Converting PlayItem |%s|' % uri) - is_strm = str(context.get_param('strm', False)).lower() == 'true' - - thumb = play_item.get_image() if play_item.get_image() else 'DefaultVideo.png' - title = play_item.get_title() if play_item.get_title() else play_item.get_name() - fanart = '' settings = context.get_settings() - if is_strm: - list_item = xbmcgui.ListItem(offscreen=True) - else: - list_item = xbmcgui.ListItem(label=utils.to_unicode(title), offscreen=True) - - info_tag = ListItemInfoTag(list_item, tag_type='video') - - if not is_strm: - list_item.setProperty('IsPlayable', 'true') - - if play_item.get_fanart() and settings.show_fanart(): - fanart = play_item.get_fanart() - - list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) - headers = play_item.get_headers() license_key = play_item.get_license_key() alternative_player = settings.is_support_alternative_player_enabled() - - if (alternative_player and settings.alternative_player_web_urls() + is_strm = context.get_param('strm') + mime_type = None + + kwargs = { + 'label': (None if is_strm + else (play_item.get_title() or play_item.get_name())), + 'offscreen': True, + } + props = { + 'isPlayable': 'true', + } + + if (alternative_player + and settings.alternative_player_web_urls() and not license_key): play_item.set_uri('https://www.youtube.com/watch?v={video_id}'.format( video_id=play_item.video_id )) - isa_enabled = settings.use_isa() and context.addon_enabled('inputstream.adaptive') - - if isa_enabled and play_item.use_isa_video(): + elif (play_item.use_isa_video() + and context.addon_enabled('inputstream.adaptive')): if play_item.use_mpd_video(): manifest_type = 'mpd' mime_type = 'application/xml+dash' # MPD manifest update is currently broken # Following line will force a full update but restart live stream from start # if play_item.live: - # list_item.setProperty('inputstream.adaptive.manifest_update_parameter', 'full') + # props['inputstream.adaptive.manifest_update_parameter'] = 'full' if 'auto' in settings.stream_select(): - list_item.setProperty('inputstream.adaptive.stream_selection_type', 'adaptive') + props['inputstream.adaptive.stream_selection_type'] = 'adaptive' else: manifest_type = 'hls' mime_type = 'application/x-mpegURL' - list_item.setContentLookup(False) - list_item.setMimeType(mime_type) - list_item.setProperty('inputstream', 'inputstream.adaptive') - list_item.setProperty('inputstream.adaptive.manifest_type', manifest_type) + props['inputstream'] = 'inputstream.adaptive' + props['inputstream.adaptive.manifest_type'] = manifest_type if headers: - list_item.setProperty('inputstream.adaptive.manifest_headers', headers) - list_item.setProperty('inputstream.adaptive.stream_headers', headers) + props['inputstream.adaptive.manifest_headers'] = headers + props['inputstream.adaptive.stream_headers'] = headers if license_key: - list_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') - list_item.setProperty('inputstream.adaptive.license_key', license_key) + props['inputstream.adaptive.license_type'] = 'com.widevine.alpha' + props['inputstream.adaptive.license_key'] = license_key + else: if 'mime=' in uri: - try: - mime_type = uri.split('mime=', 1)[-1].split('&', 1)[0].replace('%2F', '/', 1) - list_item.setMimeType(mime_type) - list_item.setContentLookup(False) - except: - pass + mime_type = uri.partition('mime=')[2].partition('&')[0].replace('%2F', '/') + if not alternative_player and headers and uri.startswith('http'): play_item.set_uri('|'.join([uri, headers])) - if not is_strm: - if play_item.get_play_count() == 0: - if play_item.get_start_percent(): - list_item.setProperty('StartPercent', str(play_item.get_start_percent())) + list_item = ListItem(**kwargs) + if mime_type: + list_item.setContentLookup(False) + list_item.setMimeType(mime_type) - if play_item.get_start_time(): - list_item.setProperty('StartOffset', str(play_item.get_start_time())) + if is_strm: + return list_item - if play_item.subtitles: - list_item.setSubtitles(play_item.subtitles) + if not context.get_param('resume'): + if 'ResumeTime' in props: + del props['ResumeTime'] - _info_labels = info_labels.create_from_item(play_item) + prop_value = play_item.get_duration() + if prop_value: + props['TotalTime'] = str(prop_value) - # This should work for all versions of XBMC/KODI. - if 'duration' in _info_labels: - duration = _info_labels['duration'] - info_tag.add_stream_info('video', {'duration': duration}) + fanart = settings.show_fanart() and play_item.get_fanart() or '' + thumb = play_item.get_image() or 'DefaultVideo.png' + list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) - info_tag.set_info(_info_labels) + if play_item.subtitles: + list_item.setSubtitles(play_item.subtitles) + + info = info_labels.create_from_item(play_item) + info_tag = set_info_tag(list_item, info, 'video') + info_tag.set_resume_point(props) + + # This should work for all versions of XBMC/KODI. + if 'duration' in info: + info_tag.add_stream_info('video', {'duration': info['duration']}) + + list_item.setProperties(props) return list_item def to_video_item(context, video_item): context.log_debug('Converting VideoItem |%s|' % video_item.get_uri()) - thumb = video_item.get_image() if video_item.get_image() else 'DefaultVideo.png' - title = video_item.get_title() if video_item.get_title() else video_item.get_name() - fanart = '' - settings = context.get_settings() - item = xbmcgui.ListItem(label=utils.to_unicode(title), offscreen=True) - info_tag = ListItemInfoTag(item, tag_type='video') - - if video_item.get_fanart() and settings.show_fanart(): - fanart = video_item.get_fanart() + kwargs = { + 'label': video_item.get_title() or video_item.get_name(), + 'offscreen': True, + } + props = { + 'isPlayable': 'true', + } + + list_item = ListItem(**kwargs) + + published_at = video_item.get_aired_utc() + scheduled_start = video_item.get_scheduled_start_utc() + datetime_string = scheduled_start or published_at + local_datetime = None + if datetime_string: + local_datetime = datetime_parser.utc_to_local(datetime_string) + props['PublishedLocal'] = str(local_datetime) + if video_item.live: + props['PublishedSince'] = context.localize('30539') + elif local_datetime: + props['PublishedSince'] = str(datetime_parser.datetime_to_since( + context, local_datetime + )) - item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) + prop_value = video_item.get_start_time() + if prop_value: + props['ResumeTime'] = str(prop_value) - if video_item.get_context_menu() is not None: - item.addContextMenuItems(video_item.get_context_menu(), replaceItems=video_item.replace_context_menu()) + prop_value = video_item.get_duration() + if prop_value: + props['TotalTime'] = str(prop_value) - item.setProperty('IsPlayable', 'true') - - if not video_item.live: - published_at = video_item.get_aired_utc() - scheduled_start = video_item.get_scheduled_start_utc() - use_dt = scheduled_start or published_at - if use_dt: - local_dt = utils.datetime_parser.utc_to_local(use_dt) - item.setProperty('PublishedSince', - utils.to_unicode(utils.datetime_parser.datetime_to_since(context, local_dt))) - item.setProperty('PublishedLocal', str(local_dt)) - else: - item.setProperty('PublishedSince', context.localize('30539')) + # make channel_id property available for keymapping + prop_value = video_item.get_channel_id() + if prop_value: + props['channel_id'] = prop_value - _info_labels = info_labels.create_from_item(video_item) + # make subscription_id property available for keymapping + prop_value = video_item.get_subscription_id() + if prop_value: + props['subscription_id'] = prop_value - if video_item.get_play_count() == 0: - if video_item.get_start_percent(): - item.setProperty('StartPercent', str(video_item.get_start_percent())) + # make playlist_id property available for keymapping + prop_value = video_item.get_playlist_id() + if prop_value: + props['playlist_id'] = prop_value - if video_item.get_start_time(): - item.setProperty('StartOffset', str(video_item.get_start_time())) + # make playlist_item_id property available for keymapping + prop_value = video_item.get_playlist_item_id() + if prop_value: + props['playlist_item_id'] = prop_value - # This should work for all versions of XBMC/KODI. - if 'duration' in _info_labels: - duration = _info_labels['duration'] - info_tag.add_stream_info('video', {'duration': duration}) + fanart = (context.get_settings().show_fanart() + and video_item.get_fanart() + or '') + thumb = video_item.get_image() or 'DefaultVideo.png' + list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) - info_tag.set_info(_info_labels) + if video_item.subtitles: + list_item.setSubtitles(video_item.subtitles) - if video_item.get_channel_id(): # make channel_id property available for keymapping - item.setProperty('channel_id', video_item.get_channel_id()) + info = info_labels.create_from_item(video_item) + info_tag = set_info_tag(list_item, info, 'video') + info_tag.set_resume_point(props) - if video_item.get_subscription_id(): # make subscription_id property available for keymapping - item.setProperty('subscription_id', video_item.get_subscription_id()) + # This should work for all versions of XBMC/KODI. + if 'duration' in info: + info_tag.add_stream_info('video', {'duration': info['duration']}) - if video_item.get_playlist_id(): # make playlist_id property available for keymapping - item.setProperty('playlist_id', video_item.get_playlist_id()) + list_item.setProperties(props) - if video_item.get_playlist_item_id(): # make playlist_item_id property available for keymapping - item.setProperty('playlist_item_id', video_item.get_playlist_item_id()) + context_menu = video_item.get_context_menu() + if context_menu: + list_item.addContextMenuItems( + context_menu, replaceItems=video_item.replace_context_menu() + ) - return item + return list_item def to_audio_item(context, audio_item): context.log_debug('Converting AudioItem |%s|' % audio_item.get_uri()) - thumb = audio_item.get_image() if audio_item.get_image() else 'DefaultAudio.png' - title = audio_item.get_name() - fanart = '' - settings = context.get_settings() - item = xbmcgui.ListItem(label=utils.to_unicode(title), offscreen=True) - info_tag = ListItemInfoTag(item, tag_type='music') - if audio_item.get_fanart() and settings.show_fanart(): - fanart = audio_item.get_fanart() + kwargs = { + 'label': audio_item.get_title() or audio_item.get_name(), + 'offscreen': True, + } + props = { + 'isPlayable': 'true', + } - item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) + list_item = ListItem(**kwargs) - if audio_item.get_context_menu() is not None: - item.addContextMenuItems(audio_item.get_context_menu(), replaceItems=audio_item.replace_context_menu()) + fanart = (context.get_settings().show_fanart() + and audio_item.get_fanart() + or '') + thumb = audio_item.get_image() or 'DefaultAudio.png' + list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) - item.setProperty('IsPlayable', 'true') + info = info_labels.create_from_item(audio_item) + set_info_tag(list_item, info, 'music') - info_tag.set_info(info_labels.create_from_item(audio_item)) - return item + list_item.setProperties(props) + + context_menu = audio_item.get_context_menu() + if context_menu: + list_item.addContextMenuItems( + context_menu, replaceItems=audio_item.replace_context_menu() + ) + + return list_item def to_uri_item(context, base_item): context.log_debug('Converting UriItem') - item = xbmcgui.ListItem(path=base_item.get_uri(), offscreen=True) + item = ListItem(path=base_item.get_uri(), offscreen=True) item.setProperty('IsPlayable', 'true') return item From 9ba6c09e810bfeed8d371ae7f491f115268b6254 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:22:59 +1100 Subject: [PATCH 032/141] Update stream selection logic for codecs and HDR - Fix #532 - Workaround for ISA not initialising decoder when codec changes within Adaptationset - AV1 and H264 are no longer grouped together in the same AdaptationSet - HDR is grouped separately for ISA stream selection dialog --- .../lib/youtube_plugin/youtube/helper/video_info.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 9b46657ee..d042e2970 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1529,12 +1529,16 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): else: frame_rate = None - mime_group = mime_type + mime_group = '{mime_type}_{codec}{hdr}'.format( + mime_type=mime_type, + codec=codec, + hdr='_hdr' if hdr else '' + ) channels = language = role = role_type = sample_rate = None label = quality['label'].format(fps if fps > 30 else '', ' HDR' if hdr else '', compare_height) - quality_group = '{0}_{1}'.format(container, label) + quality_group = '{0}_{1}_{2}'.format(container, codec, label) if mime_group not in data: data[mime_group] = {} @@ -1599,7 +1603,7 @@ def _group_sort(item): main_stream = streams[0] key = ( - group != main_stream['mimeType'], + not group.startswith(main_stream['mimeType']), ) if main_stream['mediaType'] == 'video' else ( not group.startswith(main_stream['mimeType']), preferred_audio['id'] not in group, From db0d8c18e9a7cda585dcd9cbb794bb2965a3007b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:04:38 +1100 Subject: [PATCH 033/141] Improve robustness of JSONStore - Fix #536 and hopefully prevents it from reoccuring --- .../kodion/json_store/api_keys.py | 4 +- .../kodion/json_store/json_store.py | 103 +++++++++++------- .../kodion/json_store/login_tokens.py | 4 +- 3 files changed, 66 insertions(+), 45 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py index e5a4b8533..aeaf8d0cd 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py +++ b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py @@ -14,8 +14,8 @@ class APIKeyStore(JSONStore): def __init__(self): super(APIKeyStore, self).__init__('api_keys.json') - def set_defaults(self): - data = self.get_data() + def set_defaults(self, reset=False): + data = {} if reset else self.get_data() if 'keys' not in data: data = {'keys': {'personal': {'api_key': '', 'client_id': '', 'client_secret': ''}, 'developer': {}}} if 'personal' not in data['keys']: diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 52b1e06f3..001397737 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -9,13 +9,13 @@ import os import json -from copy import deepcopy import xbmcaddon import xbmcvfs import xbmc -from .. import logger +from ..logger import log_debug, log_error +from ..utils import make_dirs try: @@ -34,53 +34,74 @@ def __init__(self, filename): except AttributeError: self.base_path = xbmc.translatePath(addon.getAddonInfo('profile')) - self.filename = os.path.join(self.base_path, filename) + if not xbmcvfs.exists(self.base_path) and not make_dirs(self.base_path): + log_error('JSONStore.__init__ |{path}| invalid path'.format( + path=self.base_path + )) + return - self._data = None + self.filename = os.path.join(self.base_path, filename) + self._data = {} self.load() self.set_defaults() - def set_defaults(self): + def set_defaults(self, reset=False): raise NotImplementedError def save(self, data): - if data != self._data: - self._data = deepcopy(data) - if not xbmcvfs.exists(self.base_path): - if not self.make_dirs(self.base_path): - logger.log_debug('JSONStore Save |{filename}| failed to create directories.'.format(filename=self.filename.encode("utf-8"))) - return - with open(self.filename, 'w') as jsonfile: - logger.log_debug('JSONStore Save |{filename}|'.format(filename=self.filename.encode("utf-8"))) - json.dump(self._data, jsonfile, indent=4, sort_keys=True) + if data == self._data: + log_debug('JSONStore.save |{filename}| data unchanged'.format( + filename=self.filename + )) + return + log_debug('JSONStore.save |{filename}|'.format( + filename=self.filename + )) + try: + if not data: + raise ValueError + _data = json.loads(json.dumps(data)) + with open(self.filename, mode='w', encoding='utf-8') as jsonfile: + json.dump(_data, jsonfile, indent=4, sort_keys=True) + self._data = _data + except (IOError, OSError): + log_error('JSONStore.save |{filename}| no access to file'.format( + filename=self.filename + )) + return + except (TypeError, ValueError): + log_error('JSONStore.save |{data}| invalid data'.format( + data=data + )) + self.set_defaults(reset=True) def load(self): - if xbmcvfs.exists(self.filename) and xbmcvfs.Stat(self.filename).st_size() > 0: - with open(self.filename, 'r') as jsonfile: - data = json.load(jsonfile) - self._data = data - logger.log_debug('JSONStore Load |{filename}|'.format(filename=self.filename.encode("utf-8"))) - else: - self._data = dict() + log_debug('JSONStore.load |{filename}|'.format( + filename=self.filename + )) + try: + with open(self.filename, mode='r', encoding='utf-8') as jsonfile: + data = jsonfile.read() + if not data: + raise ValueError + self._data = json.loads(data) + except (IOError, OSError): + log_error('JSONStore.load |{filename}| no access to file'.format( + filename=self.filename + )) + except (TypeError, ValueError): + log_error('JSONStore.load |{data}| invalid data'.format( + data=data + )) def get_data(self): - return deepcopy(self._data) - - @staticmethod - def make_dirs(path): - if not path.endswith('/'): - path = ''.join([path, '/']) - path = xbmc.translatePath(path) - if not xbmcvfs.exists(path): - try: - _ = xbmcvfs.mkdirs(path) - except: - pass - if not xbmcvfs.exists(path): - try: - os.makedirs(path) - except: - pass - return xbmcvfs.exists(path) - - return True + try: + if not self._data: + raise ValueError + return json.loads(json.dumps(self._data)) + except (TypeError, ValueError): + log_error('JSONStore.get_data |{data}| invalid data'.format( + data=self._data + )) + self.set_defaults(reset=True) + return json.loads(json.dumps(self._data)) diff --git a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py index 3f691e1ca..e6eb27745 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py +++ b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py @@ -16,8 +16,8 @@ class LoginTokenStore(JSONStore): def __init__(self): super(LoginTokenStore, self).__init__('access_manager.json') - def set_defaults(self): - data = self.get_data() + def set_defaults(self, reset=False): + data = {} if reset else self.get_data() if 'access_manager' not in data: data = {'access_manager': {'users': {'0': {'access_token': '', 'refresh_token': '', 'token_expires': -1, 'last_key_hash': '', 'name': 'Default', 'watch_later': ' WL', 'watch_history': 'HL'}}}} From e67fa3aa19e7732fce7a20dcd285f6171df263ee Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:22:37 +1100 Subject: [PATCH 034/141] Update date processing and display - Remove workarounds for total_seconds() - Fix issues with incorrect date display - Fix issue with storing incorrect date values - Fix mixups with dateadded, date, premiered, etc. - Fixes #464 - Possibly fixes #425, #434 --- .../resource.language.en_au/strings.po | 4 + .../resource.language.en_gb/strings.po | 4 + .../resource.language.en_nz/strings.po | 4 + .../resource.language.en_us/strings.po | 4 + .../kodion/context/xbmc/xbmc_context.py | 14 +- .../youtube_plugin/kodion/items/base_item.py | 30 +++- .../youtube_plugin/kodion/items/video_item.py | 31 ++-- .../lib/youtube_plugin/kodion/service.py | 5 +- .../kodion/ui/xbmc/info_labels.py | 4 +- .../kodion/ui/xbmc/xbmc_items.py | 8 +- .../kodion/utils/datetime_parser.py | 148 ++++++++++-------- .../kodion/utils/function_cache.py | 1 - .../youtube_plugin/youtube/client/youtube.py | 5 +- .../youtube_plugin/youtube/helper/utils.py | 67 +++++--- .../lib/youtube_plugin/youtube/helper/v3.py | 5 +- .../lib/youtube_plugin/youtube/provider.py | 1 + 16 files changed, 198 insertions(+), 137 deletions(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index 7d72626af..fd9e033c3 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -1360,3 +1360,7 @@ msgstr "" msgctxt "#30765" msgid "Requests read timeout" msgstr "" + +msgctxt "#30766" +msgid "Premieres" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 461237005..f1eff0027 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1360,3 +1360,7 @@ msgstr "" msgctxt "#30765" msgid "Requests read timeout" msgstr "" + +msgctxt "#30766" +msgid "Premieres" +msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index 168654a38..03fe7bcd5 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -1356,3 +1356,7 @@ msgstr "" msgctxt "#30765" msgid "Requests read timeout" msgstr "" + +msgctxt "#30766" +msgid "Premieres" +msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index 731014011..770bf552d 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -1361,3 +1361,7 @@ msgstr "" msgctxt "#30765" msgid "Requests read timeout" msgstr "" + +msgctxt "#30766" +msgid "Premieres" +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index e5bdd7ac4..aa0c6c9bc 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -98,7 +98,13 @@ def addon(self): def is_plugin_path(self, uri, uri_path): return uri.startswith('plugin://%s/%s/' % (self.get_id(), uri_path)) - def format_date_short(self, date_obj): + @staticmethod + def format_date_short(date_obj, short_isoformat=False): + if short_isoformat: + if isinstance(date_obj, datetime.datetime): + date_obj = date_obj.date() + return date_obj.isoformat() + date_format = xbmc.getRegion('dateshort') _date_obj = date_obj if isinstance(_date_obj, datetime.date): @@ -106,7 +112,11 @@ def format_date_short(self, date_obj): return _date_obj.strftime(date_format) - def format_time(self, time_obj): + @staticmethod + def format_time(time_obj, short_isoformat=False): + if short_isoformat: + return '{:02d}:{:02d}'.format(time_obj.hour, time_obj.minute) + time_format = xbmc.getRegion('time') _time_obj = time_obj if isinstance(_time_obj, datetime.time): diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index d3b8572e2..1ff804c51 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -35,7 +35,9 @@ def __init__(self, name, uri, image='', fanart=''): self._fanart = fanart self._context_menu = None self._replace_context_menu = False + self._added_utc = None self._date = None + self._dateadded = None self._next_page = False @@ -103,12 +105,36 @@ def set_date(self, year, month, day, hour=0, minute=0, second=0): self._date = date.isoformat(sep=' ') def set_date_from_datetime(self, date_time): - self.set_date(year=date_time.year, month=date_time.month, day=date_time.day, hour=date_time.hour, - minute=date_time.minute, second=date_time.second) + self.set_date(year=date_time.year, + month=date_time.month, + day=date_time.day, + hour=date_time.hour, + minute=date_time.minute, + second=date_time.second) + + def set_dateadded(self, year, month, day, hour=0, minute=0, second=0): + date = datetime.datetime(year, month, day, hour, minute, second) + self._dateadded = date.isoformat(sep=' ') + + def set_dateadded_from_datetime(self, date_time): + self.set_dateadded(year=date_time.year, + month=date_time.month, + day=date_time.day, + hour=date_time.hour, + minute=date_time.minute, + second=date_time.second) + + def set_added_utc(self, dt): + self._added_utc = dt + + def get_added_utc(self): + return self._added_utc def get_date(self): return self._date + def get_dateadded(self): + return self._dateadded @property def next_page(self): return self._next_page diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 65e3579ef..5aca523a6 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -23,7 +23,6 @@ def __init__(self, name, uri, image='', fanart=''): super(VideoItem, self).__init__(name, uri, image, fanart) self._genre = None self._aired = None - self._aired_utc = None self._scheduled_start_utc = None self._duration = None self._director = None @@ -46,6 +45,7 @@ def __init__(self, name, uri, image='', fanart=''): self._start_percent = None self._start_time = None self._live = False + self._upcoming = False self.subtitles = None self._headers = None self.license_key = None @@ -186,12 +186,6 @@ def set_aired(self, year, month, day): date = datetime.date(year, month, day) self._aired = date.isoformat() - def set_aired_utc(self, dt): - self._aired_utc = dt - - def get_aired_utc(self): - return self._aired_utc - def set_aired_from_datetime(self, date_time): self.set_aired(year=date_time.year, month=date_time.month, @@ -211,6 +205,14 @@ def live(self): def live(self, value): self._live = value + @property + def upcoming(self): + return self._upcoming + + @upcoming.setter + def upcoming(self, value): + self._upcoming = value + def get_aired(self): return self._aired @@ -220,21 +222,6 @@ def set_genre(self, genre): def get_genre(self): return self._genre - def set_date(self, year, month, day, hour=0, minute=0, second=0): - date = datetime.datetime(year, month, day, hour, minute, second) - self._date = date.isoformat(sep=' ') - - def set_date_from_datetime(self, date_time): - self.set_date(year=date_time.year, - month=date_time.month, - day=date_time.day, - hour=date_time.hour, - minute=date_time.minute, - second=date_time.second) - - def get_date(self): - return self._date - def set_isa_video(self, value=True): self._uses_isa = value diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index 163f1375c..5c75b7d3b 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -38,10 +38,9 @@ def get_stamp_diff(current_stamp): stamp_datetime = datetime(*(strptime(current_stamp, stamp_format)[0:6])) time_delta = current_datetime - stamp_datetime - total_seconds = 0 if time_delta: - total_seconds = ((time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) // (10 ** 6) - return total_seconds + return time_delta.total_seconds() + return 0 def run(): diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index 9f779b9aa..13601ddf2 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -130,8 +130,8 @@ def create_from_item(base_item): # 'artist' = [] (list) _process_list_value(info_labels, 'artist', base_item.get_artist()) - # 'dateadded' = '2014-08-11 13:08:56' (string) will be taken from 'date' - _process_video_dateadded(info_labels, base_item.get_date()) + # 'dateadded' = '2014-08-11 13:08:56' (string) will be taken from 'dateadded' + _process_video_dateadded(info_labels, base_item.get_dateadded()) # TODO: starting with Helix this could be seconds # 'duration' = '3:18' (string) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index b938c4250..67ad59afa 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -143,12 +143,12 @@ def to_video_item(context, video_item): list_item = ListItem(**kwargs) - published_at = video_item.get_aired_utc() + published_at = video_item.get_added_utc() scheduled_start = video_item.get_scheduled_start_utc() - datetime_string = scheduled_start or published_at + datetime = scheduled_start or published_at local_datetime = None - if datetime_string: - local_datetime = datetime_parser.utc_to_local(datetime_string) + if datetime: + local_datetime = datetime_parser.utc_to_local(datetime) props['PublishedLocal'] = str(local_datetime) if video_item.live: props['PublishedSince'] = context.localize('30539') diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 41b7bf4c1..b547bd228 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -10,17 +10,23 @@ import re import time -from datetime import date, datetime, timedelta -from datetime import time as dt_time +from datetime import date, datetime, time as dt_time, timedelta from ..exceptions import KodionException + __RE_MATCH_TIME_ONLY__ = re.compile(r'^(?P[0-9]{2})([:]?(?P[0-9]{2})([:]?(?P[0-9]{2}))?)?$') __RE_MATCH_DATE_ONLY__ = re.compile(r'^(?P[0-9]{4})[-]?(?P[0-9]{2})[-]?(?P[0-9]{2})$') __RE_MATCH_DATETIME__ = re.compile(r'^(?P[0-9]{4})[-]?(?P[0-9]{2})[-]?(?P[0-9]{2})["T ](?P[0-9]{2})[:]?(?P[0-9]{2})[:]?(?P[0-9]{2})') __RE_MATCH_PERIOD__ = re.compile(r'P((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?') __RE_MATCH_ABBREVIATED__ = re.compile(r'(\w+), (?P\d+) (?P\w+) (?P\d+) (?P\d+):(?P\d+):(?P\d+)') +now = time.time() +__LOCAL_OFFSET__ = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) + +__EPOCH_DT__ = datetime.fromtimestamp(0) + + now = datetime.now @@ -28,8 +34,8 @@ def py2_utf8(text): return text -def parse(datetime_string, localize=True): - _utc_to_local = utc_to_local if localize else lambda x: x +def parse(datetime_string, as_utc=True): + offset = 0 if as_utc else None def _to_int(value): if value is None: @@ -39,28 +45,38 @@ def _to_int(value): # match time only '00:45:10' time_only_match = __RE_MATCH_TIME_ONLY__.match(datetime_string) if time_only_match: - return _utc_to_local(datetime.combine(date.today(), - dt_time(hour=_to_int(time_only_match.group('hour')), - minute=_to_int(time_only_match.group('minute')), - second=_to_int(time_only_match.group('second')))) - ).time() + return utc_to_local( + dt=datetime.combine( + date.today(), + dt_time(hour=_to_int(time_only_match.group('hour')), + minute=_to_int(time_only_match.group('minute')), + second=_to_int(time_only_match.group('second'))) + ), + offset=offset + ).time() # match date only '2014-11-08' date_only_match = __RE_MATCH_DATE_ONLY__.match(datetime_string) if date_only_match: - return _utc_to_local(date(_to_int(date_only_match.group('year')), - _to_int(date_only_match.group('month')), - _to_int(date_only_match.group('day')))) + return utc_to_local( + dt=date(_to_int(date_only_match.group('year')), + _to_int(date_only_match.group('month')), + _to_int(date_only_match.group('day'))), + offset=offset + ) # full date time date_time_match = __RE_MATCH_DATETIME__.match(datetime_string) if date_time_match: - return _utc_to_local(datetime(_to_int(date_time_match.group('year')), - _to_int(date_time_match.group('month')), - _to_int(date_time_match.group('day')), - _to_int(date_time_match.group('hour')), - _to_int(date_time_match.group('minute')), - _to_int(date_time_match.group('second')))) + return utc_to_local( + dt=datetime(_to_int(date_time_match.group('year')), + _to_int(date_time_match.group('month')), + _to_int(date_time_match.group('day')), + _to_int(date_time_match.group('hour')), + _to_int(date_time_match.group('minute')), + _to_int(date_time_match.group('second'))), + offset=offset + ) # period - at the moment we support only hours, minutes and seconds (e.g. videos and audio) period_match = __RE_MATCH_PERIOD__.match(datetime_string) @@ -74,40 +90,40 @@ def _to_int(value): if abbreviated_match: month = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'June': 6, 'Jun': 6, 'July': 7, 'Jul': 7, 'Aug': 8, 'Sept': 9, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} - return _utc_to_local(datetime(year=_to_int(abbreviated_match.group('year')), - month=month[abbreviated_match.group('month')], - day=_to_int(abbreviated_match.group('day')), - hour=_to_int(abbreviated_match.group('hour')), - minute=_to_int(abbreviated_match.group('minute')), - second=_to_int(abbreviated_match.group('second')))) + return utc_to_local( + dt=datetime(year=_to_int(abbreviated_match.group('year')), + month=month[abbreviated_match.group('month')], + day=_to_int(abbreviated_match.group('day')), + hour=_to_int(abbreviated_match.group('hour')), + minute=_to_int(abbreviated_match.group('minute')), + second=_to_int(abbreviated_match.group('second'))), + offset=offset + ) raise KodionException("Could not parse iso 8601 timestamp '%s'" % datetime_string) -def get_scheduled_start(datetime_object, localize=True): - start_hour = '{:02d}'.format(datetime_object.hour) - start_minute = '{:<02d}'.format(datetime_object.minute) - start_time = ':'.join([start_hour, start_minute]) - start_date = str(datetime_object.date()) - if localize: - now = datetime.now() - else: - now = datetime.utcnow() - start_date = start_date.replace(str(now.year), '').lstrip('-') - start_date = start_date.replace('-'.join(['{:02d}'.format(now.month), '{:02d}'.format(now.day)]), '') - return start_date, start_time - - -local_timezone_offset = None - +def get_scheduled_start(context, datetime_object, local=True): + now = datetime.now() if local else datetime.utcnow() + if datetime_object.date() == now.date(): + return '@ {start_time}'.format( + start_time=context.format_time( + datetime_object.timetz(), short_isoformat=True + ) + ) + return '@ {start_date}, {start_time}'.format( + start_time=context.format_time( + datetime_object.timetz(), short_isoformat=True + ), + start_date=context.format_date_short( + datetime_object.date(), short_isoformat=True + ) + ) -def utc_to_local(dt): - global local_timezone_offset - if local_timezone_offset is None: - now = time.time() - local_timezone_offset = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) - return dt + local_timezone_offset +def utc_to_local(dt, offset=None): + offset = __LOCAL_OFFSET__ if offset is None else timedelta(hours=offset) + return dt + offset def datetime_to_since(context, dt): @@ -115,47 +131,47 @@ def datetime_to_since(context, dt): diff = now - dt yesterday = now - timedelta(days=1) yyesterday = now - timedelta(days=2) - use_yesterday = total_seconds(now - yesterday) > 10800 + use_yesterday = (now - yesterday).total_seconds() > 10800 today = now.date() tomorrow = today + timedelta(days=1) - seconds = total_seconds(diff) + seconds = diff.total_seconds() if seconds > 0: if seconds < 60: return py2_utf8(context.localize('30676')) - elif 60 <= seconds < 120: + if 60 <= seconds < 120: return py2_utf8(context.localize('30677')) - elif 120 <= seconds < 3600: + if 120 <= seconds < 3600: return py2_utf8(context.localize('30678')) - elif 3600 <= seconds < 7200: + if 3600 <= seconds < 7200: return py2_utf8(context.localize('30679')) - elif 7200 <= seconds < 10800: + if 7200 <= seconds < 10800: return py2_utf8(context.localize('30680')) - elif 10800 <= seconds < 14400: + if 10800 <= seconds < 14400: return py2_utf8(context.localize('30681')) - elif use_yesterday and dt.date() == yesterday.date(): + if use_yesterday and dt.date() == yesterday.date(): return ' '.join([py2_utf8(context.localize('30682')), context.format_time(dt)]) - elif dt.date() == yyesterday.date(): + if dt.date() == yyesterday.date(): return py2_utf8(context.localize('30683')) - elif 5400 <= seconds < 86400: + if 5400 <= seconds < 86400: return ' '.join([py2_utf8(context.localize('30684')), context.format_time(dt)]) - elif 86400 <= seconds < 172800: + if 86400 <= seconds < 172800: return ' '.join([py2_utf8(context.localize('30682')), context.format_time(dt)]) else: seconds *= -1 if seconds < 60: return py2_utf8(context.localize('30691')) - elif 60 <= seconds < 120: + if 60 <= seconds < 120: return py2_utf8(context.localize('30692')) - elif 120 <= seconds < 3600: + if 120 <= seconds < 3600: return py2_utf8(context.localize('30693')) - elif 3600 <= seconds < 7200: + if 3600 <= seconds < 7200: return py2_utf8(context.localize('30694')) - elif 7200 <= seconds < 10800: + if 7200 <= seconds < 10800: return py2_utf8(context.localize('30695')) - elif dt.date() == today: + if dt.date() == today: return ' '.join([py2_utf8(context.localize('30696')), context.format_time(dt)]) - elif dt.date() == tomorrow: + if dt.date() == tomorrow: return ' '.join([py2_utf8(context.localize('30697')), context.format_time(dt)]) return ' '.join([context.format_date_short(dt), context.format_time(dt)]) @@ -178,9 +194,5 @@ def strptime(s, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): return datetime(*time.strptime(s, fmt)[:6]) -def total_seconds(t_delta): # required for python 2.6 which doesn't have datetime.timedelta.total_seconds - return 24 * 60 * 60 * t_delta.days + t_delta.seconds + (t_delta.microseconds // 1000000.) - - def since_epoch(dt_object): - return total_seconds(dt_object - datetime(1970, 1, 1)) + return (dt_object - __EPOCH_DT__).total_seconds() diff --git a/resources/lib/youtube_plugin/kodion/utils/function_cache.py b/resources/lib/youtube_plugin/kodion/utils/function_cache.py index 7218a8ba8..ea0b7c03f 100644 --- a/resources/lib/youtube_plugin/kodion/utils/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/utils/function_cache.py @@ -101,7 +101,6 @@ def get(self, seconds, func, *args, **keywords): diff_seconds = 0 if cached_time is not None: - # this is so stupid, but we have the function 'total_seconds' only starting with python 2.7 diff_seconds = self.get_seconds_diff(cached_time) if cached_data is None or diff_seconds > seconds: diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index aa03648d4..5943a012b 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -400,13 +400,10 @@ def helper(video_id, responses): items.append(item) # Finally sort items per page by date for a better distribution - now = datetime_parser.now() sorted_items.sort( key=lambda a: ( a['page_number'], - datetime_parser.total_seconds( - now - datetime_parser.parse(a['snippet']['publishedAt']) - ) + -datetime_parser.parse(a['snippet']['publishedAt']).timestamp() ), ) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 70f6e688b..df23b1291 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -245,7 +245,7 @@ def update_playlist_infos(provider, context, playlist_id_dict, def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict=None, channel_items_dict=None, - live_details=False, + live_details=True, use_play_data=True, data=None): video_ids = list(video_id_dict) @@ -283,7 +283,9 @@ def update_video_infos(provider, context, video_id_dict, snippet = yt_item['snippet'] # crash if not conform play_data = use_play_data and yt_item.get('play_data') - video_item.live = snippet.get('liveBroadcastContent') == 'live' + broadcast_type = snippet.get('liveBroadcastContent') + video_item.live = broadcast_type == 'live' + video_item.upcoming = broadcast_type == 'upcoming' # duration if not video_item.live and play_data and 'total_time' in play_data: @@ -311,21 +313,34 @@ def update_video_infos(provider, context, video_id_dict, elif video_item.live: video_item.set_play_count(0) - scheduled_start = yt_item.get('liveStreamingDetails', {}).get('scheduledStartTime') - if scheduled_start: - datetime = utils.datetime_parser.parse(scheduled_start) + if ((video_item.live or video_item.upcoming) + and 'liveStreamingDetails' in yt_item): + start_at = yt_item['liveStreamingDetails'].get('scheduledStartTime') + else: + start_at = None + if start_at: + datetime = utils.datetime_parser.parse(start_at, as_utc=True) video_item.set_scheduled_start_utc(datetime) - start_date, start_time = utils.datetime_parser.get_scheduled_start(datetime) - if start_date: - title = '({live} {date}@{time}) {title}' \ - .format(live=context.localize(provider.LOCAL_MAP['youtube.live']), date=start_date, time=start_time, title=snippet['title']) - else: - title = '({live} @ {time}) {title}' \ - .format(live=context.localize(provider.LOCAL_MAP['youtube.live']), time=start_time, title=snippet['title']) - video_item.set_title(title) - # set the title - elif not video_item.get_title(): - video_item.set_title(snippet['title']) + local_datetime = utils.datetime_parser.utc_to_local(datetime) + video_item.set_year_from_datetime(local_datetime) + video_item.set_aired_from_datetime(local_datetime) + video_item.set_premiered_from_datetime(local_datetime) + video_item.set_date_from_datetime(local_datetime) + type_label = context.localize(provider.LOCAL_MAP[ + 'youtube.live' if video_item.live else 'youtube.upcoming' + ]) + start_at = '{type_label} {start_at}'.format( + type_label=type_label, + start_at=utils.datetime_parser.get_scheduled_start( + context, local_datetime + ) + ) + + # update and set the title + title = video_item.get_title() or snippet['title'] or '' + if video_item.upcoming: + title = ui.italic(title) + video_item.set_title(title) """ This is experimental. We try to get the most information out of the title of a video. @@ -355,15 +370,17 @@ def update_video_infos(provider, context, video_id_dict, video_item.set_plot(description) # date time - if not datetime and 'publishedAt' in snippet and snippet['publishedAt']: - datetime = utils.datetime_parser.parse(snippet['publishedAt']) - video_item.set_aired_utc(utils.datetime_parser.strptime(snippet['publishedAt'])) - - if datetime: - video_item.set_year_from_datetime(datetime) - video_item.set_aired_from_datetime(datetime) - video_item.set_premiered_from_datetime(datetime) - video_item.set_date_from_datetime(datetime) + published_at = snippet.get('publishedAt') + if published_at: + datetime = utils.datetime_parser.parse(published_at, as_utc=True) + video_item.set_added_utc(datetime) + local_datetime = utils.datetime_parser.utc_to_local(datetime) + video_item.set_dateadded_from_datetime(local_datetime) + if not start_at: + video_item.set_year_from_datetime(local_datetime) + video_item.set_aired_from_datetime(local_datetime) + video_item.set_premiered_from_datetime(local_datetime) + video_item.set_date_from_datetime(local_datetime) # try to find a better resolution for the image image = video_item.get_image() diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 3a5c1a7b3..e7fd3e429 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -23,8 +23,6 @@ def _process_list_response(provider, context, json_data): result = [] - is_upcoming = False - thumb_size = context.get_settings().use_thumbnail_size() yt_items = json_data.get('items', []) if not yt_items: @@ -208,7 +206,6 @@ def _process_list_response(provider, context, json_data): if kind == 'video': video_id = yt_item['id']['videoId'] snippet = yt_item.get('snippet', {}) - is_upcoming = snippet.get('liveBroadcastContent', '').lower() == 'upcoming' title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} @@ -271,7 +268,7 @@ def _process_list_response(provider, context, json_data): # this will also update the channel_id_dict with the correct channel id for each video. channel_items_dict = {} utils.update_video_infos(provider, context, video_id_dict, playlist_item_id_dict, channel_items_dict, - live_details=is_upcoming, use_play_data=use_play_data) + live_details=True, use_play_data=use_play_data) utils.update_playlist_infos(provider, context, playlist_id_dict, channel_items_dict) utils.update_channel_infos(provider, context, channel_id_dict, subscription_id_dict, channel_items_dict) if video_id_dict or playlist_id_dict: diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index f92b017ca..f4eb1dba3 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -104,6 +104,7 @@ class Provider(AbstractProvider): 'youtube.playlist.play.from_here': 30537, 'youtube.video.disliked': 30538, 'youtube.live': 30539, + 'youtube.upcoming': 30766, 'youtube.video.play_with': 30540, 'youtube.error.rtmpe_not_supported': 30542, 'youtube.refresh': 30543, From 940a106bd5d2bfa9731b5a02467382dc5761b45d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:49:20 +1100 Subject: [PATCH 035/141] Update label formatting methods - Adds new format: - [LIGHT]blah[/LIGHT] - [I]blah[/I] - [TABS]x[/TABS]blah - [CR]blah and/or blah[CR] - Optional [CR] to other format methods --- .../kodion/ui/xbmc/xbmc_context_ui.py | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index b8cdb0185..2e03d801a 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -169,16 +169,62 @@ def clear_home_window_property(property_id): xbmcgui.Window(10000).clearProperty(property_id) @staticmethod - def bold(value): - return ''.join(['[B]', value, '[/B]']) + def bold(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[B]', value, '[/B]', + '[CR]' * cr_after, + )) @staticmethod - def uppercase(value): - return ''.join(['[UPPERCASE]', value, '[/UPPERCASE]']) + def uppercase(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[UPPERCASE]', value, '[/UPPERCASE]', + '[CR]' * cr_after, + )) @staticmethod - def color(color, value): - return ''.join(['[COLOR=', color.lower(), ']', value, '[/COLOR]']) + def color(color, value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[COLOR=', color.lower(), ']', value, '[/COLOR]', + '[CR]' * cr_after, + )) + + @staticmethod + def light(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[LIGHT]', value, '[/LIGHT]', + '[CR]' * cr_after, + )) + + @staticmethod + def italic(value, cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[I]', value, '[/I]', + '[CR]' * cr_after, + )) + + @staticmethod + def indent(number=1, value='', cr_before=0, cr_after=0): + return ''.join(( + '[CR]' * cr_before, + '[TABS]', str(number), '[/TABS]', value, + '[CR]' * cr_after, + )) + + @staticmethod + def new_line(value=1, cr_before=0, cr_after=0): + if isinstance(value, int): + return '[CR]' * value + return ''.join(( + '[CR]' * cr_before, + value, + '[CR]' * cr_after, + )) def set_focus_next_item(self): cid = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId() From c542880f4f08894251f8c6748a27f7787a4b118f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:17:26 +1100 Subject: [PATCH 036/141] Retrieve and display video stats - Existing "Show channel name in description" option changed to "Show channel name and video details in description" - Displays number of views, likes, comments in description if enabled - Displays stats and premiere datetime in label2 - Closes #503, #18 --- .../resource.language.en_au/strings.po | 6 +++- .../resource.language.en_gb/strings.po | 6 +++- .../resource.language.en_nz/strings.po | 6 +++- .../resource.language.en_us/strings.po | 6 +++- .../kodion/constants/const_settings.py | 1 + .../youtube_plugin/kodion/items/base_item.py | 8 +++++ .../kodion/settings/abstract_settings.py | 3 ++ .../kodion/ui/xbmc/xbmc_items.py | 3 ++ .../youtube_plugin/kodion/utils/__init__.py | 6 ++-- .../youtube_plugin/kodion/utils/methods.py | 29 ++++++++++++++++-- .../youtube_plugin/youtube/client/youtube.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 30 +++++++++++++++++-- .../lib/youtube_plugin/youtube/provider.py | 4 +++ resources/settings.xml | 2 +- 14 files changed, 99 insertions(+), 13 deletions(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index fd9e033c3..783ff9c92 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -462,7 +462,7 @@ msgid "Play with..." msgstr "" msgctxt "#30541" -msgid "Show channel name in description" +msgid "Show channel name and video details in description" msgstr "" msgctxt "#30542" @@ -1364,3 +1364,7 @@ msgstr "" msgctxt "#30766" msgid "Premieres" msgstr "" + +msgctxt "#30767" +msgid "Views" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index f1eff0027..f8007413a 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -462,7 +462,7 @@ msgid "Play with..." msgstr "" msgctxt "#30541" -msgid "Show channel name in description" +msgid "Show channel name and video details in description" msgstr "" msgctxt "#30542" @@ -1364,3 +1364,7 @@ msgstr "" msgctxt "#30766" msgid "Premieres" msgstr "" + +msgctxt "#30767" +msgid "Views" +msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index 03fe7bcd5..b1e21fa84 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -458,7 +458,7 @@ msgid "Play with..." msgstr "" msgctxt "#30541" -msgid "Show channel name in description" +msgid "Show channel name and video details in description" msgstr "" msgctxt "#30542" @@ -1360,3 +1360,7 @@ msgstr "" msgctxt "#30766" msgid "Premieres" msgstr "" + +msgctxt "#30767" +msgid "Views" +msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index 770bf552d..2248415ba 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -463,7 +463,7 @@ msgid "Play with..." msgstr "" msgctxt "#30541" -msgid "Show channel name in description" +msgid "Show channel name and video details in description" msgstr "" msgctxt "#30542" @@ -1365,3 +1365,7 @@ msgstr "" msgctxt "#30766" msgid "Premieres" msgstr "" + +msgctxt "#30767" +msgid "Views" +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 1f13d7468..25a61cc0a 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -26,6 +26,7 @@ USE_REMOTE_HISTORY = 'kodion.history.remote' # (bool) REMOTE_FRIENDLY_SEARCH = 'youtube.search.remote.friendly' # (bool) HIDE_SHORT_VIDEOS = 'youtube.hide_shorts' # (bool) +DETAILED_DESCRIPTION = 'youtube.view.description.details' # (bool) SUPPORT_ALTERNATIVE_PLAYER = 'kodion.support.alternative_player' # (bool) ALTERNATIVE_PLAYER_WEB_URLS = 'kodion.alternative_player.web.urls' # (bool) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 1ff804c51..02c96ff79 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -38,6 +38,7 @@ def __init__(self, name, uri, image='', fanart=''): self._added_utc = None self._date = None self._dateadded = None + self._short_details = None self._next_page = False @@ -135,6 +136,13 @@ def get_date(self): def get_dateadded(self): return self._dateadded + + def get_short_details(self): + return self._short_details + + def set_short_details(self, details): + self._short_details = details or '' + @property def next_page(self): return self._next_page diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index b4d975f45..5a461224f 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -314,3 +314,6 @@ def hide_short_videos(self): def client_selection(self): return self.get_int(SETTINGS.CLIENT_SELECTION, 0) + + def show_detailed_description(self): + return self.get_bool(SETTINGS.DETAILED_DESCRIPTION, True) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index 67ad59afa..c7fe95dd9 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -48,6 +48,7 @@ def to_play_item(context, play_item): kwargs = { 'label': (None if is_strm else (play_item.get_title() or play_item.get_name())), + 'label2': None if is_strm else play_item.get_short_details(), 'offscreen': True, } props = { @@ -135,6 +136,7 @@ def to_video_item(context, video_item): kwargs = { 'label': video_item.get_title() or video_item.get_name(), + 'label2': video_item.get_short_details(), 'offscreen': True, } props = { @@ -218,6 +220,7 @@ def to_audio_item(context, audio_item): kwargs = { 'label': audio_item.get_title() or audio_item.get_name(), + 'label2': audio_item.get_short_details(), 'offscreen': True, } props = { diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index f74045045..fe5c07d99 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -14,6 +14,7 @@ create_uri_path, find_best_fit, find_video_id, + friendly_number, loose_version, make_dirs, select_stream, @@ -33,12 +34,13 @@ from .system_version import SystemVersion -__all__ = [ +__all__ = ( 'create_path', 'create_uri_path', 'datetime_parser', 'find_best_fit', 'find_video_id', + 'friendly_number', 'loose_version', 'make_dirs', 'select_stream', @@ -55,4 +57,4 @@ 'WatchLaterList', 'YouTubeMonitor', 'YouTubePlayer' -] +) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index dc5df817f..a4d506e88 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -11,6 +11,7 @@ import os import copy import re +from math import floor, log from urllib.parse import quote from ..constants import localize @@ -19,8 +20,21 @@ import xbmcvfs -__all__ = ['create_path', 'create_uri_path', 'strip_html_from_text', 'print_items', 'find_best_fit', 'to_utf8', - 'to_str', 'to_unicode', 'select_stream', 'make_dirs', 'loose_version', 'find_video_id'] +__all__ = ( + 'create_path', + 'create_uri_path', + 'find_best_fit', + 'find_video_id', + 'friendly_number', + 'loose_version', + 'make_dirs', + 'print_items', + 'select_stream', + 'strip_html_from_text', + 'to_str', + 'to_unicode', + 'to_utf8', +) try: @@ -251,3 +265,14 @@ def find_video_id(plugin_path): if match: return match.group('video_id') return '' + + +def friendly_number(number, precision=3, scale=('', 'K', 'M', 'B')): + _input = float('{input:.{precision}g}'.format( + input=float(number), precision=precision + )) + _abs_input = abs(_input) + magnitude = 0 if _abs_input < 1000 else int(log(floor(_abs_input), 1000)) + return '{output:f}'.format( + output=_input / 1000 ** magnitude + ).rstrip('0').rstrip('.') + scale[magnitude] diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 5943a012b..fdb04295c 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -551,7 +551,7 @@ def get_videos(self, video_id, live_details=False): if isinstance(video_id, list): video_id = ','.join(video_id) - parts = ['snippet,contentDetails,status'] + parts = ['snippet,contentDetails,status,statistics'] if live_details: parts.append(',liveStreamingDetails') diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index df23b1291..5a11bb1f5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -265,7 +265,7 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict = {} settings = context.get_settings() - show_channel_name = settings.get_bool('youtube.view.description.show_channel_name', True) + show_details = settings.show_detailed_description() alternate_player = settings.is_support_alternative_player_enabled() thumb_size = settings.use_thumbnail_size() thumb_stamp = get_thumb_timestamp() @@ -342,6 +342,24 @@ def update_video_infos(provider, context, video_id_dict, title = ui.italic(title) video_item.set_title(title) + stats = [] + if 'statistics' in yt_item: + for stat, value in yt_item['statistics'].items(): + label = provider.LOCAL_MAP.get('youtube.stats.' + stat) + if label: + stats.append('{value} {name}'.format( + name=context.localize(label).lower(), + value=utils.friendly_number(value) + )) + stats = ', '.join(stats) + + # Used for label2, but is poorly supported in skins + video_details = ' | '.join((detail for detail in ( + ui.light(stats) if stats else '', + ui.italic(start_at) if start_at else '', + ) if detail)) + video_item.set_short_details(video_details) + """ This is experimental. We try to get the most information out of the title of a video. This is not based on any language. In some cases this won't work at all. @@ -362,8 +380,14 @@ def update_video_infos(provider, context, video_id_dict, # plot channel_name = snippet.get('channelTitle', '') description = utils.strip_html_from_text(snippet['description']) - if show_channel_name and channel_name: - description = '%s[CR][CR]%s' % (ui.uppercase(ui.bold(channel_name)), description) + if show_details: + description = ''.join(( + ui.bold(channel_name, cr_after=2) if channel_name else '', + ui.light(stats, cr_after=1) if stats else '', + ui.italic(start_at, cr_after=1) if start_at else '', + ui.new_line() if stats or start_at else '', + description, + )) video_item.set_studio(channel_name) # video_item.add_cast(channel_name) video_item.add_artist(channel_name) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index f4eb1dba3..a5a9c433c 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -200,6 +200,10 @@ class Provider(AbstractProvider): 'youtube.video.comments.likes': 30733, 'youtube.video.comments.replies': 30734, 'youtube.video.comments.edited': 30735, + 'youtube.stats.viewCount': 30767, + 'youtube.stats.likeCount': 30733, + # 'youtube.stats.favoriteCount': 30100, + 'youtube.stats.commentCount': 30732, } def __init__(self): diff --git a/resources/settings.xml b/resources/settings.xml index ae4d15438..e6ee7a6e6 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -669,7 +669,7 @@ - + 0 true From 2ff3e3ed357f922eff32206ac0785b688b3903a4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:23:30 +1100 Subject: [PATCH 037/141] Remove duplicated update_video_infos code --- .../youtube_plugin/youtube/helper/utils.py | 203 ++++++------------ .../youtube_plugin/youtube/helper/yt_play.py | 4 +- 2 files changed, 63 insertions(+), 144 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 5a11bb1f5..8dfaea98d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -16,17 +16,20 @@ from ...youtube.helper import yt_context_menu try: - import inputstreamhelper + from inputstreamhelper import Helper as ISHelper except ImportError: - inputstreamhelper = None + ISHelper = None -__RE_SEASON_EPISODE_MATCHES__ = [re.compile(r'Part (?P\d+)'), - re.compile(r'#(?P\d+)'), - re.compile(r'Ep.[^\w]?(?P\d+)'), - re.compile(r'\[(?P\d+)\]'), - re.compile(r'S(?P\d+)E(?P\d+)'), - re.compile(r'Season (?P\d+)(.+)Episode (?P\d+)'), - re.compile(r'Episode (?P\d+)')] + +__RE_SEASON_EPISODE_MATCHES__ = [ + re.compile(r'Part (?P\d+)'), + re.compile(r'#(?P\d+)'), + re.compile(r'Ep.[^\w]?(?P\d+)'), + re.compile(r'\[(?P\d+)\]'), + re.compile(r'S(?P\d+)E(?P\d+)'), + re.compile(r'Season (?P\d+)(.+)Episode (?P\d+)'), + re.compile(r'Episode (?P\d+)'), +] def extract_urls(text): @@ -113,7 +116,9 @@ def update_channel_infos(provider, context, channel_id_dict, subscription_id_dict = {} filter_list = [] - if context.get_path() == '/subscriptions/list/': + logged_in = provider.is_logged_in() + path = context.get_path() + if path == '/subscriptions/list/': filter_string = context.get_settings().get_string('youtube.filter.my_subscriptions_filtered.list', '') filter_string = filter_string.replace(', ', ',') filter_list = filter_string.split(',') @@ -143,10 +148,10 @@ def update_channel_infos(provider, context, channel_id_dict, channel_item.set_channel_subscription_id(subscription_id) yt_context_menu.append_unsubscribe_from_channel(context_menu, provider, context, subscription_id) # -- subscribe to the channel - if provider.is_logged_in() and context.get_path() != '/subscriptions/list/': + if logged_in and path != '/subscriptions/list/': yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id) - if context.get_path() == '/subscriptions/list/': + if path == '/subscriptions/list/': channel = title.lower().replace(',', '') if channel in filter_list: yt_context_menu.append_remove_my_subscriptions_filter(context_menu, provider, context, title) @@ -187,6 +192,8 @@ def update_playlist_infos(provider, context, playlist_id_dict, access_manager = context.get_access_manager() custom_watch_later_id = access_manager.get_watch_later_id() custom_history_id = access_manager.get_watch_history_id() + logged_in = provider.is_logged_in() + path = context.get_path() thumb_size = context.get_settings().use_thumbnail_size() for playlist_id, yt_item in data.items(): @@ -200,14 +207,14 @@ def update_playlist_infos(provider, context, playlist_id_dict, channel_id = snippet['channelId'] # if the path directs to a playlist of our own, we correct the channel id to 'mine' - if context.get_path() == '/channel/mine/playlists/': + if path == '/channel/mine/playlists/': channel_id = 'mine' channel_name = snippet.get('channelTitle', '') context_menu = [] # play all videos of the playlist yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id) - if provider.is_logged_in(): + if logged_in: if channel_id != 'mine': # subscribe to the channel via the playlist item yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id, @@ -265,14 +272,15 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id_dict = {} settings = context.get_settings() - show_details = settings.show_detailed_description() alternate_player = settings.is_support_alternative_player_enabled() + logged_in = provider.is_logged_in() + path = context.get_path() + show_details = settings.show_detailed_description() thumb_size = settings.use_thumbnail_size() thumb_stamp = get_thumb_timestamp() ui = context.get_ui() for video_id, yt_item in data.items(): - datetime = None video_item = video_id_dict[video_id] # set mediatype @@ -439,7 +447,7 @@ def update_video_infos(provider, context, video_id_dict, /channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/ /playlist/[PLAYLIST_ID]/ """ - some_playlist_match = re.match(r'^(/channel/([^/]+))/playlist/(?P[^/]+)/$', context.get_path()) + some_playlist_match = re.match(r'^(/channel/([^/]+))/playlist/(?P[^/]+)/$', path) if some_playlist_match: replace_context_menu = True playlist_id = some_playlist_match.group('playlist_id') @@ -451,7 +459,7 @@ def update_video_infos(provider, context, video_id_dict, if alternate_player: yt_context_menu.append_play_with(context_menu, provider, context) - if provider.is_logged_in(): + if logged_in: # add 'Watch Later' only if we are not in my 'Watch Later' list watch_later_playlist_id = context.get_access_manager().get_watch_later_id() if watch_later_playlist_id: @@ -459,7 +467,7 @@ def update_video_infos(provider, context, video_id_dict, # provide 'remove' for videos in my playlists if video_id in playlist_item_id_dict: - playlist_match = re.match('^/channel/mine/playlist/(?P[^/]+)/$', context.get_path()) + playlist_match = re.match('^/channel/mine/playlist/(?P[^/]+)/$', path) if playlist_match: playlist_id = playlist_match.group('playlist_id') # we support all playlist except 'Watch History' @@ -470,8 +478,10 @@ def update_video_infos(provider, context, video_id_dict, context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove']), 'RunPlugin(%s)' % context.create_uri( ['playlist', 'remove', 'video'], - {'playlist_id': playlist_id, 'video_id': playlist_item_id, - 'video_name': video_item.get_name()}))) + {'playlist_id': playlist_id, + 'video_id': playlist_item_id, + 'video_name': video_item.get_name()} + ))) is_history = re.match('^/special/watch_history_tv/$', context.get_path()) if is_history: @@ -479,11 +489,11 @@ def update_video_infos(provider, context, video_id_dict, # got to [CHANNEL], only if we are not directly in the channel provide a jump to the channel if (channel_id and channel_name and - utils.create_path('channel', channel_id) != context.get_path()): + utils.create_path('channel', channel_id) != path): video_item.set_channel_id(channel_id) yt_context_menu.append_go_to_channel(context_menu, provider, context, channel_id, channel_name) - if provider.is_logged_in(): + if logged_in: # subscribe to the channel of the video video_item.set_subscription_id(channel_id) yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name) @@ -498,16 +508,15 @@ def update_video_infos(provider, context, video_id_dict, yt_context_menu.append_reset_resume_point(context_menu, provider, context, video_id) # more... - refresh_container = \ - context.get_path().startswith('/channel/mine/playlist/LL') or \ - context.get_path() == '/special/disliked_videos/' - yt_context_menu.append_more_for_video(context_menu, provider, context, video_id, - is_logged_in=provider.is_logged_in(), + refresh_container = (path.startswith('/channel/mine/playlist/LL') + or path == '/special/disliked_videos/') + yt_context_menu.append_more_for_video(context_menu, context, video_id, + is_logged_in=logged_in, refresh_container=refresh_container) if not video_item.live: - yt_context_menu.append_play_with_subtitles(context_menu, provider, context, video_id) - yt_context_menu.append_play_audio_only(context_menu, provider, context, video_id) + yt_context_menu.append_play_with_subtitles(context_menu, context, video_id) + yt_context_menu.append_play_audio_only(context_menu, context, video_id) yt_context_menu.append_play_ask_for_quality(context_menu, provider, context, video_id) @@ -515,22 +524,26 @@ def update_video_infos(provider, context, video_id_dict, video_item.set_context_menu(context_menu, replace=replace_context_menu) -def update_play_info(provider, context, video_id, video_item, video_stream, use_play_data=True): +def update_play_info(provider, context, video_id, video_item, video_stream, + use_play_data=True): + video_item.video_id = video_id + update_video_infos(provider, + context, + {video_id: video_item}, + use_play_data=use_play_data) + settings = context.get_settings() ui = context.get_ui() - resource_manager = provider.get_resource_manager(context) - - video_data = resource_manager.get_videos([video_id], suppress_errors=True) meta_data = video_stream.get('meta', None) - thumb_size = settings.use_thumbnail_size() - image = None - - video_item.video_id = video_id - if meta_data: video_item.set_subtitles(meta_data.get('subtitles', None)) - image = get_thumbnail(thumb_size, meta_data.get('images', {})) + image = get_thumbnail(settings.use_thumbnail_size(), + meta_data.get('images', {})) + if image: + if video_item.live: + image = ''.join([image, '?ct=', get_thumb_timestamp()]) + video_item.set_image(image) if 'headers' in video_stream: video_item.set_headers(video_stream['headers']) @@ -542,110 +555,16 @@ def update_play_info(provider, context, video_id, video_item, video_stream, use_ video_item.set_isa_video(settings.use_isa()) license_info = video_stream.get('license_info', {}) + license_proxy = license_info.get('proxy', '') + license_url = license_info.get('url', '') + license_token = license_info.get('token', '') - if inputstreamhelper and \ - license_info.get('proxy') and \ - license_info.get('url') and \ - license_info.get('token'): - ishelper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha') - ishelper.check_inputstream() - - video_item.set_license_key(license_info.get('proxy')) - ui.set_home_window_property('license_url', license_info.get('url')) - ui.set_home_window_property('license_token', license_info.get('token')) - - """ - This is experimental. We try to get the most information out of the title of a video. - This is not based on any language. In some cases this won't work at all. - TODO: via language and settings provide the regex for matching episode and season. - """ - - for regex in __RE_SEASON_EPISODE_MATCHES__: - re_match = regex.search(video_item.get_name()) - if re_match: - if 'season' in re_match.groupdict(): - video_item.set_season(int(re_match.group('season'))) - - if 'episode' in re_match.groupdict(): - video_item.set_episode(int(re_match.group('episode'))) - break - - if video_item.live: - video_item.set_play_count(0) - - if image: - if video_item.live: - image = ''.join([image, '?ct=', get_thumb_timestamp()]) - video_item.set_image(image) - - # set fanart - video_item.set_fanart(provider.get_fanart(context)) - - if not video_data: - return video_item - - # requires API - # =============== - yt_item = video_data[video_id] - - snippet = yt_item['snippet'] # crash if not conform - play_data = use_play_data and yt_item.get('play_data') - video_item.live = snippet.get('liveBroadcastContent') == 'live' - - # set the title - if not video_item.get_title(): - video_item.set_title(snippet['title']) - - # duration - if not video_item.live and play_data and 'total_time' in play_data: - duration = play_data['total_time'] - else: - duration = yt_item.get('contentDetails', {}).get('duration') - if duration: - # subtract 1s because YouTube duration is +1s too long - duration = utils.datetime_parser.parse(duration).seconds - 1 - if duration: - video_item.set_duration_from_seconds(duration) - - if not video_item.live and play_data: - if 'play_count' in play_data: - video_item.set_play_count(play_data['play_count']) - - if 'played_percent' in play_data: - video_item.set_start_percent(play_data['played_percent']) - - if 'played_time' in play_data: - video_item.set_start_time(play_data['played_time']) - - if 'last_played' in play_data: - video_item.set_last_played(play_data['last_played']) - - # plot - channel_name = snippet.get('channelTitle', '') - description = utils.strip_html_from_text(snippet['description']) - if channel_name and settings.get_bool('youtube.view.description.show_channel_name', True): - description = '%s[CR][CR]%s' % (ui.uppercase(ui.bold(channel_name)), description) - video_item.set_studio(channel_name) - # video_item.add_cast(channel_name) - video_item.add_artist(channel_name) - video_item.set_plot(description) - - # date time - if 'publishedAt' in snippet and snippet['publishedAt']: - date_time = utils.datetime_parser.parse(snippet['publishedAt']) - video_item.set_year_from_datetime(date_time) - video_item.set_aired_from_datetime(date_time) - video_item.set_premiered_from_datetime(date_time) - video_item.set_date_from_datetime(date_time) - - if not image: - image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) - - if video_item.live and image: - image = ''.join([image, '?ct=', get_thumb_timestamp()]) - video_item.set_image(image) + if ISHelper and license_proxy and license_url and license_token: + ISHelper('mpd', drm='com.widevine.alpha').check_inputstream() - return video_item + video_item.set_license_key(license_proxy) + ui.set_home_window_property('license_url', license_url) + ui.set_home_window_property('license_token', license_token) def update_fanarts(provider, context, channel_items_dict, data=None): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index a2c126327..75e97e5c0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -85,8 +85,8 @@ def play_video(provider, context): use_remote_history = use_history and settings.use_remote_history() use_play_data = use_history and settings.use_local_history() - video_item = utils.update_play_info(provider, context, video_id, video_item, video_stream, - use_play_data=use_play_data) + utils.update_play_info(provider, context, video_id, video_item, + video_stream, use_play_data=use_play_data) seek_time = None play_count = 0 From 483dfad4779e529afd284437d812070a384d6776 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 23 Nov 2023 10:37:03 +1100 Subject: [PATCH 038/141] Update UrlToItemConverter for timestamps - Also support live videos and playlists - Partially fix #115 - Fix #502 - Fix #538 - Remove unnecessary seek on start when resuming --- .../youtube/helper/url_resolver.py | 14 +- .../youtube/helper/url_to_item_converter.py | 143 +++++++++++------- .../youtube_plugin/youtube/helper/yt_play.py | 18 +-- .../lib/youtube_plugin/youtube/provider.py | 2 +- 4 files changed, 107 insertions(+), 70 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index fe6750c34..ffca68a24 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -139,13 +139,13 @@ def _loop(_url, tries=5): if _url_components.path == '/supported_browsers': # "sometimes", we get a redirect through an URL of the form https://.../supported_browsers?next_url=&further=paramaters&stuck=here # put together query string from both what's encoded inside next_url and the remaining paramaters of this URL... - _query = parse_qs(_url_components.query) # top-level query string - _nc = urlparse(_query['next_url'][0]) # components of next_url - _next_query = parse_qs(_nc.query) # query string encoded inside next_url - del _query['next_url'] # remove next_url from top level query string - _next_query.update(_query) # add/overwrite all other params from top level query string - _next_query = dict(map(lambda kv: (kv[0], kv[1][0]), _next_query.items())) # flatten to only use first argument of each param - _next_url = urlunsplit((_nc.scheme, _nc.netloc, _nc.path, urlencode(_next_query), _nc.fragment)) # build new URL from these components + _query = parse_qs(_url_components.query) # top-level query string + _nc = urlparse(_query['next_url'][0]) # components of next_url + _next_query = parse_qs(_nc.query) # query string encoded inside next_url + del _query['next_url'] # remove next_url from top level query string + _next_query.update(_query) # add/overwrite all other params from top level query string + _next_query = dict(map(lambda kv: (kv[0], kv[1][0]), _next_query.items())) # flatten to only use first argument of each param + _next_url = urlunsplit((_nc.scheme, _nc.netloc, _nc.path, urlencode(_next_query), _nc.fragment)) # build new URL from these components return _next_url except: diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index 068d853b0..d26ded6b3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -17,8 +17,15 @@ class UrlToItemConverter(object): - RE_CHANNEL_ID = re.compile(r'^/channel/(?P.+)$') - RE_SHORTS_VID = re.compile(r'^/shorts/(?P.+)$') + RE_CHANNEL_ID = re.compile(r'^/channel/(?P.+)$', re.I) + RE_LIVE_VID = re.compile(r'^/live/(?P.+)$', re.I) + RE_SHORTS_VID = re.compile(r'^/shorts/(?P[^?/]+)$', re.I) + RE_SEEK_TIME = re.compile(r'\d+') + VALID_HOSTNAMES = { + 'youtube.com', + 'www.youtube.com', + 'm.youtube.com', + } def __init__(self, flatten=True): self._flatten = flatten @@ -35,50 +42,78 @@ def __init__(self, flatten=True): self._channel_ids = [] def add_url(self, url, provider, context): - url_components = urlparse(url) - if url_components.hostname.lower() in ('youtube.com', 'www.youtube.com', 'm.youtube.com'): - params = dict(parse_qsl(url_components.query)) - if url_components.path.lower() == '/watch': - video_id = params.get('v', '') - if video_id: - plugin_uri = context.create_uri(['play'], {'video_id': video_id}) - video_item = VideoItem('', plugin_uri) - self._video_id_dict[video_id] = video_item - - playlist_id = params.get('list', '') - if playlist_id: - if self._flatten: - self._playlist_ids.append(playlist_id) - else: - playlist_item = DirectoryItem('', context.create_uri(['playlist', playlist_id])) - playlist_item.set_fanart(provider.get_fanart(context)) - self._playlist_id_dict[playlist_id] = playlist_item - elif url_components.path.lower() == '/playlist': - playlist_id = params.get('list', '') - if playlist_id: - if self._flatten: - self._playlist_ids.append(playlist_id) - else: - playlist_item = DirectoryItem('', context.create_uri(['playlist', playlist_id])) - playlist_item.set_fanart(provider.get_fanart(context)) - self._playlist_id_dict[playlist_id] = playlist_item - elif self.RE_SHORTS_VID.match(url_components.path): - re_match = self.RE_SHORTS_VID.match(url_components.path) - video_id = re_match.group('video_id') - plugin_uri = context.create_uri(['play'], {'video_id': video_id}) - video_item = VideoItem('', plugin_uri) - self._video_id_dict[video_id] = video_item - elif self.RE_CHANNEL_ID.match(url_components.path): - re_match = self.RE_CHANNEL_ID.match(url_components.path) - channel_id = re_match.group('channel_id') - if self._flatten: - self._channel_ids.append(channel_id) - else: - channel_item = DirectoryItem('', context.create_uri(['channel', channel_id])) - channel_item.set_fanart(provider.get_fanart(context)) - self._channel_id_dict[channel_id] = channel_item + parsed_url = urlparse(url) + if parsed_url.hostname.lower() not in self.VALID_HOSTNAMES: + context.log_debug('Unknown hostname "{0}" in url "{1}"'.format( + parsed_url.hostname, url + )) + return + + params = dict(parse_qsl(parsed_url.query)) + path = parsed_url.path.lower() + + video_id = playlist_id = channel_id = seek_time = None + if path == '/watch': + video_id = params.get('v') + playlist_id = params.get('list') + seek_time = params.get('t') + elif path == '/playlist': + playlist_id = params.get('list') + elif path.startswith('/shorts/'): + re_match = self.RE_SHORTS_VID.match(parsed_url.path) + video_id = re_match.group('video_id') + elif path.startswith('/channel/'): + re_match = self.RE_CHANNEL_ID.match(parsed_url.path) + channel_id = re_match.group('channel_id') + elif path.startswith('/live/'): + re_match = self.RE_LIVE_VID.match(parsed_url.path) + video_id = re_match.group('video_id') + else: + context.log_debug('Unknown path "{0}" in url "{1}"'.format( + parsed_url.path, url + )) + return + + if video_id: + plugin_params = { + 'video_id': video_id, + } + if seek_time: + seek_time = sum( + int(number) * seconds_per_unit + for number, seconds_per_unit in zip( + reversed(re.findall(self.RE_SEEK_TIME, seek_time)), + (1, 60, 3600, 86400) + ) + if number + ) + plugin_params['seek'] = seek_time + plugin_uri = context.create_uri(['play'], plugin_params) + video_item = VideoItem('', plugin_uri) + self._video_id_dict[video_id] = video_item + + elif playlist_id: + if self._flatten: + self._playlist_ids.append(playlist_id) else: - context.log_debug('Unknown path "%s"' % url_components.path) + playlist_item = DirectoryItem( + '', context.create_uri(['playlist', playlist_id]) + ) + playlist_item.set_fanart(provider.get_fanart(context)) + self._playlist_id_dict[playlist_id] = playlist_item + + elif channel_id: + if self._flatten: + self._channel_ids.append(channel_id) + else: + channel_item = DirectoryItem( + '', context.create_uri(['channel', channel_id]) + ) + channel_item.set_fanart(provider.get_fanart(context)) + self._channel_id_dict[channel_id] = channel_item + + else: + context.log_debug('No items found in url "{0}"'.format(url)) def add_urls(self, urls, provider, context): for url in urls: @@ -91,10 +126,12 @@ def get_items(self, provider, context, title_required=True): # remove duplicates self._channel_ids = list(set(self._channel_ids)) - channels_item = DirectoryItem(context.get_ui().bold(context.localize(provider.LOCAL_MAP['youtube.channels'])), - context.create_uri(['special', 'description_links'], - {'channel_ids': ','.join(self._channel_ids)}), - context.create_resource_path('media', 'playlist.png')) + channels_item = DirectoryItem( + context.get_ui().bold(context.localize(provider.LOCAL_MAP['youtube.channels'])), + context.create_uri(['special', 'description_links'], + {'channel_ids': ','.join(self._channel_ids)}), + context.create_resource_path('media', 'playlist.png') + ) channels_item.set_fanart(provider.get_fanart(context)) result.append(channels_item) @@ -126,7 +163,9 @@ def get_video_items(self, provider, context, title_required=True): if not self._video_items: channel_id_dict = {} - utils.update_video_infos(provider, context, self._video_id_dict, None, channel_id_dict, use_play_data=use_play_data) + utils.update_video_infos(provider, context, self._video_id_dict, + channel_items_dict=channel_id_dict, + use_play_data=use_play_data) utils.update_fanarts(provider, context, channel_id_dict) self._video_items = [ @@ -140,7 +179,9 @@ def get_video_items(self, provider, context, title_required=True): def get_playlist_items(self, provider, context): if not self._playlist_items: channel_id_dict = {} - utils.update_playlist_infos(provider, context, self._playlist_id_dict, channel_id_dict) + utils.update_playlist_infos(provider, context, + self._playlist_id_dict, + channel_items_dict=channel_id_dict) utils.update_fanarts(provider, context, channel_id_dict) self._playlist_items = [ diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 75e97e5c0..3d21507f3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -88,26 +88,22 @@ def play_video(provider, context): utils.update_play_info(provider, context, video_id, video_item, video_stream, use_play_data=use_play_data) - seek_time = None + seek_time = 0.0 play_count = 0 playback_stats = video_stream.get('playback_stats') + if not context.get_param('resume'): + try: + seek_time = context.get_param('seek', 0.0) + except (ValueError, TypeError): + pass + if use_play_data: - seek = video_item.get_start_time() - if seek and context.get_param('resume'): - seek_time = start_time play_count = video_item.get_play_count() or 0 item = to_playback_item(context, video_item) item.setPath(video_item.get_uri()) - try: - seek = float(context.get_param('seek', None)) - if seek: - seek_time = seek - except (ValueError, TypeError): - pass - playback_json = { "video_id": video_id, "channel_id": metadata.get('channel', {}).get('id', ''), diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index a5a9c433c..a64685dd6 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -436,7 +436,7 @@ def on_uri2addon(self, context, re_match): resolver = UrlResolver(context) res_url = resolver.resolve(uri) url_converter = UrlToItemConverter(flatten=True) - url_converter.add_urls([res_url], self, context) + url_converter.add_url(res_url, self, context) items = url_converter.get_items(self, context, title_required=False) if items: return items[0] From 4bc2d3943e74e3ca57b3bfabb5f4dd7dcb8d914b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:52:45 +1100 Subject: [PATCH 039/141] Consolidate localisation ids - Move LOCAL_MAP from provider to context as that is where it is used - Remove const_localize (redundant when using LOCAL_MAP) - Remove _local_map (redundant when using LOCAL_MAP) - Update and sort id names - Replace hardcoded integer ids with id names - Tidy up provider --- .patches/unofficial.patch | 2 +- .../kodion/abstract_provider.py | 20 +- .../kodion/constants/__init__.py | 3 +- .../kodion/constants/const_localize.py | 46 - .../kodion/context/xbmc/xbmc_context.py | 224 ++++- .../kodion/items/favorites_item.py | 2 +- .../kodion/items/new_search_item.py | 2 +- .../kodion/items/next_page_item.py | 3 +- .../kodion/items/search_history_item.py | 16 +- .../kodion/items/search_item.py | 6 +- .../kodion/items/watch_later_item.py | 2 +- .../kodion/ui/xbmc/xbmc_context_ui.py | 9 +- .../kodion/ui/xbmc/xbmc_items.py | 2 +- .../kodion/utils/datetime_parser.py | 50 +- .../youtube_plugin/kodion/utils/methods.py | 4 +- .../youtube/helper/resource_manager.py | 2 +- .../youtube/helper/subtitles.py | 2 +- .../youtube/helper/url_to_item_converter.py | 11 +- .../youtube_plugin/youtube/helper/utils.py | 71 +- .../lib/youtube_plugin/youtube/helper/v3.py | 32 +- .../youtube/helper/video_info.py | 23 +- .../youtube/helper/yt_context_menu.py | 116 +-- .../youtube_plugin/youtube/helper/yt_login.py | 10 +- .../youtube_plugin/youtube/helper/yt_play.py | 19 +- .../youtube/helper/yt_playlist.py | 36 +- .../youtube/helper/yt_setup_wizard.py | 96 +-- .../youtube/helper/yt_specials.py | 25 +- .../youtube/helper/yt_subscriptions.py | 10 +- .../youtube_plugin/youtube/helper/yt_video.py | 31 +- .../lib/youtube_plugin/youtube/provider.py | 784 ++++++++---------- 30 files changed, 858 insertions(+), 801 deletions(-) delete mode 100644 resources/lib/youtube_plugin/kodion/constants/const_localize.py diff --git a/.patches/unofficial.patch b/.patches/unofficial.patch index 758a8d719..32e255cce 100644 --- a/.patches/unofficial.patch +++ b/.patches/unofficial.patch @@ -34,7 +34,7 @@ index ff8ffd44..1306ca2c 100644 def _process_wizard(self, context): + def _setup_views(_context, _view): + view_manager = utils.ViewManager(_context) -+ if not view_manager.update_view_mode(_context.localize(self._local_map['kodion.wizard.view.%s' % _view]), ++ if not view_manager.update_view_mode(_context.localize('setup_wizard.view.%s' % _view]), + _view): + return + diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 0a21812eb..15c150ece 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -30,16 +30,6 @@ class AbstractProvider(object): RESULT_UPDATE_LISTING = 'update_listing' def __init__(self): - self._local_map = { - 'kodion.wizard.view.default': 30027, - 'kodion.wizard.view.episodes': 30028, - 'kodion.wizard.view.movies': 30029, - 'kodion.wizard.view.tvshows': 30032, - 'kodion.wizard.view.songs': 30033, - 'kodion.wizard.view.artists': 30034, - 'kodion.wizard.view.albums': 30035 - } - # map for regular expression (path) to method (names) self._dict_path = {} @@ -85,7 +75,7 @@ def _process_wizard(self, context): wizard_steps.extend(self.get_wizard_steps(context)) if wizard_steps and context.get_ui().on_yes_no_input(context.get_name(), - context.localize(constants.localize.SETUP_WIZARD_EXECUTE)): + context.localize('setup_wizard.execute')): for wizard_step in wizard_steps: wizard_step[0](*wizard_step[1]) @@ -162,7 +152,7 @@ def _internal_favorite(context, re_match): directory_items = context.get_favorite_list().get_items() for directory_item in directory_items: - context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), + context_menu = [(context.localize('watch_later.remove'), 'RunPlugin(%s)' % context.create_uri([constants.paths.FAVORITES, 'remove'], params={'item': to_jsons(directory_item)}))] directory_item.set_context_menu(context_menu) @@ -189,7 +179,7 @@ def _internal_watch_later(self, context, re_match): video_items = context.get_watch_later_list().get_items() for video_item in video_items: - context_menu = [(context.localize(constants.localize.WATCH_LATER_REMOVE), + context_menu = [(context.localize('watch_later.remove'), 'RunPlugin(%s)' % context.create_uri([constants.paths.WATCH_LATER, 'remove'], params={'item': to_jsons(video_item)}))] video_item.set_context_menu(context_menu) @@ -220,7 +210,7 @@ def _internal_search(self, context, re_match): return True if command == 'rename': query = params['q'] - result, new_query = context.get_ui().on_keyboard_input(context.localize(constants.localize.SEARCH_RENAME), + result, new_query = context.get_ui().on_keyboard_input(context.localize('search.rename'), query) if result: search_history.rename(query, new_query) @@ -244,7 +234,7 @@ def _internal_search(self, context, re_match): query = to_unicode(query) query = unquote(query) else: - result, input_query = context.get_ui().on_keyboard_input(context.localize(constants.localize.SEARCH_TITLE)) + result, input_query = context.get_ui().on_keyboard_input(context.localize('search.title')) if result: query = input_query diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 83f50817b..655985e9b 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -9,10 +9,9 @@ """ from . import const_settings as setting -from . import const_localize as localize from . import const_sort_methods as sort_method from . import const_content_types as content_type from . import const_paths as paths -__all__ = ['setting', 'localize', 'sort_method', 'content_type', 'paths'] +__all__ = ['setting', 'sort_method', 'content_type', 'paths'] diff --git a/resources/lib/youtube_plugin/kodion/constants/const_localize.py b/resources/lib/youtube_plugin/kodion/constants/const_localize.py deleted file mode 100644 index e944a0fa0..000000000 --- a/resources/lib/youtube_plugin/kodion/constants/const_localize.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -SELECT_VIDEO_QUALITY = 30010 - -COMMON_PLEASE_WAIT = 30119 - -FAVORITES = 30100 -FAVORITES_ADD = 30101 -FAVORITES_REMOVE = 30108 - -SEARCH = 30102 -SEARCH_TITLE = 30102 -SEARCH_NEW = 30110 -SEARCH_RENAME = 30113 -SEARCH_REMOVE = 30108 -SEARCH_CLEAR = 30120 - -SETUP_WIZARD_EXECUTE = 30030 -SETUP_VIEW_DEFAULT = 30027 -SETUP_VIEW_VIDEOS = 30028 - -LIBRARY = 30103 -HIGHLIGHTS = 30104 -ARCHIVE = 30105 -NEXT_PAGE = 30106 - -WATCH_LATER = 30107 -WATCH_LATER_ADD = 30107 -WATCH_LATER_REMOVE = 30108 - -LATEST_VIDEOS = 30109 - -CONFIRM_DELETE = 30114 -CONFIRM_REMOVE = 30115 -DELETE_CONTENT = 30116 -REMOVE_CONTENT = 30117 - -WATCH_LATER_RETRIEVAL_PAGE = 30711 diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index aa0c6c9bc..0ab4d9236 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -35,6 +35,217 @@ class XbmcContext(AbstractContext): + LOCAL_MAP = { + 'api.id': 30202, + 'api.key': 30201, + 'api.key.incorrect': 30648, + 'api.personal.enabled': 30598, + 'api.personal.failed': 30599, + 'api.secret': 30203, + 'archive': 30105, + 'are_you_sure': 30703, + 'browse_channels': 30512, + 'cache.data': 30687, + 'cache.function': 30557, + 'cancel': 30615, + 'channels': 30500, + 'clear_history': 30609, + 'clear_history_confirmation': 30610, + 'client.id.incorrect': 30649, + 'client.ip': 30700, + 'client.ip.failed': 30701, + 'client.secret.incorrect': 30650, + 'content.delete': 30116, + 'content.delete.confirm': 30114, + 'content.remove': 30117, + 'content.remove.confirm': 30115, + 'datetime.a_minute_ago': 30677, + 'datetime.airing_now': 30691, + 'datetime.airing_soon': 30693, + 'datetime.airing_today_at': 30696, + 'datetime.an_hour_ago': 30679, + 'datetime.in_a_minute': 30692, + 'datetime.in_over_an_hour': 30694, + 'datetime.in_over_two_hours': 30695, + 'datetime.just_now': 30676, + 'datetime.recently': 30678, + 'datetime.three_hours_ago': 30681, + 'datetime.today_at': 30684, + 'datetime.tomorrow_at': 30697, + 'datetime.two_days_ago': 30683, + 'datetime.two_hours_ago': 30680, + 'datetime.yesterday_at': 30682, + 'delete': 30118, + 'disliked.video': 30717, + 'error.no_video_streams_found': 30549, + 'error.rtmpe_not_supported': 30542, + 'failed': 30576, + 'failed.watch_later.retry': 30614, + 'failed.watch_later.retry.2': 30709, + 'failed.watch_later.retry.3': 30710, + 'favorites': 30100, + 'favorites.add': 30101, + 'favorites.remove': 30108, + 'go_to_channel': 30502, + 'highlights': 30104, + 'history': 30509, + 'history.list.remove': 30572, + 'history.list.remove.confirm': 30573, + 'history.list.set': 30571, + 'history.list.set.confirm': 30574, + 'httpd.not.running': 30699, + 'inputstreamhelper.is_installed': 30625, + 'isa.enable.confirm': 30579, + 'key.requirement.notification': 30731, + 'latest_videos': 30109, + 'library': 30103, + 'liked.video': 30716, + 'live': 30539, + 'live.completed': 30647, + 'live.upcoming': 30646, + 'mark.unwatched': 30669, + 'mark.watched': 30670, + 'must_be_signed_in': 30616, + 'my_channel': 30507, + 'my_location': 30654, + 'my_subscriptions': 30510, + 'my_subscriptions.filter.add': 30587, + 'my_subscriptions.filter.added': 30589, + 'my_subscriptions.filter.remove': 30588, + 'my_subscriptions.filter.removed': 30590, + 'my_subscriptions.filtered': 30584, + 'next_page': 30106, + 'none': 30561, + 'perform_geolocation': 30653, + 'playback.history': 30673, + 'playlist.added_to': 30714, + 'playlist.create': 30522, + 'playlist.play.all': 30531, + 'playlist.play.default': 30532, + 'playlist.play.from_here': 30537, + 'playlist.play.reverse': 30533, + 'playlist.play.select': 30535, + 'playlist.play.shuffle': 30534, + 'playlist.progress.updating': 30536, + 'playlist.removed_from': 30715, + 'playlist.select': 30521, + 'playlists': 30501, + 'please_wait': 30119, + 'popular_right_now': 30513, + 'prompt': 30566, + 'purchases': 30622, + 'recommendations': 30551, + 'refresh': 30543, + 'related_videos': 30514, + 'remove': 30108, + 'removed': 30666, + 'rename': 30113, + 'renamed': 30667, + 'requires.krypton': 30624, + 'reset.access_manager.confirm': 30581, + 'reset.resume_point': 30674, + 'retry': 30612, + 'saved.playlists': 30611, + 'search': 30102, + 'search.clear': 30120, + 'search.history': 30558, + 'search.new': 30110, + 'search.quick': 30605, + 'search.quick.incognito': 30606, + 'search.remove': 30108, + 'search.rename': 30113, + 'search.title': 30102, + 'select.listen.ip': 30644, + 'select_video_quality': 30010, + 'setting.auto_remove_watch_later': 30515, + 'settings': 30577, + 'setup.view_default': 30027, + 'setup.view_videos': 30028, + 'setup_wizard.adjust': 30526, + 'setup_wizard.adjust.language_and_region': 30527, + 'setup_wizard.execute': 30030, + 'setup_wizard.select_language': 30524, + 'setup_wizard.select_region': 30525, + 'sign.enter_code': 30519, + 'sign.go_to': 30518, + 'sign.in': 30111, + 'sign.out': 30112, + 'sign.twice.text': 30547, + 'sign.twice.title': 30546, + 'stats.commentCount': 30732, + 'stats.favoriteCount': 30100, + 'stats.likeCount': 30733, + 'stats.viewCount': 30767, + 'stream.alternate': 30747, + 'stream.automatic': 30583, + 'stream.descriptive': 30746, + 'stream.dubbed': 30745, + 'stream.multi_audio': 30763, + 'stream.multi_language': 30762, + 'stream.original': 30744, + 'subscribe': 30506, + 'subscribe_to': 30517, + 'subscribed.to.channel': 30719, + 'subscriptions': 30504, + 'subtitles.download': 30705, + 'subtitles.download.pre': 30706, + 'subtitles.language': 30560, + 'subtitles.no_auto_generated': 30602, + 'subtitles.with_fallback': 30601, + 'succeeded': 30575, + 'unrated.video': 30718, + 'unsubscribe': 30505, + 'unsubscribed.from.channel': 30720, + 'untitled': 30707, + 'upcoming': 30766, + 'updated_': 30597, + 'uploads': 30726, + 'user.changed': 30659, + 'user.enter_name': 30658, + 'user.new': 30656, + 'user.remove': 30662, + 'user.rename': 30663, + 'user.switch': 30655, + 'user.switch.now': 30665, + 'user.unnamed': 30657, + 'video.add_to_playlist': 30520, + 'video.comments': 30732, + 'video.comments.edited': 30735, + 'video.comments.likes': 30733, + 'video.comments.replies': 30734, + 'video.description.links': 30544, + 'video.description.links.not_found': 30545, + 'video.disliked': 30538, + 'video.liked': 30508, + 'video.more': 30548, + 'video.play.ask_for_quality': 30730, + 'video.play.audio_only': 30708, + 'video.play.with': 30540, + 'video.play.with_subtitles': 30702, + 'video.queue': 30511, + 'video.rate': 30528, + 'video.rate.dislike': 30530, + 'video.rate.like': 30529, + 'video.rate.none': 30108, + 'watch_later': 30107, + 'watch_later.add': 30107, + 'watch_later.added_to': 30713, + 'watch_later.list.remove': 30568, + 'watch_later.list.remove.confirm': 30569, + 'watch_later.list.set': 30567, + 'watch_later.list.set.confirm': 30570, + 'watch_later.remove': 30108, + 'watch_later.retrieval_page': 30711, + # For unofficial setup wizard + 'setup_wizard.view.episodes': 30028, + 'setup_wizard.view.movies': 30029, + 'setup_wizard.view.tvshows': 30032, + 'setup_wizard.view.default': 30027, + 'setup_wizard.view.songs': 30033, + 'setup_wizard.view.artists': 30034, + 'setup_wizard.view.albums': 30035 + } + def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override=True): super(XbmcContext, self).__init__(path, params, plugin_name, plugin_id) @@ -104,7 +315,7 @@ def format_date_short(date_obj, short_isoformat=False): if isinstance(date_obj, datetime.datetime): date_obj = date_obj.date() return date_obj.isoformat() - + date_format = xbmc.getRegion('dateshort') _date_obj = date_obj if isinstance(_date_obj, datetime.date): @@ -197,9 +408,12 @@ def get_settings(self): def localize(self, text_id, default_text=''): if not isinstance(text_id, int): try: - text_id = int(text_id) - except ValueError: - return default_text + text_id = self.LOCAL_MAP[text_id] + except KeyError: + try: + text_id = int(text_id) + except ValueError: + return default_text if text_id <= 0: return default_text @@ -291,7 +505,7 @@ def use_inputstream_adaptive(self): if self._settings.use_isa(): if self.addon_enabled('inputstream.adaptive'): success = True - elif self.get_ui().on_yes_no_input(self.get_name(), self.localize(30579)): + elif self.get_ui().on_yes_no_input(self.get_name(), self.localize('isa.enable.confirm')): success = self.set_addon_enabled('inputstream.adaptive') else: success = False diff --git a/resources/lib/youtube_plugin/kodion/items/favorites_item.py b/resources/lib/youtube_plugin/kodion/items/favorites_item.py index acb939436..a32c5b756 100644 --- a/resources/lib/youtube_plugin/kodion/items/favorites_item.py +++ b/resources/lib/youtube_plugin/kodion/items/favorites_item.py @@ -16,7 +16,7 @@ class FavoritesItem(DirectoryItem): def __init__(self, context, alt_name=None, image=None, fanart=None): name = alt_name if not name: - name = context.localize(constants.localize.FAVORITES) + name = context.localize('favorites') if image is None: image = context.create_resource_path('media/favorites.png') diff --git a/resources/lib/youtube_plugin/kodion/items/new_search_item.py b/resources/lib/youtube_plugin/kodion/items/new_search_item.py index 5499e598c..f0985d96d 100644 --- a/resources/lib/youtube_plugin/kodion/items/new_search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/new_search_item.py @@ -16,7 +16,7 @@ class NewSearchItem(DirectoryItem): def __init__(self, context, alt_name=None, image=None, fanart=None, incognito=False, channel_id='', addon_id='', location=False): name = alt_name if not name: - name = context.get_ui().bold(context.localize(constants.localize.SEARCH_NEW)) + name = context.get_ui().bold(context.localize('search.new')) if image is None: image = context.create_resource_path('media/new_search.png') diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index 364baa36c..fc4b83576 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -9,7 +9,6 @@ """ from .directory_item import DirectoryItem -from .. import constants class NextPageItem(DirectoryItem): @@ -17,7 +16,7 @@ def __init__(self, context, current_page=1, image=None, fanart=None): new_params = {} new_params.update(context.get_params()) new_params['page'] = current_page + 1 - name = context.localize(constants.localize.NEXT_PAGE, 'Next Page') + name = context.localize('next_page', 'Next Page') if name.find('%d') != -1: name %= current_page + 1 diff --git a/resources/lib/youtube_plugin/kodion/items/search_history_item.py b/resources/lib/youtube_plugin/kodion/items/search_history_item.py index 10b8c7646..42e9f7c1f 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_history_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_history_item.py @@ -9,7 +9,7 @@ """ from .directory_item import DirectoryItem -from .. import constants +from ..constants.const_paths import SEARCH class SearchHistoryItem(DirectoryItem): @@ -21,16 +21,16 @@ def __init__(self, context, query, image=None, fanart=None, location=False): if location: params['location'] = location - super(SearchHistoryItem, self).__init__(query, context.create_uri([constants.paths.SEARCH, 'query'], params=params), image=image) + super(SearchHistoryItem, self).__init__(query, context.create_uri([SEARCH, 'query'], params=params), image=image) if fanart: self.set_fanart(fanart) else: self.set_fanart(context.get_fanart()) - context_menu = [(context.localize(constants.localize.SEARCH_REMOVE), - 'RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'remove'], params={'q': query})), - (context.localize(constants.localize.SEARCH_RENAME), - 'RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'rename'], params={'q': query})), - (context.localize(constants.localize.SEARCH_CLEAR), - 'RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'clear']))] + context_menu = [(context.localize('search.remove'), + 'RunPlugin(%s)' % context.create_uri([SEARCH, 'remove'], params={'q': query})), + (context.localize('search.rename'), + 'RunPlugin(%s)' % context.create_uri([SEARCH, 'rename'], params={'q': query})), + (context.localize('search.clear'), + 'RunPlugin(%s)' % context.create_uri([SEARCH, 'clear']))] self.set_context_menu(context_menu) diff --git a/resources/lib/youtube_plugin/kodion/items/search_item.py b/resources/lib/youtube_plugin/kodion/items/search_item.py index 51e292dd2..39869c2c0 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_item.py @@ -9,21 +9,21 @@ """ from .directory_item import DirectoryItem -from .. import constants +from ..constants.const_paths import SEARCH class SearchItem(DirectoryItem): def __init__(self, context, alt_name=None, image=None, fanart=None, location=False): name = alt_name if not name: - name = context.localize(constants.localize.SEARCH) + name = context.localize('search') if image is None: image = context.create_resource_path('media/search.png') params = {'location': location} if location else {} - super(SearchItem, self).__init__(name, context.create_uri([constants.paths.SEARCH, 'list'], params=params), image=image) + super(SearchItem, self).__init__(name, context.create_uri([SEARCH, 'list'], params=params), image=image) if fanart: self.set_fanart(fanart) else: diff --git a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py index 0a816c277..0a10c27f0 100644 --- a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py +++ b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py @@ -16,7 +16,7 @@ class WatchLaterItem(DirectoryItem): def __init__(self, context, alt_name=None, image=None, fanart=None): name = alt_name if not name: - name = context.localize(constants.localize.WATCH_LATER) + name = context.localize('watch_later') if image is None: image = context.create_resource_path('media/watch_later.png') diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 2e03d801a..460683607 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -14,7 +14,6 @@ from .xbmc_progress_dialog import XbmcProgressDialog from .xbmc_progress_dialog_bg import XbmcProgressDialogBG from ..abstract_context_ui import AbstractContextUI -from ... import constants from ... import utils @@ -72,12 +71,12 @@ def on_ok(self, title, text): return dialog.ok(title, text) def on_remove_content(self, content_name): - text = self._context.localize(constants.localize.REMOVE_CONTENT) % utils.to_unicode(content_name) - return self.on_yes_no_input(self._context.localize(constants.localize.CONFIRM_REMOVE), text) + text = self._context.localize('content.remove') % utils.to_unicode(content_name) + return self.on_yes_no_input(self._context.localize('content.remove.confirm'), text) def on_delete_content(self, content_name): - text = self._context.localize(constants.localize.DELETE_CONTENT) % utils.to_unicode(content_name) - return self.on_yes_no_input(self._context.localize(constants.localize.CONFIRM_DELETE), text) + text = self._context.localize('content.delete') % utils.to_unicode(content_name) + return self.on_yes_no_input(self._context.localize('content.delete.confirm'), text) def on_select(self, title, items=None): if items is None: diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index c7fe95dd9..e58372b02 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -153,7 +153,7 @@ def to_video_item(context, video_item): local_datetime = datetime_parser.utc_to_local(datetime) props['PublishedLocal'] = str(local_datetime) if video_item.live: - props['PublishedSince'] = context.localize('30539') + props['PublishedSince'] = context.localize('live') elif local_datetime: props['PublishedSince'] = str(datetime_parser.datetime_to_since( context, local_datetime diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index b547bd228..a9d0cb29d 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -30,10 +30,6 @@ now = datetime.now -def py2_utf8(text): - return text - - def parse(datetime_string, as_utc=True): offset = 0 if as_utc else None @@ -88,8 +84,9 @@ def _to_int(value): # abbreviated match abbreviated_match = __RE_MATCH_ABBREVIATED__.match(datetime_string) if abbreviated_match: - month = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'June': 6, 'Jun': 6, 'July': 7, 'Jul': 7, 'Aug': 8, - 'Sept': 9, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} + month = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'June': 6, + 'Jun': 6, 'July': 7, 'Jul': 7, 'Aug': 8, 'Sept': 9, 'Sep': 9, + 'Oct': 10, 'Nov': 11, 'Dec': 12} return utc_to_local( dt=datetime(year=_to_int(abbreviated_match.group('year')), month=month[abbreviated_match.group('month')], @@ -138,43 +135,48 @@ def datetime_to_since(context, dt): if seconds > 0: if seconds < 60: - return py2_utf8(context.localize('30676')) + return context.localize('datetime.just_now') if 60 <= seconds < 120: - return py2_utf8(context.localize('30677')) + return context.localize('datetime.a_minute_ago') if 120 <= seconds < 3600: - return py2_utf8(context.localize('30678')) + return context.localize('datetime.recently') if 3600 <= seconds < 7200: - return py2_utf8(context.localize('30679')) + return context.localize('datetime.an_hour_ago') if 7200 <= seconds < 10800: - return py2_utf8(context.localize('30680')) + return context.localize('datetime.two_hours_ago') if 10800 <= seconds < 14400: - return py2_utf8(context.localize('30681')) + return context.localize('datetime.three_hours_ago') if use_yesterday and dt.date() == yesterday.date(): - return ' '.join([py2_utf8(context.localize('30682')), context.format_time(dt)]) + return ' '.join((context.localize('datetime.yesterday_at'), + context.format_time(dt))) if dt.date() == yyesterday.date(): - return py2_utf8(context.localize('30683')) + return context.localize('datetime.two_days_ago') if 5400 <= seconds < 86400: - return ' '.join([py2_utf8(context.localize('30684')), context.format_time(dt)]) + return ' '.join((context.localize('datetime.today_at'), + context.format_time(dt))) if 86400 <= seconds < 172800: - return ' '.join([py2_utf8(context.localize('30682')), context.format_time(dt)]) + return ' '.join((context.localize('datetime.yesterday_at'), + context.format_time(dt))) else: seconds *= -1 if seconds < 60: - return py2_utf8(context.localize('30691')) + return context.localize('datetime.airing_now') if 60 <= seconds < 120: - return py2_utf8(context.localize('30692')) + return context.localize('datetime.in_a_minute') if 120 <= seconds < 3600: - return py2_utf8(context.localize('30693')) + return context.localize('datetime.airing_soon') if 3600 <= seconds < 7200: - return py2_utf8(context.localize('30694')) + return context.localize('datetime.in_over_an_hour') if 7200 <= seconds < 10800: - return py2_utf8(context.localize('30695')) + return context.localize('datetime.in_over_two_hours') if dt.date() == today: - return ' '.join([py2_utf8(context.localize('30696')), context.format_time(dt)]) + return ' '.join((context.localize('datetime.airing_today_at'), + context.format_time(dt))) if dt.date() == tomorrow: - return ' '.join([py2_utf8(context.localize('30697')), context.format_time(dt)]) + return ' '.join((context.localize('datetime.tomorrow_at'), + context.format_time(dt))) - return ' '.join([context.format_date_short(dt), context.format_time(dt)]) + return ' '.join((context.format_date_short(dt), context.format_time(dt))) def strptime(s, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index a4d506e88..e3fc6d72d 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -14,8 +14,6 @@ from math import floor, log from urllib.parse import quote -from ..constants import localize - import xbmc import xbmcvfs @@ -173,7 +171,7 @@ def _find_best_fit_video(_stream_data): for sorted_stream_data in sorted_stream_data_list ] - result = context.get_ui().on_select(context.localize(localize.SELECT_VIDEO_QUALITY), items) + result = context.get_ui().on_select(context.localize('select_video_quality'), items) if result != -1: selected_stream_data = result else: diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index ca1edcf95..5f74d5824 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -239,7 +239,7 @@ def handle_error(self, json_data, suppress_errors=False): context.log_error(error_message) if reason == 'accessNotConfigured': - message = context.localize(30731) + message = context.localize('key.requirement.notification') ok_dialog = True elif reason in {'quotaExceeded', 'dailyLimitExceeded'}: diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index f61c02808..0e3c767be 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -166,7 +166,7 @@ def _prompt(self): translations = [(track.get('languageCode'), self._get_language_name(track)) for track in self.translation_langs] languages = tracks + translations if languages: - choice = self.context.get_ui().on_select(self.context.localize(30560), [language for _, language in languages]) + choice = self.context.get_ui().on_select(self.context.localize('subtitles.language'), [language for _, language in languages]) if choice != -1: return self._get(lang_code=languages[choice][0], language=languages[choice][1]) self.context.log_debug('Subtitle selection cancelled') diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index d26ded6b3..192c7c225 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -127,7 +127,7 @@ def get_items(self, provider, context, title_required=True): self._channel_ids = list(set(self._channel_ids)) channels_item = DirectoryItem( - context.get_ui().bold(context.localize(provider.LOCAL_MAP['youtube.channels'])), + context.get_ui().bold(context.localize('channels')), context.create_uri(['special', 'description_links'], {'channel_ids': ','.join(self._channel_ids)}), context.create_resource_path('media', 'playlist.png') @@ -139,10 +139,11 @@ def get_items(self, provider, context, title_required=True): # remove duplicates self._playlist_ids = list(set(self._playlist_ids)) - playlists_item = DirectoryItem(context.get_ui().bold(context.localize(provider.LOCAL_MAP['youtube.playlists'])), - context.create_uri(['special', 'description_links'], - {'playlist_ids': ','.join(self._playlist_ids)}), - context.create_resource_path('media', 'playlist.png')) + playlists_item = DirectoryItem( + context.get_ui().bold(context.localize('playlists')), + context.create_uri(['special', 'description_links'], + {'playlist_ids': ','.join(self._playlist_ids)}), + context.create_resource_path('media', 'playlist.png')) playlists_item.set_fanart(provider.get_fanart(context)) result.append(playlists_item) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 8dfaea98d..62d5790f5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -46,7 +46,7 @@ def get_thumb_timestamp(minutes=15): return str(time.mktime(time.gmtime(minutes * 60 * (round(time.time() / (minutes * 60)))))) -def make_comment_item(context, provider, snippet, uri, total_replies=0): +def make_comment_item(context, snippet, uri, total_replies=0): author = '[B]{}[/B]'.format(utils.to_str(snippet['authorDisplayName'])) body = utils.to_str(snippet['textOriginal']) @@ -60,16 +60,16 @@ def make_comment_item(context, provider, snippet, uri, total_replies=0): if snippet['likeCount'] and total_replies: label_props = '[COLOR lime][B]+%s[/B][/COLOR]|[COLOR cyan][B]%s[/B][/COLOR]' % (str_likes, str_replies) plot_props = '[COLOR lime][B]%s %s[/B][/COLOR]|[COLOR cyan][B]%s %s[/B][/COLOR]' % (str_likes, - context.localize(provider.LOCAL_MAP['youtube.video.comments.likes']), str_replies, - context.localize(provider.LOCAL_MAP['youtube.video.comments.replies'])) + context.localize('video.comments.likes'), str_replies, + context.localize('video.comments.replies')) elif snippet['likeCount']: label_props = '[COLOR lime][B]+%s[/B][/COLOR]' % str_likes plot_props = '[COLOR lime][B]%s %s[/B][/COLOR]' % (str_likes, - context.localize(provider.LOCAL_MAP['youtube.video.comments.likes'])) + context.localize('video.comments.likes')) elif total_replies: label_props = '[COLOR cyan][B]%s[/B][/COLOR]' % str_replies plot_props = '[COLOR cyan][B]%s %s[/B][/COLOR]' % (str_replies, - context.localize(provider.LOCAL_MAP['youtube.video.comments.replies'])) + context.localize('video.comments.replies')) else: pass # The comment has no likes or replies. @@ -82,7 +82,7 @@ def make_comment_item(context, provider, snippet, uri, total_replies=0): label = '{author}{edited} {body}'.format(author=author, edited=edited, body=body.replace('\n', ' ')) # Format the plot of the comment item. - edited = ' (%s)' % context.localize(provider.LOCAL_MAP['youtube.video.comments.edited']) if is_edited else '' + edited = ' (%s)' % context.localize('video.comments.edited') if is_edited else '' if plot_props: plot = '{author} ({props}){edited}[CR][CR]{body}'.format(author=author, props=plot_props, edited=edited, body=body) @@ -146,17 +146,17 @@ def update_channel_infos(provider, context, channel_id_dict, subscription_id = subscription_id_dict.get(channel_id, '') if subscription_id: channel_item.set_channel_subscription_id(subscription_id) - yt_context_menu.append_unsubscribe_from_channel(context_menu, provider, context, subscription_id) + yt_context_menu.append_unsubscribe_from_channel(context_menu, context, subscription_id) # -- subscribe to the channel if logged_in and path != '/subscriptions/list/': - yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id) + yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id) if path == '/subscriptions/list/': channel = title.lower().replace(',', '') if channel in filter_list: - yt_context_menu.append_remove_my_subscriptions_filter(context_menu, provider, context, title) + yt_context_menu.append_remove_my_subscriptions_filter(context_menu, context, title) else: - yt_context_menu.append_add_my_subscriptions_filter(context_menu, provider, context, title) + yt_context_menu.append_add_my_subscriptions_filter(context_menu, context, title) channel_item.set_context_menu(context_menu) fanart_images = yt_item.get('brandingSettings', {}).get('image', {}) @@ -212,32 +212,32 @@ def update_playlist_infos(provider, context, playlist_id_dict, channel_name = snippet.get('channelTitle', '') context_menu = [] # play all videos of the playlist - yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id) + yt_context_menu.append_play_all_from_playlist(context_menu, context, playlist_id) if logged_in: if channel_id != 'mine': # subscribe to the channel via the playlist item - yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id, + yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id, channel_name) else: # remove my playlist - yt_context_menu.append_delete_playlist(context_menu, provider, context, playlist_id, title) + yt_context_menu.append_delete_playlist(context_menu, context, playlist_id, title) # rename playlist - yt_context_menu.append_rename_playlist(context_menu, provider, context, playlist_id, title) + yt_context_menu.append_rename_playlist(context_menu, context, playlist_id, title) # remove as my custom watch later playlist if playlist_id == custom_watch_later_id: - yt_context_menu.append_remove_as_watchlater(context_menu, provider, context, playlist_id, title) + yt_context_menu.append_remove_as_watchlater(context_menu, context, playlist_id, title) # set as my custom watch later playlist else: - yt_context_menu.append_set_as_watchlater(context_menu, provider, context, playlist_id, title) + yt_context_menu.append_set_as_watchlater(context_menu, context, playlist_id, title) # remove as custom history playlist if playlist_id == custom_history_id: - yt_context_menu.append_remove_as_history(context_menu, provider, context, playlist_id, title) + yt_context_menu.append_remove_as_history(context_menu, context, playlist_id, title) # set as custom history playlist else: - yt_context_menu.append_set_as_history(context_menu, provider, context, playlist_id, title) + yt_context_menu.append_set_as_history(context_menu, context, playlist_id, title) if context_menu: playlist_item.set_context_menu(context_menu) @@ -334,9 +334,8 @@ def update_video_infos(provider, context, video_id_dict, video_item.set_aired_from_datetime(local_datetime) video_item.set_premiered_from_datetime(local_datetime) video_item.set_date_from_datetime(local_datetime) - type_label = context.localize(provider.LOCAL_MAP[ - 'youtube.live' if video_item.live else 'youtube.upcoming' - ]) + type_label = context.localize('live' if video_item.live + else 'upcoming') start_at = '{type_label} {start_at}'.format( type_label=type_label, start_at=utils.datetime_parser.get_scheduled_start( @@ -353,7 +352,7 @@ def update_video_infos(provider, context, video_id_dict, stats = [] if 'statistics' in yt_item: for stat, value in yt_item['statistics'].items(): - label = provider.LOCAL_MAP.get('youtube.stats.' + stat) + label = context.LOCAL_MAP.get('stats.' + stat) if label: stats.append('{value} {name}'.format( name=context.localize(label).lower(), @@ -436,10 +435,10 @@ def update_video_infos(provider, context, video_id_dict, replace_context_menu = False # Refresh - yt_context_menu.append_refresh(context_menu, provider, context) + yt_context_menu.append_refresh(context_menu, context) # Queue Video - yt_context_menu.append_queue_video(context_menu, provider, context) + yt_context_menu.append_queue_video(context_menu, context) """ Play all videos of the playlist. @@ -452,8 +451,8 @@ def update_video_infos(provider, context, video_id_dict, replace_context_menu = True playlist_id = some_playlist_match.group('playlist_id') - yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id, video_id) - yt_context_menu.append_play_all_from_playlist(context_menu, provider, context, playlist_id) + yt_context_menu.append_play_all_from_playlist(context_menu, context, playlist_id, video_id) + yt_context_menu.append_play_all_from_playlist(context_menu, context, playlist_id) # 'play with...' (external player) if alternate_player: @@ -463,11 +462,11 @@ def update_video_infos(provider, context, video_id_dict, # add 'Watch Later' only if we are not in my 'Watch Later' list watch_later_playlist_id = context.get_access_manager().get_watch_later_id() if watch_later_playlist_id: - yt_context_menu.append_watch_later(context_menu, provider, context, watch_later_playlist_id, video_id) + yt_context_menu.append_watch_later(context_menu, context, watch_later_playlist_id, video_id) # provide 'remove' for videos in my playlists if video_id in playlist_item_id_dict: - playlist_match = re.match('^/channel/mine/playlist/(?P[^/]+)/$', path) + playlist_match = re.match('^/channel/mine/playlist/(?P[^/]+)/$', context.get_path()) if playlist_match: playlist_id = playlist_match.group('playlist_id') # we support all playlist except 'Watch History' @@ -475,7 +474,7 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id = playlist_item_id_dict[video_id] video_item.set_playlist_id(playlist_id) video_item.set_playlist_item_id(playlist_item_id) - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove']), + context_menu.append((context.localize('remove'), 'RunPlugin(%s)' % context.create_uri( ['playlist', 'remove', 'video'], {'playlist_id': playlist_id, @@ -485,27 +484,27 @@ def update_video_infos(provider, context, video_id_dict, is_history = re.match('^/special/watch_history_tv/$', context.get_path()) if is_history: - yt_context_menu.append_clear_watch_history(context_menu, provider, context) + yt_context_menu.append_clear_watch_history(context_menu, context) # got to [CHANNEL], only if we are not directly in the channel provide a jump to the channel if (channel_id and channel_name and utils.create_path('channel', channel_id) != path): video_item.set_channel_id(channel_id) - yt_context_menu.append_go_to_channel(context_menu, provider, context, channel_id, channel_name) + yt_context_menu.append_go_to_channel(context_menu, context, channel_id, channel_name) if logged_in: # subscribe to the channel of the video video_item.set_subscription_id(channel_id) - yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name) + yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id, channel_name) if not video_item.live and play_data: if not play_data.get('play_count'): - yt_context_menu.append_mark_watched(context_menu, provider, context, video_id) + yt_context_menu.append_mark_watched(context_menu, context, video_id) else: - yt_context_menu.append_mark_unwatched(context_menu, provider, context, video_id) + yt_context_menu.append_mark_unwatched(context_menu, context, video_id) if play_data.get('played_percent', 0) > 0 or play_data.get('played_time', 0) > 0: - yt_context_menu.append_reset_resume_point(context_menu, provider, context, video_id) + yt_context_menu.append_reset_resume_point(context_menu, context, video_id) # more... refresh_container = (path.startswith('/channel/mine/playlist/LL') @@ -518,7 +517,7 @@ def update_video_infos(provider, context, video_id_dict, yt_context_menu.append_play_with_subtitles(context_menu, context, video_id) yt_context_menu.append_play_audio_only(context_menu, context, video_id) - yt_context_menu.append_play_ask_for_quality(context_menu, provider, context, video_id) + yt_context_menu.append_play_ask_for_quality(context_menu, context, video_id) if context_menu: video_item.set_context_menu(context_menu, replace=replace_context_menu) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index e7fd3e429..6fe14423b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -42,7 +42,7 @@ def _process_list_response(provider, context, json_data): if kind == 'video': video_id = yt_item['id'] snippet = yt_item['snippet'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: @@ -60,7 +60,7 @@ def _process_list_response(provider, context, json_data): elif kind == 'channel': channel_id = yt_item['id'] snippet = yt_item['snippet'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {} if incognito: @@ -74,14 +74,14 @@ def _process_list_response(provider, context, json_data): # if logged in => provide subscribing to the channel if provider.is_logged_in(): context_menu = [] - yt_context_menu.append_subscribe_to_channel(context_menu, provider, context, channel_id) + yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id) channel_item.set_context_menu(context_menu) result.append(channel_item) channel_id_dict[channel_id] = channel_item elif kind == 'guidecategory': guide_id = yt_item['id'] snippet = yt_item['snippet'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) item_params = {'guide_id': guide_id} if incognito: item_params['incognito'] = incognito @@ -93,7 +93,7 @@ def _process_list_response(provider, context, json_data): result.append(guide_item) elif kind == 'subscription': snippet = yt_item['snippet'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) channel_id = snippet['resourceId']['channelId'] item_params = {} @@ -113,7 +113,7 @@ def _process_list_response(provider, context, json_data): elif kind == 'playlist': playlist_id = yt_item['id'] snippet = yt_item['snippet'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) channel_id = snippet['channelId'] @@ -138,7 +138,7 @@ def _process_list_response(provider, context, json_data): # store the id of the playlistItem - for deleting this item we need this item playlist_item_id_dict[video_id] = yt_item['id'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: @@ -169,7 +169,7 @@ def _process_list_response(provider, context, json_data): else: continue - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: @@ -194,10 +194,10 @@ def _process_list_response(provider, context, json_data): item_uri = context.create_uri(['special', 'child_comments'], item_params) else: item_uri = '' - result.append(utils.make_comment_item(context, provider, snippet, item_uri, total_replies)) + result.append(utils.make_comment_item(context, snippet, item_uri, total_replies)) elif kind == 'comment': - result.append(utils.make_comment_item(context, provider, yt_item['snippet'], uri='')) + result.append(utils.make_comment_item(context, yt_item['snippet'], uri='')) elif kind == 'searchresult': _, kind = _parse_kind(yt_item.get('id', {})) @@ -206,7 +206,7 @@ def _process_list_response(provider, context, json_data): if kind == 'video': video_id = yt_item['id']['videoId'] snippet = yt_item.get('snippet', {}) - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: @@ -225,7 +225,7 @@ def _process_list_response(provider, context, json_data): elif kind == 'playlist': playlist_id = yt_item['id']['playlistId'] snippet = yt_item['snippet'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) channel_id = snippet['channelId'] @@ -246,7 +246,7 @@ def _process_list_response(provider, context, json_data): elif kind == 'channel': channel_id = yt_item['id']['channelId'] snippet = yt_item['snippet'] - title = snippet.get('title', context.localize(provider.LOCAL_MAP['youtube.untitled'])) + title = snippet.get('title', context.localize('untitled')) image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {} if incognito: @@ -329,7 +329,7 @@ def response_to_items(provider, context, json_data, sort=None, reverse_sort=Fals return result -def handle_error(provider, context, json_data): +def handle_error(context, json_data): if json_data and 'error' in json_data: ok_dialog = False message_timeout = 5000 @@ -342,11 +342,11 @@ def handle_error(provider, context, json_data): context.log_error('Error reason: |%s| with message: |%s|' % (reason, log_message)) if reason == 'accessNotConfigured': - message = context.localize(provider.LOCAL_MAP['youtube.key.requirement.notification']) + message = context.localize('key.requirement.notification') ok_dialog = True if reason == 'keyInvalid' and message == 'Bad Request': - message = context.localize(provider.LOCAL_MAP['youtube.api.key.incorrect']) + message = context.localize('api.key.incorrect') message_timeout = 7000 if reason in {'quotaExceeded', 'dailyLimitExceeded'}: diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index d042e2970..28637160b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1285,7 +1285,7 @@ def _get_video_info(self): manifest_url, main_stream = self._generate_mpd_manifest( video_data, audio_data, license_info.get('url') ) - + # extract non-adaptive streams if all_fmts: stream_list.extend(self._create_stream_list( @@ -1330,11 +1330,15 @@ def _get_video_info(self): details['title'].append(' [ASR]') if main_stream['multi_lang']: details['title'].extend(( - ' [', self._context.localize(30762), ']' + ' [', + self._context.localize('stream.multi_language'), + ']' )) if main_stream['multi_audio']: details['title'].extend(( - ' [', self._context.localize(30763), ']' + ' [', + self._context.localize('stream.multi_audio'), + ']' )) details['title'] = ''.join(details['title']) @@ -1441,18 +1445,18 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): if role_type == 4 or audio_track.get('audioIsDefault'): role = 'main' - label = self._context.localize(30744) + label = self._context.localize('stream.original') elif role_type == 3: role = 'dub' - label = self._context.localize(30745) + label = self._context.localize('stream.dubbed') elif role_type == 2: role = 'description' - label = self._context.localize(30746) + label = self._context.localize('stream.descriptive') # Unsure of what other audio types are actually available # Role set to "alternate" as default fallback else: role = 'alternate' - label = self._context.localize(30747) + label = self._context.localize('stream.alternate') mime_group = '{0}_{1}.{2}'.format( mime_type, language_code, role_type @@ -1471,7 +1475,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): language_code = default_lang_code role = 'main' role_type = 4 - label = self._context.localize(30744) + label = self._context.localize('stream.original') mime_group = mime_type sample_rate = int(stream.get('audioSampleRate', '0'), 10) @@ -1723,7 +1727,8 @@ def _filter_group(previous_group, previous_stream, item): if group.startswith(mime_type) and 'auto' in stream_select: label = '{0} [{1}]'.format( - stream['langName'] or self._context.localize(30583), + stream['langName'] + or self._context.localize('stream.automatic'), stream['label'] ) if stream == main_stream[media_type]: diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py b/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py index 941e86e34..d35080dd6 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py @@ -11,7 +11,7 @@ from ... import kodion -def append_more_for_video(context_menu, provider, context, video_id, is_logged_in=False, refresh_container=False): +def append_more_for_video(context_menu, context, video_id, is_logged_in=False, refresh_container=False): _is_logged_in = '0' if is_logged_in: _is_logged_in = '1' @@ -20,195 +20,195 @@ def append_more_for_video(context_menu, provider, context, video_id, is_logged_i if refresh_container: _refresh_container = '1' - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.more']), + context_menu.append((context.localize('video.more'), 'RunPlugin(%s)' % context.create_uri(['video', 'more'], {'video_id': video_id, 'logged_in': _is_logged_in, 'refresh_container': _refresh_container}))) -def append_content_from_description(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.description.links']), +def append_content_from_description(context_menu, context, video_id): + context_menu.append((context.localize('video.description.links'), 'Container.Update(%s)' % context.create_uri(['special', 'description_links'], {'video_id': video_id}))) -def append_play_with(context_menu, provider, context): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.play_with']), 'Action(SwitchPlayer)')) +def append_play_with(context_menu, context): + context_menu.append((context.localize('video.play.with'), 'Action(SwitchPlayer)')) -def append_queue_video(context_menu, provider, context): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.queue']), 'Action(Queue)')) +def append_queue_video(context_menu, context): + context_menu.append((context.localize('video.queue'), 'Action(Queue)')) -def append_play_all_from_playlist(context_menu, provider, context, playlist_id, video_id=''): +def append_play_all_from_playlist(context_menu, context, playlist_id, video_id=''): if video_id: - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.playlist.play.from_here']), + context_menu.append((context.localize('playlist.play.from_here'), 'RunPlugin(%s)' % context.create_uri(['play'], {'playlist_id': playlist_id, 'video_id': video_id, 'play': '1'}))) else: - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.playlist.play.all']), + context_menu.append((context.localize('playlist.play.all'), 'RunPlugin(%s)' % context.create_uri(['play'], {'playlist_id': playlist_id, 'play': '1'}))) -def append_add_video_to_playlist(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.add_to_playlist']), +def append_add_video_to_playlist(context_menu, context, video_id): + context_menu.append((context.localize('video.add_to_playlist'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'select', 'playlist'], {'video_id': video_id}))) -def append_rename_playlist(context_menu, provider, context, playlist_id, playlist_name): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.rename']), +def append_rename_playlist(context_menu, context, playlist_id, playlist_name): + context_menu.append((context.localize('rename'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'rename', 'playlist'], {'playlist_id': playlist_id, 'playlist_name': playlist_name}))) -def append_delete_playlist(context_menu, provider, context, playlist_id, playlist_name): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.delete']), +def append_delete_playlist(context_menu, context, playlist_id, playlist_name): + context_menu.append((context.localize('delete'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'remove', 'playlist'], {'playlist_id': playlist_id, 'playlist_name': playlist_name}))) -def append_remove_as_watchlater(context_menu, provider, context, playlist_id, playlist_name): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove.as.watchlater']), +def append_remove_as_watchlater(context_menu, context, playlist_id, playlist_name): + context_menu.append((context.localize('watch_later.list.remove'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'remove', 'watchlater'], {'playlist_id': playlist_id, 'playlist_name': playlist_name}))) -def append_set_as_watchlater(context_menu, provider, context, playlist_id, playlist_name): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.set.as.watchlater']), +def append_set_as_watchlater(context_menu, context, playlist_id, playlist_name): + context_menu.append((context.localize('watch_later.list.set'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'set', 'watchlater'], {'playlist_id': playlist_id, 'playlist_name': playlist_name}))) -def append_remove_as_history(context_menu, provider, context, playlist_id, playlist_name): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove.as.history']), +def append_remove_as_history(context_menu, context, playlist_id, playlist_name): + context_menu.append((context.localize('history.list.remove'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'remove', 'history'], {'playlist_id': playlist_id, 'playlist_name': playlist_name}))) -def append_set_as_history(context_menu, provider, context, playlist_id, playlist_name): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.set.as.history']), +def append_set_as_history(context_menu, context, playlist_id, playlist_name): + context_menu.append((context.localize('history.list.set'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'set', 'history'], {'playlist_id': playlist_id, 'playlist_name': playlist_name}))) -def append_remove_my_subscriptions_filter(context_menu, provider, context, channel_name): +def append_remove_my_subscriptions_filter(context_menu, context, channel_name): if context.get_settings().get_bool('youtube.folder.my_subscriptions_filtered.show', False): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.remove.my_subscriptions.filter']), + context_menu.append((context.localize('my_subscriptions.filter.remove'), 'RunPlugin(%s)' % context.create_uri(['my_subscriptions', 'filter'], {'channel_name': channel_name, 'action': 'remove'}))) -def append_add_my_subscriptions_filter(context_menu, provider, context, channel_name): +def append_add_my_subscriptions_filter(context_menu, context, channel_name): if context.get_settings().get_bool('youtube.folder.my_subscriptions_filtered.show', False): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.add.my_subscriptions.filter']), + context_menu.append((context.localize('my_subscriptions.filter.add'), 'RunPlugin(%s)' % context.create_uri(['my_subscriptions', 'filter'], {'channel_name': channel_name, 'action': 'add'}))) -def append_rate_video(context_menu, provider, context, video_id, refresh_container=False): +def append_rate_video(context_menu, context, video_id, refresh_container=False): refresh_container = '1' if refresh_container else '0' - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.rate']), + context_menu.append((context.localize('video.rate'), 'RunPlugin(%s)' % context.create_uri(['video', 'rate'], {'video_id': video_id, 'refresh_container': refresh_container}))) -def append_watch_later(context_menu, provider, context, playlist_id, video_id): +def append_watch_later(context_menu, context, playlist_id, video_id): playlist_path = kodion.utils.create_path('channel', 'mine', 'playlist', playlist_id) if playlist_id and playlist_path != context.get_path(): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.watch_later']), + context_menu.append((context.localize('watch_later'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'add', 'video'], {'playlist_id': playlist_id, 'video_id': video_id}))) -def append_go_to_channel(context_menu, provider, context, channel_id, channel_name): - text = context.localize(provider.LOCAL_MAP['youtube.go_to_channel']) % context.get_ui().bold(channel_name) +def append_go_to_channel(context_menu, context, channel_id, channel_name): + text = context.localize('go_to_channel') % context.get_ui().bold(channel_name) context_menu.append((text, 'Container.Update(%s)' % context.create_uri(['channel', channel_id]))) -def append_related_videos(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.related_videos']), +def append_related_videos(context_menu, context, video_id): + context_menu.append((context.localize('related_videos'), 'Container.Update(%s)' % context.create_uri(['special', 'related_videos'], {'video_id': video_id}))) -def append_clear_watch_history(context_menu, provider, context): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.clear_history']), +def append_clear_watch_history(context_menu, context): + context_menu.append((context.localize('clear_history'), 'Container.Update(%s)' % context.create_uri(['history', 'clear']))) -def append_refresh(context_menu, provider, context): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.refresh']), 'Container.Refresh')) +def append_refresh(context_menu, context): + context_menu.append((context.localize('refresh'), 'Container.Refresh')) -def append_subscribe_to_channel(context_menu, provider, context, channel_id, channel_name=''): +def append_subscribe_to_channel(context_menu, context, channel_id, channel_name=''): if channel_name: - text = context.localize(provider.LOCAL_MAP['youtube.subscribe_to']) % context.get_ui().bold(channel_name) + text = context.localize('subscribe_to') % context.get_ui().bold(channel_name) context_menu.append( (text, 'RunPlugin(%s)' % context.create_uri(['subscriptions', 'add'], {'subscription_id': channel_id}))) else: - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.subscribe']), + context_menu.append((context.localize('subscribe'), 'RunPlugin(%s)' % context.create_uri(['subscriptions', 'add'], {'subscription_id': channel_id}))) -def append_unsubscribe_from_channel(context_menu, provider, context, channel_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.unsubscribe']), +def append_unsubscribe_from_channel(context_menu, context, channel_id): + context_menu.append((context.localize('unsubscribe'), 'RunPlugin(%s)' % context.create_uri(['subscriptions', 'remove'], {'subscription_id': channel_id}))) -def append_mark_watched(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.mark.watched']), +def append_mark_watched(context_menu, context, video_id): + context_menu.append((context.localize('mark.watched'), 'RunPlugin(%s)' % context.create_uri(['playback_history'], {'video_id': video_id, 'action': 'mark_watched'}))) -def append_mark_unwatched(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.mark.unwatched']), +def append_mark_unwatched(context_menu, context, video_id): + context_menu.append((context.localize('mark.unwatched'), 'RunPlugin(%s)' % context.create_uri(['playback_history'], {'video_id': video_id, 'action': 'mark_unwatched'}))) -def append_reset_resume_point(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.reset.resume.point']), +def append_reset_resume_point(context_menu, context, video_id): + context_menu.append((context.localize('reset.resume_point'), 'RunPlugin(%s)' % context.create_uri(['playback_history'], {'video_id': video_id, 'action': 'reset_resume'}))) -def append_play_with_subtitles(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.play_with_subtitles']), +def append_play_with_subtitles(context_menu, context, video_id): + context_menu.append((context.localize('video.play.with_subtitles'), 'RunPlugin(%s)' % context.create_uri(['play'], {'video_id': video_id, 'prompt_for_subtitles': '1'}))) -def append_play_audio_only(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.play_audio_only']), +def append_play_audio_only(context_menu, context, video_id): + context_menu.append((context.localize('video.play.audio_only'), 'RunPlugin(%s)' % context.create_uri(['play'], {'video_id': video_id, 'audio_only': '1'}))) -def append_play_ask_for_quality(context_menu, provider, context, video_id): - context_menu.append((context.localize(provider.LOCAL_MAP['youtube.video.play_ask_for_quality']), +def append_play_ask_for_quality(context_menu, context, video_id): + context_menu.append((context.localize('video.play.ask_for_quality'), 'RunPlugin(%s)' % context.create_uri(['play'], {'video_id': video_id, 'ask_for_quality': '1'}))) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index f06b86509..fc96c5662 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -60,12 +60,12 @@ def _do_login(_for_tv=False): user_code = json_data['user_code'] verification_url = json_data.get('verification_url', 'youtube.com/activate').lstrip('https://www.') - text = [context.localize(provider.LOCAL_MAP['youtube.sign.go_to']) % context.get_ui().bold(verification_url), - '[CR]%s %s' % (context.localize(provider.LOCAL_MAP['youtube.sign.enter_code']), + text = [context.localize('sign.go_to') % context.get_ui().bold(verification_url), + '[CR]%s %s' % (context.localize('sign.enter_code'), context.get_ui().bold(user_code))] text = ''.join(text) dialog = context.get_ui().create_progress_dialog( - heading=context.localize(provider.LOCAL_MAP['youtube.sign.in']), text=text, background=False) + heading=context.localize('sign.in'), text=text, background=False) steps = ((10 * 60 * 1000) // interval) # 10 Minutes dialog.set_total(steps) @@ -115,8 +115,8 @@ def _do_login(_for_tv=False): context.get_ui().refresh_container() elif mode == 'in': - context.get_ui().on_ok(context.localize(provider.LOCAL_MAP['youtube.sign.twice.title']), - context.localize(provider.LOCAL_MAP['youtube.sign.twice.text'])) + context.get_ui().on_ok(context.localize('sign.twice.title'), + context.localize('sign.twice.text')) access_token_tv, expires_in_tv, refresh_token_tv = _do_login(_for_tv=True) # abort tv login diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 3d21507f3..65ada0f74 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -16,7 +16,6 @@ import xbmcplugin from ... import kodion -from ...kodion import constants from ...kodion.items import VideoItem from ...kodion.ui.xbmc.xbmc_items import to_playback_item from ...youtube.youtube_exceptions import YouTubeException @@ -54,7 +53,7 @@ def play_video(provider, context): return False if not video_streams: - message = context.localize(provider.LOCAL_MAP['youtube.error.no_video_streams_found']) + message = context.localize('error.no_video_streams_found') context.get_ui().show_notification(message, time_milliseconds=5000) return False @@ -67,7 +66,7 @@ def play_video(provider, context): is_live = video_stream.get('Live') if is_video and video_stream['video'].get('rtmpe', False): - message = context.localize(provider.LOCAL_MAP['youtube.error.rtmpe_not_supported']) + message = context.localize('error.rtmpe_not_supported') context.get_ui().show_notification(message, time_milliseconds=5000) return False @@ -137,17 +136,17 @@ def play_playlist(provider, context): def _load_videos(_page_token='', _progress_dialog=None): if _progress_dialog is None: _progress_dialog = context.get_ui().create_progress_dialog( - context.localize(provider.LOCAL_MAP['youtube.playlist.progress.updating']), - context.localize(constants.localize.COMMON_PLEASE_WAIT), background=True) + context.localize('playlist.progress.updating'), + context.localize('please_wait'), background=True) json_data = client.get_playlist_items(playlist_id, page_token=_page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return None _progress_dialog.set_total(int(json_data.get('pageInfo', {}).get('totalResults', 0))) result = v3.response_to_items(provider, context, json_data, process_next_page=False) videos.extend(result) progress_text = '%s %d/%d' % ( - context.localize(constants.localize.COMMON_PLEASE_WAIT), len(videos), _progress_dialog.get_total()) + context.localize('please_wait'), len(videos), _progress_dialog.get_total()) _progress_dialog.update(steps=len(result), text=progress_text) next_page_token = json_data.get('nextPageToken', '') @@ -166,9 +165,9 @@ def _load_videos(_page_token='', _progress_dialog=None): order_list.append('shuffle') items = [] for order in order_list: - items.append((context.localize(provider.LOCAL_MAP['youtube.playlist.play.%s' % order]), order)) + items.append((context.localize('playlist.play.%s' % order), order)) - order = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.playlist.play.select']), items) + order = context.get_ui().on_select(context.localize('playlist.play.select'), items) if order not in order_list: return False @@ -234,7 +233,7 @@ def play_channel_live(provider, context): if index < 0: index = 0 json_data = provider.get_client(context).search(q='', search_type='video', event_type='live', channel_id=channel_id, safe_search=False) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False video_items = v3.response_to_items(provider, context, json_data, process_next_page=False) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 6d1a32d7d..d055238d1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -37,13 +37,13 @@ def _process_add_video(provider, context, keymap_action=False): if playlist_id != 'HL': json_data = client.add_video_to_playlist(playlist_id=playlist_id, video_id=video_id) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False if playlist_id == watch_later_id: - notify_message = context.localize(provider.LOCAL_MAP['youtube.added.to.watch.later']) + notify_message = context.localize('watch_later.added_to') else: - notify_message = context.localize(provider.LOCAL_MAP['youtube.added.to.playlist']) + notify_message = context.localize('playlist.added_to') context.get_ui().show_notification( message=notify_message, @@ -95,13 +95,13 @@ def _process_remove_video(provider, context): if context.get_ui().on_remove_content(video_name): json_data = provider.get_client(context).remove_video_from_playlist(playlist_id=playlist_id, playlist_item_id=video_id) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False context.get_ui().refresh_container() context.get_ui().show_notification( - message=context.localize(provider.LOCAL_MAP['youtube.removed.from.playlist']), + message=context.localize('playlist.removed_from'), time_milliseconds=2500, audible=False ) @@ -127,7 +127,7 @@ def _process_remove_playlist(provider, context): if context.get_ui().on_delete_content(playlist_name): json_data = provider.get_client(context).remove_playlist(playlist_id=playlist_id) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False context.get_ui().refresh_container() @@ -168,7 +168,7 @@ def _process_select_playlist(provider, context): items = [] if current_page == 1: # create playlist - items.append((ui.bold(context.localize(provider.LOCAL_MAP['youtube.playlist.create'])), '', + items.append((ui.bold(context.localize('playlist.create')), '', 'playlist.create', context.create_resource_path('media', 'playlist.png'))) # add the 'Watch Later' playlist @@ -177,7 +177,7 @@ def _process_select_playlist(provider, context): if 'watchLater' in my_playlists: watch_later_playlist_id = context.get_access_manager().get_watch_later_id() if watch_later_playlist_id: - items.append((ui.bold(context.localize(provider.LOCAL_MAP['youtube.watch_later'])), '', + items.append((ui.bold(context.localize('watch_later')), '', watch_later_playlist_id, context.create_resource_path('media', 'watch_later.png'))) for playlist in playlists: @@ -190,16 +190,16 @@ def _process_select_playlist(provider, context): items.append((title, description, playlist_id, thumbnail)) if page_token: - items.append((ui.bold(context.localize(provider.LOCAL_MAP['youtube.next_page'])).replace('%d', str(current_page + 1)), '', + items.append((ui.bold(context.localize('next_page')).replace('%d', str(current_page + 1)), '', 'playlist.next', 'DefaultFolder.png')) - result = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.playlist.select']), items) + result = context.get_ui().on_select(context.localize('playlist.select'), items) if result == 'playlist.create': result, text = context.get_ui().on_keyboard_input( - context.localize(provider.LOCAL_MAP['youtube.playlist.create'])) + context.localize('playlist.create')) if result and text: json_data = provider.get_client(context).create_playlist(title=text) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): break playlist_id = json_data.get('id', '') @@ -228,11 +228,11 @@ def _process_rename_playlist(provider, context): raise kodion.KodionException('playlist/rename: missing playlist_id') current_playlist_name = context.get_param('playlist_name', '') - result, text = context.get_ui().on_keyboard_input(context.localize(provider.LOCAL_MAP['youtube.rename']), + result, text = context.get_ui().on_keyboard_input(context.localize('rename'), default=current_playlist_name) if result and text: json_data = provider.get_client(context).rename_playlist(playlist_id=playlist_id, new_title=text) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return context.get_ui().refresh_container() @@ -247,12 +247,12 @@ def _watchlater_playlist_id_change(context, method): raise kodion.KodionException('watchlater_list/%s: missing playlist_name' % method) if method == 'set': - if context.get_ui().on_yes_no_input(context.get_name(), context.localize(30570) % playlist_name): + if context.get_ui().on_yes_no_input(context.get_name(), context.localize('watch_later.list.set.confirm') % playlist_name): context.get_access_manager().set_watch_later_id(playlist_id) else: return elif method == 'remove': - if context.get_ui().on_yes_no_input(context.get_name(), context.localize(30569) % playlist_name): + if context.get_ui().on_yes_no_input(context.get_name(), context.localize('watch_later.list.remove.confirm') % playlist_name): context.get_access_manager().set_watch_later_id(' WL') else: return @@ -270,12 +270,12 @@ def _history_playlist_id_change(context, method): raise kodion.KodionException('history_list/%s: missing playlist_name' % method) if method == 'set': - if context.get_ui().on_yes_no_input(context.get_name(), context.localize(30574) % playlist_name): + if context.get_ui().on_yes_no_input(context.get_name(), context.localize('history.list.set.confirm') % playlist_name): context.get_access_manager().set_watch_history_id(playlist_id) else: return elif method == 'remove': - if context.get_ui().on_yes_no_input(context.get_name(), context.localize(30573) % playlist_name): + if context.get_ui().on_yes_no_input(context.get_name(), context.localize('history.list.remove.confirm') % playlist_name): context.get_access_manager().set_watch_history_id('HL') else: return diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index c6fe1d609..372097040 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -12,53 +12,53 @@ DEFAULT_LANGUAGES = {'items': [{'snippet': {'name': 'Afrikaans', 'hl': 'af'}, 'id': 'af'}, {'snippet': {'name': 'Azerbaijani', 'hl': 'az'}, 'id': 'az'}, {'snippet': {'name': 'Indonesian', 'hl': 'id'}, 'id': 'id'}, {'snippet': {'name': 'Malay', 'hl': 'ms'}, 'id': 'ms'}, - {'snippet': {'name': 'Catalan', 'hl': 'ca'}, 'id': 'ca'}, {'snippet': {'name': 'Czech', 'hl': 'cs'}, 'id': 'cs'}, {'snippet': {'name': 'Danish', 'hl': 'da'}, 'id': 'da'}, {'snippet': {'name': 'German', 'hl': 'de'}, 'id': 'de'}, - {'snippet': {'name': 'Estonian', 'hl': 'et'}, 'id': 'et'}, {'snippet': {'name': 'English (United Kingdom)', 'hl': 'en-GB'}, 'id': 'en-GB'}, {'snippet': {'name': 'English', 'hl': 'en'}, 'id': 'en'}, - {'snippet': {'name': 'Spanish (Spain)', 'hl': 'es'}, 'id': 'es'}, {'snippet': {'name': 'Spanish (Latin America)', 'hl': 'es-419'}, 'id': 'es-419'}, {'snippet': {'name': 'Basque', 'hl': 'eu'}, 'id': 'eu'}, - {'snippet': {'name': 'Filipino', 'hl': 'fil'}, 'id': 'fil'}, {'snippet': {'name': 'French', 'hl': 'fr'}, 'id': 'fr'}, {'snippet': {'name': 'French (Canada)', 'hl': 'fr-CA'}, 'id': 'fr-CA'}, {'snippet': {'name': 'Galician', 'hl': 'gl'}, 'id': 'gl'}, - {'snippet': {'name': 'Croatian', 'hl': 'hr'}, 'id': 'hr'}, {'snippet': {'name': 'Zulu', 'hl': 'zu'}, 'id': 'zu'}, {'snippet': {'name': 'Icelandic', 'hl': 'is'}, 'id': 'is'}, {'snippet': {'name': 'Italian', 'hl': 'it'}, 'id': 'it'}, - {'snippet': {'name': 'Swahili', 'hl': 'sw'}, 'id': 'sw'}, {'snippet': {'name': 'Latvian', 'hl': 'lv'}, 'id': 'lv'}, {'snippet': {'name': 'Lithuanian', 'hl': 'lt'}, 'id': 'lt'}, {'snippet': {'name': 'Hungarian', 'hl': 'hu'}, 'id': 'hu'}, - {'snippet': {'name': 'Dutch', 'hl': 'nl'}, 'id': 'nl'}, {'snippet': {'name': 'Norwegian', 'hl': 'no'}, 'id': 'no'}, {'snippet': {'name': 'Uzbek', 'hl': 'uz'}, 'id': 'uz'}, {'snippet': {'name': 'Polish', 'hl': 'pl'}, 'id': 'pl'}, - {'snippet': {'name': 'Portuguese (Portugal)', 'hl': 'pt-PT'}, 'id': 'pt-PT'}, {'snippet': {'name': 'Portuguese (Brazil)', 'hl': 'pt'}, 'id': 'pt'}, {'snippet': {'name': 'Romanian', 'hl': 'ro'}, 'id': 'ro'}, - {'snippet': {'name': 'Albanian', 'hl': 'sq'}, 'id': 'sq'}, {'snippet': {'name': 'Slovak', 'hl': 'sk'}, 'id': 'sk'}, {'snippet': {'name': 'Slovenian', 'hl': 'sl'}, 'id': 'sl'}, {'snippet': {'name': 'Finnish', 'hl': 'fi'}, 'id': 'fi'}, - {'snippet': {'name': 'Swedish', 'hl': 'sv'}, 'id': 'sv'}, {'snippet': {'name': 'Vietnamese', 'hl': 'vi'}, 'id': 'vi'}, {'snippet': {'name': 'Turkish', 'hl': 'tr'}, 'id': 'tr'}, {'snippet': {'name': 'Bulgarian', 'hl': 'bg'}, 'id': 'bg'}, - {'snippet': {'name': 'Kyrgyz', 'hl': 'ky'}, 'id': 'ky'}, {'snippet': {'name': 'Kazakh', 'hl': 'kk'}, 'id': 'kk'}, {'snippet': {'name': 'Macedonian', 'hl': 'mk'}, 'id': 'mk'}, {'snippet': {'name': 'Mongolian', 'hl': 'mn'}, 'id': 'mn'}, - {'snippet': {'name': 'Russian', 'hl': 'ru'}, 'id': 'ru'}, {'snippet': {'name': 'Serbian', 'hl': 'sr'}, 'id': 'sr'}, {'snippet': {'name': 'Ukrainian', 'hl': 'uk'}, 'id': 'uk'}, {'snippet': {'name': 'Greek', 'hl': 'el'}, 'id': 'el'}, - {'snippet': {'name': 'Armenian', 'hl': 'hy'}, 'id': 'hy'}, {'snippet': {'name': 'Hebrew', 'hl': 'iw'}, 'id': 'iw'}, {'snippet': {'name': 'Urdu', 'hl': 'ur'}, 'id': 'ur'}, {'snippet': {'name': 'Arabic', 'hl': 'ar'}, 'id': 'ar'}, - {'snippet': {'name': 'Persian', 'hl': 'fa'}, 'id': 'fa'}, {'snippet': {'name': 'Nepali', 'hl': 'ne'}, 'id': 'ne'}, {'snippet': {'name': 'Marathi', 'hl': 'mr'}, 'id': 'mr'}, {'snippet': {'name': 'Hindi', 'hl': 'hi'}, 'id': 'hi'}, - {'snippet': {'name': 'Bengali', 'hl': 'bn'}, 'id': 'bn'}, {'snippet': {'name': 'Punjabi', 'hl': 'pa'}, 'id': 'pa'}, {'snippet': {'name': 'Gujarati', 'hl': 'gu'}, 'id': 'gu'}, {'snippet': {'name': 'Tamil', 'hl': 'ta'}, 'id': 'ta'}, - {'snippet': {'name': 'Telugu', 'hl': 'te'}, 'id': 'te'}, {'snippet': {'name': 'Kannada', 'hl': 'kn'}, 'id': 'kn'}, {'snippet': {'name': 'Malayalam', 'hl': 'ml'}, 'id': 'ml'}, {'snippet': {'name': 'Sinhala', 'hl': 'si'}, 'id': 'si'}, - {'snippet': {'name': 'Thai', 'hl': 'th'}, 'id': 'th'}, {'snippet': {'name': 'Lao', 'hl': 'lo'}, 'id': 'lo'}, {'snippet': {'name': 'Myanmar (Burmese)', 'hl': 'my'}, 'id': 'my'}, {'snippet': {'name': 'Georgian', 'hl': 'ka'}, 'id': 'ka'}, - {'snippet': {'name': 'Amharic', 'hl': 'am'}, 'id': 'am'}, {'snippet': {'name': 'Khmer', 'hl': 'km'}, 'id': 'km'}, {'snippet': {'name': 'Chinese', 'hl': 'zh-CN'}, 'id': 'zh-CN'}, {'snippet': {'name': 'Chinese (Taiwan)', 'hl': 'zh-TW'}, 'id': 'zh-TW'}, - {'snippet': {'name': 'Chinese (Hong Kong)', 'hl': 'zh-HK'}, 'id': 'zh-HK'}, {'snippet': {'name': 'Japanese', 'hl': 'ja'}, 'id': 'ja'}, {'snippet': {'name': 'Korean', 'hl': 'ko'}, 'id': 'ko'}]} + {'snippet': {'name': 'Catalan', 'hl': 'ca'}, 'id': 'ca'}, {'snippet': {'name': 'Czech', 'hl': 'cs'}, 'id': 'cs'}, {'snippet': {'name': 'Danish', 'hl': 'da'}, 'id': 'da'}, {'snippet': {'name': 'German', 'hl': 'de'}, 'id': 'de'}, + {'snippet': {'name': 'Estonian', 'hl': 'et'}, 'id': 'et'}, {'snippet': {'name': 'English (United Kingdom)', 'hl': 'en-GB'}, 'id': 'en-GB'}, {'snippet': {'name': 'English', 'hl': 'en'}, 'id': 'en'}, + {'snippet': {'name': 'Spanish (Spain)', 'hl': 'es'}, 'id': 'es'}, {'snippet': {'name': 'Spanish (Latin America)', 'hl': 'es-419'}, 'id': 'es-419'}, {'snippet': {'name': 'Basque', 'hl': 'eu'}, 'id': 'eu'}, + {'snippet': {'name': 'Filipino', 'hl': 'fil'}, 'id': 'fil'}, {'snippet': {'name': 'French', 'hl': 'fr'}, 'id': 'fr'}, {'snippet': {'name': 'French (Canada)', 'hl': 'fr-CA'}, 'id': 'fr-CA'}, {'snippet': {'name': 'Galician', 'hl': 'gl'}, 'id': 'gl'}, + {'snippet': {'name': 'Croatian', 'hl': 'hr'}, 'id': 'hr'}, {'snippet': {'name': 'Zulu', 'hl': 'zu'}, 'id': 'zu'}, {'snippet': {'name': 'Icelandic', 'hl': 'is'}, 'id': 'is'}, {'snippet': {'name': 'Italian', 'hl': 'it'}, 'id': 'it'}, + {'snippet': {'name': 'Swahili', 'hl': 'sw'}, 'id': 'sw'}, {'snippet': {'name': 'Latvian', 'hl': 'lv'}, 'id': 'lv'}, {'snippet': {'name': 'Lithuanian', 'hl': 'lt'}, 'id': 'lt'}, {'snippet': {'name': 'Hungarian', 'hl': 'hu'}, 'id': 'hu'}, + {'snippet': {'name': 'Dutch', 'hl': 'nl'}, 'id': 'nl'}, {'snippet': {'name': 'Norwegian', 'hl': 'no'}, 'id': 'no'}, {'snippet': {'name': 'Uzbek', 'hl': 'uz'}, 'id': 'uz'}, {'snippet': {'name': 'Polish', 'hl': 'pl'}, 'id': 'pl'}, + {'snippet': {'name': 'Portuguese (Portugal)', 'hl': 'pt-PT'}, 'id': 'pt-PT'}, {'snippet': {'name': 'Portuguese (Brazil)', 'hl': 'pt'}, 'id': 'pt'}, {'snippet': {'name': 'Romanian', 'hl': 'ro'}, 'id': 'ro'}, + {'snippet': {'name': 'Albanian', 'hl': 'sq'}, 'id': 'sq'}, {'snippet': {'name': 'Slovak', 'hl': 'sk'}, 'id': 'sk'}, {'snippet': {'name': 'Slovenian', 'hl': 'sl'}, 'id': 'sl'}, {'snippet': {'name': 'Finnish', 'hl': 'fi'}, 'id': 'fi'}, + {'snippet': {'name': 'Swedish', 'hl': 'sv'}, 'id': 'sv'}, {'snippet': {'name': 'Vietnamese', 'hl': 'vi'}, 'id': 'vi'}, {'snippet': {'name': 'Turkish', 'hl': 'tr'}, 'id': 'tr'}, {'snippet': {'name': 'Bulgarian', 'hl': 'bg'}, 'id': 'bg'}, + {'snippet': {'name': 'Kyrgyz', 'hl': 'ky'}, 'id': 'ky'}, {'snippet': {'name': 'Kazakh', 'hl': 'kk'}, 'id': 'kk'}, {'snippet': {'name': 'Macedonian', 'hl': 'mk'}, 'id': 'mk'}, {'snippet': {'name': 'Mongolian', 'hl': 'mn'}, 'id': 'mn'}, + {'snippet': {'name': 'Russian', 'hl': 'ru'}, 'id': 'ru'}, {'snippet': {'name': 'Serbian', 'hl': 'sr'}, 'id': 'sr'}, {'snippet': {'name': 'Ukrainian', 'hl': 'uk'}, 'id': 'uk'}, {'snippet': {'name': 'Greek', 'hl': 'el'}, 'id': 'el'}, + {'snippet': {'name': 'Armenian', 'hl': 'hy'}, 'id': 'hy'}, {'snippet': {'name': 'Hebrew', 'hl': 'iw'}, 'id': 'iw'}, {'snippet': {'name': 'Urdu', 'hl': 'ur'}, 'id': 'ur'}, {'snippet': {'name': 'Arabic', 'hl': 'ar'}, 'id': 'ar'}, + {'snippet': {'name': 'Persian', 'hl': 'fa'}, 'id': 'fa'}, {'snippet': {'name': 'Nepali', 'hl': 'ne'}, 'id': 'ne'}, {'snippet': {'name': 'Marathi', 'hl': 'mr'}, 'id': 'mr'}, {'snippet': {'name': 'Hindi', 'hl': 'hi'}, 'id': 'hi'}, + {'snippet': {'name': 'Bengali', 'hl': 'bn'}, 'id': 'bn'}, {'snippet': {'name': 'Punjabi', 'hl': 'pa'}, 'id': 'pa'}, {'snippet': {'name': 'Gujarati', 'hl': 'gu'}, 'id': 'gu'}, {'snippet': {'name': 'Tamil', 'hl': 'ta'}, 'id': 'ta'}, + {'snippet': {'name': 'Telugu', 'hl': 'te'}, 'id': 'te'}, {'snippet': {'name': 'Kannada', 'hl': 'kn'}, 'id': 'kn'}, {'snippet': {'name': 'Malayalam', 'hl': 'ml'}, 'id': 'ml'}, {'snippet': {'name': 'Sinhala', 'hl': 'si'}, 'id': 'si'}, + {'snippet': {'name': 'Thai', 'hl': 'th'}, 'id': 'th'}, {'snippet': {'name': 'Lao', 'hl': 'lo'}, 'id': 'lo'}, {'snippet': {'name': 'Myanmar (Burmese)', 'hl': 'my'}, 'id': 'my'}, {'snippet': {'name': 'Georgian', 'hl': 'ka'}, 'id': 'ka'}, + {'snippet': {'name': 'Amharic', 'hl': 'am'}, 'id': 'am'}, {'snippet': {'name': 'Khmer', 'hl': 'km'}, 'id': 'km'}, {'snippet': {'name': 'Chinese', 'hl': 'zh-CN'}, 'id': 'zh-CN'}, {'snippet': {'name': 'Chinese (Taiwan)', 'hl': 'zh-TW'}, 'id': 'zh-TW'}, + {'snippet': {'name': 'Chinese (Hong Kong)', 'hl': 'zh-HK'}, 'id': 'zh-HK'}, {'snippet': {'name': 'Japanese', 'hl': 'ja'}, 'id': 'ja'}, {'snippet': {'name': 'Korean', 'hl': 'ko'}, 'id': 'ko'}]} DEFAULT_REGIONS = {'items': [{'snippet': {'gl': 'DZ', 'name': 'Algeria'}, 'id': 'DZ'}, {'snippet': {'gl': 'AR', 'name': 'Argentina'}, 'id': 'AR'}, {'snippet': {'gl': 'AU', 'name': 'Australia'}, 'id': 'AU'}, {'snippet': {'gl': 'AT', 'name': 'Austria'}, 'id': 'AT'}, - {'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}, 'id': 'AZ'}, {'snippet': {'gl': 'BH', 'name': 'Bahrain'}, 'id': 'BH'}, {'snippet': {'gl': 'BY', 'name': 'Belarus'}, 'id': 'BY'}, {'snippet': {'gl': 'BE', 'name': 'Belgium'}, 'id': 'BE'}, - {'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}, 'id': 'BA'}, {'snippet': {'gl': 'BR', 'name': 'Brazil'}, 'id': 'BR'}, {'snippet': {'gl': 'BG', 'name': 'Bulgaria'}, 'id': 'BG'}, {'snippet': {'gl': 'CA', 'name': 'Canada'}, 'id': 'CA'}, - {'snippet': {'gl': 'CL', 'name': 'Chile'}, 'id': 'CL'}, {'snippet': {'gl': 'CO', 'name': 'Colombia'}, 'id': 'CO'}, {'snippet': {'gl': 'HR', 'name': 'Croatia'}, 'id': 'HR'}, {'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}, 'id': 'CZ'}, - {'snippet': {'gl': 'DK', 'name': 'Denmark'}, 'id': 'DK'}, {'snippet': {'gl': 'EG', 'name': 'Egypt'}, 'id': 'EG'}, {'snippet': {'gl': 'EE', 'name': 'Estonia'}, 'id': 'EE'}, {'snippet': {'gl': 'FI', 'name': 'Finland'}, 'id': 'FI'}, - {'snippet': {'gl': 'FR', 'name': 'France'}, 'id': 'FR'}, {'snippet': {'gl': 'GE', 'name': 'Georgia'}, 'id': 'GE'}, {'snippet': {'gl': 'DE', 'name': 'Germany'}, 'id': 'DE'}, {'snippet': {'gl': 'GH', 'name': 'Ghana'}, 'id': 'GH'}, - {'snippet': {'gl': 'GR', 'name': 'Greece'}, 'id': 'GR'}, {'snippet': {'gl': 'HK', 'name': 'Hong Kong'}, 'id': 'HK'}, {'snippet': {'gl': 'HU', 'name': 'Hungary'}, 'id': 'HU'}, {'snippet': {'gl': 'IS', 'name': 'Iceland'}, 'id': 'IS'}, - {'snippet': {'gl': 'IN', 'name': 'India'}, 'id': 'IN'}, {'snippet': {'gl': 'ID', 'name': 'Indonesia'}, 'id': 'ID'}, {'snippet': {'gl': 'IQ', 'name': 'Iraq'}, 'id': 'IQ'}, {'snippet': {'gl': 'IE', 'name': 'Ireland'}, 'id': 'IE'}, - {'snippet': {'gl': 'IL', 'name': 'Israel'}, 'id': 'IL'}, {'snippet': {'gl': 'IT', 'name': 'Italy'}, 'id': 'IT'}, {'snippet': {'gl': 'JM', 'name': 'Jamaica'}, 'id': 'JM'}, {'snippet': {'gl': 'JP', 'name': 'Japan'}, 'id': 'JP'}, - {'snippet': {'gl': 'JO', 'name': 'Jordan'}, 'id': 'JO'}, {'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}, 'id': 'KZ'}, {'snippet': {'gl': 'KE', 'name': 'Kenya'}, 'id': 'KE'}, {'snippet': {'gl': 'KW', 'name': 'Kuwait'}, 'id': 'KW'}, - {'snippet': {'gl': 'LV', 'name': 'Latvia'}, 'id': 'LV'}, {'snippet': {'gl': 'LB', 'name': 'Lebanon'}, 'id': 'LB'}, {'snippet': {'gl': 'LY', 'name': 'Libya'}, 'id': 'LY'}, {'snippet': {'gl': 'LT', 'name': 'Lithuania'}, 'id': 'LT'}, - {'snippet': {'gl': 'LU', 'name': 'Luxembourg'}, 'id': 'LU'}, {'snippet': {'gl': 'MK', 'name': 'Macedonia'}, 'id': 'MK'}, {'snippet': {'gl': 'MY', 'name': 'Malaysia'}, 'id': 'MY'}, {'snippet': {'gl': 'MX', 'name': 'Mexico'}, 'id': 'MX'}, - {'snippet': {'gl': 'ME', 'name': 'Montenegro'}, 'id': 'ME'}, {'snippet': {'gl': 'MA', 'name': 'Morocco'}, 'id': 'MA'}, {'snippet': {'gl': 'NP', 'name': 'Nepal'}, 'id': 'NP'}, {'snippet': {'gl': 'NL', 'name': 'Netherlands'}, 'id': 'NL'}, - {'snippet': {'gl': 'NZ', 'name': 'New Zealand'}, 'id': 'NZ'}, {'snippet': {'gl': 'NG', 'name': 'Nigeria'}, 'id': 'NG'}, {'snippet': {'gl': 'NO', 'name': 'Norway'}, 'id': 'NO'}, {'snippet': {'gl': 'OM', 'name': 'Oman'}, 'id': 'OM'}, - {'snippet': {'gl': 'PK', 'name': 'Pakistan'}, 'id': 'PK'}, {'snippet': {'gl': 'PE', 'name': 'Peru'}, 'id': 'PE'}, {'snippet': {'gl': 'PH', 'name': 'Philippines'}, 'id': 'PH'}, {'snippet': {'gl': 'PL', 'name': 'Poland'}, 'id': 'PL'}, - {'snippet': {'gl': 'PT', 'name': 'Portugal'}, 'id': 'PT'}, {'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}, 'id': 'PR'}, {'snippet': {'gl': 'QA', 'name': 'Qatar'}, 'id': 'QA'}, {'snippet': {'gl': 'RO', 'name': 'Romania'}, 'id': 'RO'}, - {'snippet': {'gl': 'RU', 'name': 'Russia'}, 'id': 'RU'}, {'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}, 'id': 'SA'}, {'snippet': {'gl': 'SN', 'name': 'Senegal'}, 'id': 'SN'}, {'snippet': {'gl': 'RS', 'name': 'Serbia'}, 'id': 'RS'}, - {'snippet': {'gl': 'SG', 'name': 'Singapore'}, 'id': 'SG'}, {'snippet': {'gl': 'SK', 'name': 'Slovakia'}, 'id': 'SK'}, {'snippet': {'gl': 'SI', 'name': 'Slovenia'}, 'id': 'SI'}, {'snippet': {'gl': 'ZA', 'name': 'South Africa'}, 'id': 'ZA'}, - {'snippet': {'gl': 'KR', 'name': 'South Korea'}, 'id': 'KR'}, {'snippet': {'gl': 'ES', 'name': 'Spain'}, 'id': 'ES'}, {'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}, 'id': 'LK'}, {'snippet': {'gl': 'SE', 'name': 'Sweden'}, 'id': 'SE'}, - {'snippet': {'gl': 'CH', 'name': 'Switzerland'}, 'id': 'CH'}, {'snippet': {'gl': 'TW', 'name': 'Taiwan'}, 'id': 'TW'}, {'snippet': {'gl': 'TZ', 'name': 'Tanzania'}, 'id': 'TZ'}, {'snippet': {'gl': 'TH', 'name': 'Thailand'}, 'id': 'TH'}, - {'snippet': {'gl': 'TN', 'name': 'Tunisia'}, 'id': 'TN'}, {'snippet': {'gl': 'TR', 'name': 'Turkey'}, 'id': 'TR'}, {'snippet': {'gl': 'UG', 'name': 'Uganda'}, 'id': 'UG'}, {'snippet': {'gl': 'UA', 'name': 'Ukraine'}, 'id': 'UA'}, - {'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}, 'id': 'AE'}, {'snippet': {'gl': 'GB', 'name': 'United Kingdom'}, 'id': 'GB'}, {'snippet': {'gl': 'US', 'name': 'United States'}, 'id': 'US'}, {'snippet': {'gl': 'VN', 'name': 'Vietnam'}, 'id': 'VN'}, - {'snippet': {'gl': 'YE', 'name': 'Yemen'}, 'id': 'YE'}, {'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}, 'id': 'ZW'}]} + {'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}, 'id': 'AZ'}, {'snippet': {'gl': 'BH', 'name': 'Bahrain'}, 'id': 'BH'}, {'snippet': {'gl': 'BY', 'name': 'Belarus'}, 'id': 'BY'}, {'snippet': {'gl': 'BE', 'name': 'Belgium'}, 'id': 'BE'}, + {'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}, 'id': 'BA'}, {'snippet': {'gl': 'BR', 'name': 'Brazil'}, 'id': 'BR'}, {'snippet': {'gl': 'BG', 'name': 'Bulgaria'}, 'id': 'BG'}, {'snippet': {'gl': 'CA', 'name': 'Canada'}, 'id': 'CA'}, + {'snippet': {'gl': 'CL', 'name': 'Chile'}, 'id': 'CL'}, {'snippet': {'gl': 'CO', 'name': 'Colombia'}, 'id': 'CO'}, {'snippet': {'gl': 'HR', 'name': 'Croatia'}, 'id': 'HR'}, {'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}, 'id': 'CZ'}, + {'snippet': {'gl': 'DK', 'name': 'Denmark'}, 'id': 'DK'}, {'snippet': {'gl': 'EG', 'name': 'Egypt'}, 'id': 'EG'}, {'snippet': {'gl': 'EE', 'name': 'Estonia'}, 'id': 'EE'}, {'snippet': {'gl': 'FI', 'name': 'Finland'}, 'id': 'FI'}, + {'snippet': {'gl': 'FR', 'name': 'France'}, 'id': 'FR'}, {'snippet': {'gl': 'GE', 'name': 'Georgia'}, 'id': 'GE'}, {'snippet': {'gl': 'DE', 'name': 'Germany'}, 'id': 'DE'}, {'snippet': {'gl': 'GH', 'name': 'Ghana'}, 'id': 'GH'}, + {'snippet': {'gl': 'GR', 'name': 'Greece'}, 'id': 'GR'}, {'snippet': {'gl': 'HK', 'name': 'Hong Kong'}, 'id': 'HK'}, {'snippet': {'gl': 'HU', 'name': 'Hungary'}, 'id': 'HU'}, {'snippet': {'gl': 'IS', 'name': 'Iceland'}, 'id': 'IS'}, + {'snippet': {'gl': 'IN', 'name': 'India'}, 'id': 'IN'}, {'snippet': {'gl': 'ID', 'name': 'Indonesia'}, 'id': 'ID'}, {'snippet': {'gl': 'IQ', 'name': 'Iraq'}, 'id': 'IQ'}, {'snippet': {'gl': 'IE', 'name': 'Ireland'}, 'id': 'IE'}, + {'snippet': {'gl': 'IL', 'name': 'Israel'}, 'id': 'IL'}, {'snippet': {'gl': 'IT', 'name': 'Italy'}, 'id': 'IT'}, {'snippet': {'gl': 'JM', 'name': 'Jamaica'}, 'id': 'JM'}, {'snippet': {'gl': 'JP', 'name': 'Japan'}, 'id': 'JP'}, + {'snippet': {'gl': 'JO', 'name': 'Jordan'}, 'id': 'JO'}, {'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}, 'id': 'KZ'}, {'snippet': {'gl': 'KE', 'name': 'Kenya'}, 'id': 'KE'}, {'snippet': {'gl': 'KW', 'name': 'Kuwait'}, 'id': 'KW'}, + {'snippet': {'gl': 'LV', 'name': 'Latvia'}, 'id': 'LV'}, {'snippet': {'gl': 'LB', 'name': 'Lebanon'}, 'id': 'LB'}, {'snippet': {'gl': 'LY', 'name': 'Libya'}, 'id': 'LY'}, {'snippet': {'gl': 'LT', 'name': 'Lithuania'}, 'id': 'LT'}, + {'snippet': {'gl': 'LU', 'name': 'Luxembourg'}, 'id': 'LU'}, {'snippet': {'gl': 'MK', 'name': 'Macedonia'}, 'id': 'MK'}, {'snippet': {'gl': 'MY', 'name': 'Malaysia'}, 'id': 'MY'}, {'snippet': {'gl': 'MX', 'name': 'Mexico'}, 'id': 'MX'}, + {'snippet': {'gl': 'ME', 'name': 'Montenegro'}, 'id': 'ME'}, {'snippet': {'gl': 'MA', 'name': 'Morocco'}, 'id': 'MA'}, {'snippet': {'gl': 'NP', 'name': 'Nepal'}, 'id': 'NP'}, {'snippet': {'gl': 'NL', 'name': 'Netherlands'}, 'id': 'NL'}, + {'snippet': {'gl': 'NZ', 'name': 'New Zealand'}, 'id': 'NZ'}, {'snippet': {'gl': 'NG', 'name': 'Nigeria'}, 'id': 'NG'}, {'snippet': {'gl': 'NO', 'name': 'Norway'}, 'id': 'NO'}, {'snippet': {'gl': 'OM', 'name': 'Oman'}, 'id': 'OM'}, + {'snippet': {'gl': 'PK', 'name': 'Pakistan'}, 'id': 'PK'}, {'snippet': {'gl': 'PE', 'name': 'Peru'}, 'id': 'PE'}, {'snippet': {'gl': 'PH', 'name': 'Philippines'}, 'id': 'PH'}, {'snippet': {'gl': 'PL', 'name': 'Poland'}, 'id': 'PL'}, + {'snippet': {'gl': 'PT', 'name': 'Portugal'}, 'id': 'PT'}, {'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}, 'id': 'PR'}, {'snippet': {'gl': 'QA', 'name': 'Qatar'}, 'id': 'QA'}, {'snippet': {'gl': 'RO', 'name': 'Romania'}, 'id': 'RO'}, + {'snippet': {'gl': 'RU', 'name': 'Russia'}, 'id': 'RU'}, {'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}, 'id': 'SA'}, {'snippet': {'gl': 'SN', 'name': 'Senegal'}, 'id': 'SN'}, {'snippet': {'gl': 'RS', 'name': 'Serbia'}, 'id': 'RS'}, + {'snippet': {'gl': 'SG', 'name': 'Singapore'}, 'id': 'SG'}, {'snippet': {'gl': 'SK', 'name': 'Slovakia'}, 'id': 'SK'}, {'snippet': {'gl': 'SI', 'name': 'Slovenia'}, 'id': 'SI'}, {'snippet': {'gl': 'ZA', 'name': 'South Africa'}, 'id': 'ZA'}, + {'snippet': {'gl': 'KR', 'name': 'South Korea'}, 'id': 'KR'}, {'snippet': {'gl': 'ES', 'name': 'Spain'}, 'id': 'ES'}, {'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}, 'id': 'LK'}, {'snippet': {'gl': 'SE', 'name': 'Sweden'}, 'id': 'SE'}, + {'snippet': {'gl': 'CH', 'name': 'Switzerland'}, 'id': 'CH'}, {'snippet': {'gl': 'TW', 'name': 'Taiwan'}, 'id': 'TW'}, {'snippet': {'gl': 'TZ', 'name': 'Tanzania'}, 'id': 'TZ'}, {'snippet': {'gl': 'TH', 'name': 'Thailand'}, 'id': 'TH'}, + {'snippet': {'gl': 'TN', 'name': 'Tunisia'}, 'id': 'TN'}, {'snippet': {'gl': 'TR', 'name': 'Turkey'}, 'id': 'TR'}, {'snippet': {'gl': 'UG', 'name': 'Uganda'}, 'id': 'UG'}, {'snippet': {'gl': 'UA', 'name': 'Ukraine'}, 'id': 'UA'}, + {'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}, 'id': 'AE'}, {'snippet': {'gl': 'GB', 'name': 'United Kingdom'}, 'id': 'GB'}, {'snippet': {'gl': 'US', 'name': 'United States'}, 'id': 'US'}, {'snippet': {'gl': 'VN', 'name': 'Vietnam'}, 'id': 'VN'}, + {'snippet': {'gl': 'YE', 'name': 'Yemen'}, 'id': 'YE'}, {'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}, 'id': 'ZW'}]} def _process_language(provider, context): - if not context.get_ui().on_yes_no_input(context.localize(provider.LOCAL_MAP['youtube.setup_wizard.adjust']), - context.localize(provider.LOCAL_MAP['youtube.setup_wizard.adjust.language_and_region'])): + if not context.get_ui().on_yes_no_input(context.localize('setup_wizard.adjust'), + context.localize('setup_wizard.adjust.language_and_region')): return client = provider.get_client(context) @@ -79,7 +79,7 @@ def _process_language(provider, context): language_list.append((language_name, hl)) language_list = sorted(language_list, key=lambda x: x[0]) language_id = context.get_ui().on_select( - context.localize(provider.LOCAL_MAP['youtube.setup_wizard.select_language']), language_list) + context.localize('setup_wizard.select_language'), language_list) if language_id == -1: return @@ -94,7 +94,7 @@ def _process_language(provider, context): gl = item['snippet']['gl'] region_list.append((region_name, gl)) region_list = sorted(region_list, key=lambda x: x[0]) - region_id = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.setup_wizard.select_region']), + region_id = context.get_ui().on_select(context.localize('setup_wizard.select_region'), region_list) if region_id == -1: return @@ -105,8 +105,8 @@ def _process_language(provider, context): provider.reset_client() -def _process_geo_location(provider, context): - if not context.get_ui().on_yes_no_input(context.get_name(), context.localize(provider.LOCAL_MAP['youtube.perform.geolocation'])): +def _process_geo_location(context): + if not context.get_ui().on_yes_no_input(context.get_name(), context.localize('perform.geolocation')): return locator = Locator() @@ -118,4 +118,4 @@ def _process_geo_location(provider, context): def process(provider, context): _process_language(provider, context) - _process_geo_location(provider, context) + _process_geo_location(context) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 5cd5935da..3d8a56e55 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -22,7 +22,7 @@ def _process_related_videos(provider, context): video_id = context.get_param('video_id', '') if video_id: json_data = provider.get_client(context).get_related_videos(video_id=video_id, page_token=page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data, process_next_page=False)) @@ -37,7 +37,7 @@ def _process_parent_comments(provider, context): video_id = context.get_param('video_id', '') if video_id: json_data = provider.get_client(context).get_parent_comments(video_id=video_id, page_token=page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) @@ -52,7 +52,7 @@ def _process_child_comments(provider, context): parent_id = context.get_param('parent_id', '') if parent_id: json_data = provider.get_client(context).get_child_comments(parent_id=parent_id, page_token=page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) @@ -65,7 +65,7 @@ def _process_recommendations(provider, context): page_token = context.get_param('page_token', '') json_data = provider.get_client(context).get_activities('home', page_token=page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) return result @@ -77,7 +77,7 @@ def _process_popular_right_now(provider, context): page_token = context.get_param('page_token', '') json_data = provider.get_client(context).get_popular_videos(page_token=page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) @@ -94,12 +94,12 @@ def _process_browse_channels(provider, context): if guide_id: json_data = client.get_guide_category(guide_id) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) else: json_data = context.get_function_cache().get(kodion.utils.FunctionCache.ONE_MONTH, client.get_guide_categories) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) @@ -112,7 +112,7 @@ def _process_disliked_videos(provider, context): page_token = context.get_param('page_token', '') json_data = provider.get_client(context).get_disliked_videos(page_token=page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) return result @@ -130,7 +130,7 @@ def _sort(x): location = context.get_param('location', False) json_data = provider.get_client(context).get_live_events(event_type=event_type, page_token=page_token, location=location) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data, sort=_sort, reverse_sort=True)) @@ -148,7 +148,7 @@ def _extract_urls(_video_id): result = [] progress_dialog = \ - context.get_ui().create_progress_dialog(heading=context.localize(kodion.constants.localize.COMMON_PLEASE_WAIT), + context.get_ui().create_progress_dialog(heading=context.localize('please_wait'), background=False) resource_manager = provider.get_resource_manager(context) @@ -185,9 +185,8 @@ def _extract_urls(_video_id): if not result: progress_dialog.close() - context.get_ui().on_ok(title=context.localize(provider.LOCAL_MAP['youtube.video.description.links']), - text=context.localize( - provider.LOCAL_MAP['youtube.video.description.links.not_found'])) + context.get_ui().on_ok(title=context.localize('video.description.links'), + text=context.localize('video.description.links.not_found')) return False return result diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index 712308492..fd01629bd 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -19,7 +19,7 @@ def _process_list(provider, context): page_token = context.get_param('page_token', '') # no caching json_data = provider.get_client(context).get_subscription('mine', page_token=page_token) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return [] result.extend(v3.response_to_items(provider, context, json_data)) @@ -36,11 +36,11 @@ def _process_add(provider, context): if subscription_id: json_data = provider.get_client(context).subscribe(subscription_id) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False context.get_ui().show_notification( - context.localize(provider.LOCAL_MAP['youtube.subscribed.to.channel']), + context.localize('subscribed.to.channel'), time_milliseconds=2500, audible=False ) @@ -59,13 +59,13 @@ def _process_remove(provider, context): if subscription_id: json_data = provider.get_client(context).unsubscribe(subscription_id) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False context.get_ui().refresh_container() context.get_ui().show_notification( - context.localize(provider.LOCAL_MAP['youtube.unsubscribed.from.channel']), + context.localize('unsubscribed.from.channel'), time_milliseconds=2500, audible=False ) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 81728600e..b1362406f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -39,7 +39,7 @@ def _process_rate_video(provider, context, re_match): if not current_rating: client = provider.get_client(context) json_data = client.get_video_rating(video_id) - if not v3.handle_error(provider, context, json_data): + if not v3.handle_error(context, json_data): return False items = json_data.get('items', []) @@ -50,8 +50,8 @@ def _process_rate_video(provider, context, re_match): if not rating_param: for rating in ratings: if rating != current_rating: - rating_items.append((context.localize(provider.LOCAL_MAP['youtube.video.rate.%s' % rating]), rating)) - result = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.video.rate']), rating_items) + rating_items.append((context.localize('video.rate.%s' % rating), rating)) + result = context.get_ui().on_select(context.localize('video.rate'), rating_items) elif rating_param != current_rating: result = rating_param else: @@ -63,7 +63,7 @@ def _process_rate_video(provider, context, re_match): response = provider.get_client(context).rate_video(video_id, result) if response.get('status_code') != 204: - notify_message = context.localize(provider.LOCAL_MAP['youtube.failed']) + notify_message = context.localize('failed') elif response.get('status_code') == 204: # this will be set if we are in the 'Liked Video' playlist @@ -71,11 +71,11 @@ def _process_rate_video(provider, context, re_match): context.get_ui().refresh_container() if result == 'none': - notify_message = context.localize(provider.LOCAL_MAP['youtube.unrated.video']) + notify_message = context.localize('unrated.video') elif result == 'like': - notify_message = context.localize(provider.LOCAL_MAP['youtube.liked.video']) + notify_message = context.localize('liked.video') elif result == 'dislike': - notify_message = context.localize(provider.LOCAL_MAP['youtube.disliked.video']) + notify_message = context.localize('disliked.video') if notify_message: context.get_ui().show_notification( @@ -87,7 +87,7 @@ def _process_rate_video(provider, context, re_match): return True -def _process_more_for_video(provider, context): +def _process_more_for_video(context): video_id = context.get_param('video_id', '') if not video_id: raise kodion.KodionException('video/more/: missing video_id') @@ -97,27 +97,26 @@ def _process_more_for_video(provider, context): is_logged_in = context.get_param('logged_in', '0') if is_logged_in == '1': # add video to a playlist - items.append((context.localize(provider.LOCAL_MAP['youtube.video.add_to_playlist']), + items.append((context.localize('video.add_to_playlist'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'select', 'playlist'], {'video_id': video_id}))) - # default items - items.extend([(context.localize(provider.LOCAL_MAP['youtube.related_videos']), + items.extend([(context.localize('related_videos'), 'Container.Update(%s)' % context.create_uri(['special', 'related_videos'], {'video_id': video_id})), - (context.localize(provider.LOCAL_MAP['youtube.video.comments']), + (context.localize('video.comments'), 'Container.Update(%s)' % context.create_uri(['special', 'parent_comments'], {'video_id': video_id})), - (context.localize(provider.LOCAL_MAP['youtube.video.description.links']), + (context.localize('video.description.links'), 'Container.Update(%s)' % context.create_uri(['special', 'description_links'], {'video_id': video_id}))]) if is_logged_in == '1': # rate a video refresh_container = context.get_param('refresh_container', '0') - items.append((context.localize(provider.LOCAL_MAP['youtube.video.rate']), + items.append((context.localize('video.rate'), 'RunPlugin(%s)' % context.create_uri(['video', 'rate'], {'video_id': video_id, 'refresh_container': refresh_container}))) - result = context.get_ui().on_select(context.localize(provider.LOCAL_MAP['youtube.video.more']), items) + result = context.get_ui().on_select(context.localize('video.more'), items) if result != -1: context.execute(result) @@ -126,5 +125,5 @@ def process(method, provider, context, re_match): if method == 'rate': return _process_rate_video(provider, context, re_match) if method == 'more': - return _process_more_for_video(provider, context) + return _process_more_for_video(context) raise kodion.KodionException("Unknown method '%s'" % method) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index a64685dd6..7cedf1085 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -55,157 +55,6 @@ class Provider(AbstractProvider): - LOCAL_MAP = {'youtube.search': 30102, - 'youtube.next_page': 30106, - 'youtube.watch_later': 30107, - 'youtube.video.rate.none': 30108, - 'youtube.remove': 30108, - 'youtube.sign.in': 30111, - 'youtube.sign.out': 30112, - 'youtube.rename': 30113, - 'youtube.delete': 30118, - 'youtube.api.key': 30201, - 'youtube.api.id': 30202, - 'youtube.api.secret': 30203, - 'youtube.channels': 30500, - 'youtube.playlists': 30501, - 'youtube.go_to_channel': 30502, - 'youtube.subscriptions': 30504, - 'youtube.unsubscribe': 30505, - 'youtube.subscribe': 30506, - 'youtube.my_channel': 30507, - 'youtube.video.liked': 30508, - 'youtube.history': 30509, - 'youtube.my_subscriptions': 30510, - 'youtube.video.queue': 30511, - 'youtube.browse_channels': 30512, - 'youtube.popular_right_now': 30513, - 'youtube.related_videos': 30514, - 'youtube.setting.auto_remove_watch_later': 30515, - 'youtube.subscribe_to': 30517, - 'youtube.sign.go_to': 30518, - 'youtube.sign.enter_code': 30519, - 'youtube.video.add_to_playlist': 30520, - 'youtube.playlist.select': 30521, - 'youtube.playlist.create': 30522, - 'youtube.setup_wizard.select_language': 30524, - 'youtube.setup_wizard.select_region': 30525, - 'youtube.setup_wizard.adjust': 30526, - 'youtube.setup_wizard.adjust.language_and_region': 30527, - 'youtube.video.rate': 30528, - 'youtube.video.rate.like': 30529, - 'youtube.video.rate.dislike': 30530, - 'youtube.playlist.play.all': 30531, - 'youtube.playlist.play.default': 30532, - 'youtube.playlist.play.reverse': 30533, - 'youtube.playlist.play.shuffle': 30534, - 'youtube.playlist.play.select': 30535, - 'youtube.playlist.progress.updating': 30536, - 'youtube.playlist.play.from_here': 30537, - 'youtube.video.disliked': 30538, - 'youtube.live': 30539, - 'youtube.upcoming': 30766, - 'youtube.video.play_with': 30540, - 'youtube.error.rtmpe_not_supported': 30542, - 'youtube.refresh': 30543, - 'youtube.video.description.links': 30544, - 'youtube.video.description.links.not_found': 30545, - 'youtube.sign.twice.title': 30546, - 'youtube.sign.twice.text': 30547, - 'youtube.video.more': 30548, - 'youtube.error.no_video_streams_found': 30549, - 'youtube.recommendations': 30551, - 'youtube.function.cache': 30557, - 'youtube.search.history': 30558, - 'youtube.subtitle.language': 30560, - 'youtube.none': 30561, - 'youtube.prompt': 30566, - 'youtube.set.as.watchlater': 30567, - 'youtube.remove.as.watchlater': 30568, - 'youtube.set.as.history': 30571, - 'youtube.remove.as.history': 30572, - 'youtube.succeeded': 30575, - 'youtube.failed': 30576, - 'youtube.settings': 30577, - 'youtube.mpd.enable.confirm': 30579, - 'youtube.reset.access.manager.confirm': 30581, - 'youtube.my_subscriptions_filtered': 30584, - 'youtube.add.my_subscriptions.filter': 30587, - 'youtube.remove.my_subscriptions.filter': 30588, - 'youtube.added.my_subscriptions.filter': 30589, - 'youtube.removed.my_subscriptions.filter': 30590, - 'youtube.updated_': 30597, - 'youtube.api.personal.enabled': 30598, - 'youtube.api.personal.failed': 30599, - 'youtube.subtitle._with_fallback': 30601, - 'youtube.subtitle.no.auto.generated': 30602, - 'youtube.quick.search': 30605, - 'youtube.quick.search.incognito': 30606, - 'youtube.clear_history': 30609, - 'youtube.clear_history_confirmation': 30610, - 'youtube.saved.playlists': 30611, - 'youtube.retry': 30612, - 'youtube.failed.watch_later.retry': 30614, - 'youtube.cancel': 30615, - 'youtube.must.be.signed.in': 30616, - 'youtube.select.listen.ip': 30644, - 'youtube.purchases': 30622, - 'youtube.requires.krypton': 30624, - 'youtube.inputstreamhelper.is.installed': 30625, - 'youtube.upcoming.live': 30646, - 'youtube.completed.live': 30647, - 'youtube.api.key.incorrect': 30648, - 'youtube.client.id.incorrect': 30649, - 'youtube.client.secret.incorrect': 30650, - 'youtube.perform.geolocation': 30653, - 'youtube.my_location': 30654, - 'youtube.switch.user': 30655, - 'youtube.user.new': 30656, - 'youtube.user.unnamed': 30657, - 'youtube.enter.user.name': 30658, - 'youtube.user.changed': 30659, - 'youtube.remove.a.user': 30662, - 'youtube.rename.a.user': 30663, - 'youtube.switch.user.now': 30665, - 'youtube.removed': 30666, - 'youtube.renamed': 30667, - 'youtube.playback.history': 30673, - 'youtube.mark.watched': 30670, - 'youtube.mark.unwatched': 30669, - 'youtube.reset.resume.point': 30674, - 'youtube.data.cache': 30687, - 'youtube.httpd.not.running': 30699, - 'youtube.client.ip': 30700, - 'youtube.client.ip.failed': 30701, - 'youtube.video.play_with_subtitles': 30702, - 'youtube.are.you.sure': 30703, - 'youtube.subtitles.download': 30705, - 'youtube.pre.download.subtitles': 30706, - 'youtube.untitled': 30707, - 'youtube.video.play_audio_only': 30708, - 'youtube.failed.watch_later.retry.2': 30709, - 'youtube.failed.watch_later.retry.3': 30710, - 'youtube.added.to.watch.later': 30713, - 'youtube.added.to.playlist': 30714, - 'youtube.removed.from.playlist': 30715, - 'youtube.liked.video': 30716, - 'youtube.disliked.video': 30717, - 'youtube.unrated.video': 30718, - 'youtube.subscribed.to.channel': 30719, - 'youtube.unsubscribed.from.channel': 30720, - 'youtube.uploads': 30726, - 'youtube.video.play_ask_for_quality': 30730, - 'youtube.key.requirement.notification': 30731, - 'youtube.video.comments': 30732, - 'youtube.video.comments.likes': 30733, - 'youtube.video.comments.replies': 30734, - 'youtube.video.comments.edited': 30735, - 'youtube.stats.viewCount': 30767, - 'youtube.stats.likeCount': 30733, - # 'youtube.stats.favoriteCount': 30100, - 'youtube.stats.commentCount': 30732, - } - def __init__(self): super(Provider, self).__init__() self._resource_manager = None @@ -286,12 +135,11 @@ def get_client(self, context): youtube_config = YouTube.CONFIGS.get('main') - dev_id = context.get_param('addon_id', None) + dev_id = context.get_param('addon_id') dev_configs = YouTube.CONFIGS.get('developer') dev_config = self.get_dev_config(context, dev_id, dev_configs) dev_keys = dev_config.get('main') if dev_config else None - client = None refresh_tokens = [] if dev_id: @@ -429,7 +277,7 @@ def get_fanart(context): # noinspection PyUnusedLocal @RegisterProviderPath('^/uri2addon/$') def on_uri2addon(self, context, re_match): - uri = context.get_param('uri', '') + uri = context.get_param('uri') if not uri: return False @@ -454,7 +302,7 @@ def _on_playlist(self, context, re_match): # no caching json_data = self.get_client(context).get_playlist_items(playlist_id=playlist_id, page_token=page_token) - if not v3.handle_error(self, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) @@ -478,7 +326,7 @@ def _on_channel_playlist(self, context, re_match): # no caching json_data = client.get_playlist_items(playlist_id=playlist_id, page_token=page_token) - if not v3.handle_error(self, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) @@ -501,8 +349,8 @@ def _on_channel_playlists(self, context, re_match): resource_manager = self.get_resource_manager(context) item_params = {} - incognito = context.get_param('incognito', False) - addon_id = context.get_param('addon_id', '') + incognito = context.get_param('incognito') + addon_id = context.get_param('addon_id') if incognito: item_params.update({'incognito': incognito}) if addon_id: @@ -511,7 +359,7 @@ def _on_channel_playlists(self, context, re_match): playlists = resource_manager.get_related_playlists(channel_id) uploads_playlist = playlists.get('uploads', '') if uploads_playlist: - uploads_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.uploads'])), + uploads_item = DirectoryItem(context.get_ui().bold(context.localize('uploads')), context.create_uri(['channel', channel_id, 'playlist', uploads_playlist], item_params), image=context.create_resource_path('media', 'playlist.png')) @@ -519,7 +367,7 @@ def _on_channel_playlists(self, context, re_match): # no caching json_data = self.get_client(context).get_playlists_of_channel(channel_id, page_token) - if not v3.handle_error(self, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) @@ -542,7 +390,7 @@ def _on_channel_live(self, context, re_match): # no caching json_data = self.get_client(context).search(q='', search_type='video', event_type='live', channel_id=channel_id, page_token=page_token, safe_search=safe_search) - if not v3.handle_error(self, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) @@ -556,14 +404,21 @@ def _on_channel_live(self, context, re_match): @RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$') def _on_channel(self, context, re_match): - listitem_channel_id = context.get_ui().get_info_label('Container.ListItem(0).Property(channel_id)') + localize = context.localize + create_path = context.create_resource_path + create_uri = context.create_uri + function_cache = context.get_function_cache() + params = context.get_params() + ui = context.get_ui() + + listitem_channel_id = ui.get_info_label('Container.ListItem(0).Property(channel_id)') method = re_match.group('method') channel_id = re_match.group('channel_id') if (method == 'channel' and channel_id and channel_id.lower() == 'property' and listitem_channel_id and listitem_channel_id.lower().startswith(('mine', 'uc'))): - context.execute('Container.Update(%s)' % context.create_uri(['channel', listitem_channel_id])) # redirect if keymap, without redirect results in 'invalid handle -1' + context.execute('Container.Update(%s)' % create_uri(['channel', listitem_channel_id])) # redirect if keymap, without redirect results in 'invalid handle -1' if method == 'channel' and not channel_id: return False @@ -582,9 +437,9 @@ def _on_channel(self, context, re_match): if method == 'user' or channel_id == 'mine': context.log_debug('Trying to get channel id for user "%s"' % channel_id) - json_data = context.get_function_cache().get(FunctionCache.ONE_DAY, - self.get_client(context).get_channel_by_username, channel_id) - if not v3.handle_error(self, context, json_data): + json_data = function_cache.get(FunctionCache.ONE_DAY, + self.get_client(context).get_channel_by_username, channel_id) + if not v3.handle_error(context, json_data): return False # we correct the channel id based on the username @@ -600,52 +455,52 @@ def _on_channel(self, context, re_match): return False channel_fanarts = resource_manager.get_fanarts([channel_id]) - page = context.get_param('page', 1) - page_token = context.get_param('page_token', '') - incognito = context.get_param('incognito', False) - addon_id = context.get_param('addon_id', '') + page = params.get('page', 1) + page_token = params.get('page_token', '') + incognito = params.get('incognito') + addon_id = params.get('addon_id') item_params = {} if incognito: item_params.update({'incognito': incognito}) if addon_id: item_params.update({'addon_id': addon_id}) - hide_folders = context.get_param('hide_folders', False) + hide_folders = params.get('hide_folders') if page == 1 and not hide_folders: - hide_playlists = context.get_param('hide_playlists', False) - hide_search = context.get_param('hide_search', False) - hide_live = context.get_param('hide_live', False) + hide_playlists = params.get('hide_playlists') + hide_search = params.get('hide_search') + hide_live = params.get('hide_live') if not hide_playlists: - playlists_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.playlists'])), - context.create_uri(['channel', channel_id, 'playlists'], item_params), - image=context.create_resource_path('media', 'playlist.png')) + playlists_item = DirectoryItem(ui.bold(localize('playlists')), + create_uri(['channel', channel_id, 'playlists'], item_params), + image=create_path('media', 'playlist.png')) playlists_item.set_fanart(channel_fanarts.get(channel_id, self.get_fanart(context))) result.append(playlists_item) search_live_id = mine_id if mine_id else channel_id if not hide_search: - search_item = NewSearchItem(context, alt_name=context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.search'])), - image=context.create_resource_path('media', 'search.png'), + search_item = NewSearchItem(context, alt_name=ui.bold(localize('search')), + image=create_path('media', 'search.png'), fanart=self.get_fanart(context), channel_id=search_live_id, incognito=incognito, addon_id=addon_id) search_item.set_fanart(self.get_fanart(context)) result.append(search_item) if not hide_live: - live_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.live'])), - context.create_uri(['channel', search_live_id, 'live'], item_params), - image=context.create_resource_path('media', 'live.png')) + live_item = DirectoryItem(ui.bold(localize('live')), + create_uri(['channel', search_live_id, 'live'], item_params), + image=create_path('media', 'live.png')) live_item.set_fanart(self.get_fanart(context)) result.append(live_item) playlists = resource_manager.get_related_playlists(channel_id) upload_playlist = playlists.get('uploads', '') if upload_playlist: - json_data = context.get_function_cache().get(FunctionCache.ONE_MINUTE * 5, - self.get_client(context).get_playlist_items, upload_playlist, - page_token=page_token) - if not v3.handle_error(self, context, json_data): + json_data = function_cache.get(FunctionCache.ONE_MINUTE * 5, + self.get_client(context).get_playlist_items, upload_playlist, + page_token=page_token) + if not v3.handle_error(context, json_data): return False result.extend( @@ -658,34 +513,47 @@ def _on_channel(self, context, re_match): def _on_my_location(self, context, re_match): self.set_content_type(context, constants.content_type.FILES) + create_path = context.create_resource_path + create_uri = context.create_uri + localize = context.localize settings = context.get_settings() result = [] # search - search_item = SearchItem(context, image=context.create_resource_path('media', 'search.png'), - fanart=self.get_fanart(context), location=True) + search_item = SearchItem( + context, + image=create_path('media', 'search.png'), + fanart=self.get_fanart(context), + location=True + ) result.append(search_item) # completed live events if settings.get_bool('youtube.folder.completed.live.show', True): - live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.completed.live']), - context.create_uri(['special', 'completed_live'], params={'location': True}), - image=context.create_resource_path('media', 'live.png')) + live_events_item = DirectoryItem( + localize('live.completed'), + create_uri(['special', 'completed_live'], params={'location': True}), + image=create_path('media', 'live.png') + ) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # upcoming live events if settings.get_bool('youtube.folder.upcoming.live.show', True): - live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.upcoming.live']), - context.create_uri(['special', 'upcoming_live'], params={'location': True}), - image=context.create_resource_path('media', 'live.png')) + live_events_item = DirectoryItem( + localize('live.upcoming'), + create_uri(['special', 'upcoming_live'], params={'location': True}), + image=create_path('media', 'live.png') + ) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # live events - live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.live']), - context.create_uri(['special', 'live'], params={'location': True}), - image=context.create_resource_path('media', 'live.png')) + live_events_item = DirectoryItem( + localize('live'), + create_uri(['special', 'live'], params={'location': True}), + image=create_path('media', 'live.png') + ) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) @@ -707,7 +575,8 @@ def _on_my_location(self, context, re_match): # noinspection PyUnusedLocal @RegisterProviderPath('^/play/$') def on_play(self, context, re_match): - listitem_path = context.get_ui().get_info_label('Container.ListItem(0).FileNameAndPath') + ui = context.get_ui() + listitem_path = ui.get_info_label('Container.ListItem(0).FileNameAndPath') redirect = False params = context.get_params() @@ -724,21 +593,21 @@ def on_play(self, context, re_match): else: return False - if context.get_ui().get_home_window_property('prompt_for_subtitles') != params.get('video_id'): - context.get_ui().clear_home_window_property('prompt_for_subtitles') + if ui.get_home_window_property('prompt_for_subtitles') != params.get('video_id'): + ui.clear_home_window_property('prompt_for_subtitles') - if context.get_ui().get_home_window_property('audio_only') != params.get('video_id'): - context.get_ui().clear_home_window_property('audio_only') + if ui.get_home_window_property('audio_only') != params.get('video_id'): + ui.clear_home_window_property('audio_only') - if context.get_ui().get_home_window_property('ask_for_quality') != params.get('video_id'): - context.get_ui().clear_home_window_property('ask_for_quality') + if ui.get_home_window_property('ask_for_quality') != params.get('video_id'): + ui.clear_home_window_property('ask_for_quality') if 'prompt_for_subtitles' in params: prompt_subtitles = params['prompt_for_subtitles'] del params['prompt_for_subtitles'] if prompt_subtitles and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems - context.get_ui().set_home_window_property('prompt_for_subtitles', params['video_id']) + ui.set_home_window_property('prompt_for_subtitles', params['video_id']) context.log_debug('Redirecting playback with subtitles') redirect = True @@ -747,7 +616,7 @@ def on_play(self, context, re_match): del params['audio_only'] if audio_only and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems - context.get_ui().set_home_window_property('audio_only', params['video_id']) + ui.set_home_window_property('audio_only', params['video_id']) context.log_debug('Redirecting audio only playback') redirect = True @@ -756,7 +625,7 @@ def on_play(self, context, re_match): del params['ask_for_quality'] if ask_for_quality and 'video_id' in params and 'playlist_id' not in params: # redirect to builtin after setting home window property, so playback url matches playable listitems - context.get_ui().set_home_window_property('ask_for_quality', params['video_id']) + ui.set_home_window_property('ask_for_quality', params['video_id']) context.log_debug('Redirecting audio only playback') redirect = True @@ -819,38 +688,40 @@ def _on_yt_specials(self, context, re_match): # noinspection PyUnusedLocal @RegisterProviderPath('^/history/clear/$') def _on_yt_clear_history(self, context, re_match): - if context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.clear_history_confirmation'])): + if context.get_ui().on_yes_no_input(context.get_name(), context.localize('clear_history_confirmation')): json_data = self.get_client(context).clear_watch_history() if 'error' not in json_data: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) + context.get_ui().show_notification(context.localize('succeeded')) @RegisterProviderPath('^/users/(?P[^/]+)/$') def _on_users(self, context, re_match): action = re_match.group('action') refresh = context.get_param('refresh') + localize = context.localize access_manager = context.get_access_manager() ui = context.get_ui() def add_user(_access_manager_users): - _results = ui.on_keyboard_input(context.localize(self.LOCAL_MAP['youtube.enter.user.name'])) + _results = ui.on_keyboard_input(localize('user.enter_name')) if _results[0] is False: return None _new_user_name = _results[1] if not _new_user_name.strip(): - _new_user_name = context.localize(self.LOCAL_MAP['youtube.user.unnamed']) - _new_users = {} - for idx, key in enumerate(list(_access_manager_users.keys())): - _new_users[str(idx)] = _access_manager_users[key] + _new_user_name = localize('user.unnamed') + _new_users = { + str(idx): user + for idx, user in enumerate(_access_manager_users.values()) + } _new_users[str(len(_new_users))] = access_manager.get_new_user(_new_user_name) access_manager.set_users(_new_users) return str(len(_new_users) - 1) def switch_to_user(_user): - _user_name = access_manager.get_users()[_user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) + _user_name = access_manager.get_users()[_user].get('name', localize('user.unnamed')) access_manager.set_user(_user, switch_to=True) - ui.show_notification(context.localize(self.LOCAL_MAP['youtube.user.changed']) % _user_name, - context.localize(self.LOCAL_MAP['youtube.switch.user'])) + ui.show_notification(localize('user.changed') % _user_name, + localize('user.switch')) self.get_resource_manager(context).clear() if refresh: ui.refresh_container() @@ -858,23 +729,23 @@ def switch_to_user(_user): if action == 'switch': access_manager_users = access_manager.get_users() current_user = access_manager.get_user() - users = [ui.bold(context.localize(self.LOCAL_MAP['youtube.user.new']))] + users = [ui.bold(localize('user.new'))] user_index_map = [] - for k in list(access_manager_users.keys()): - if k == current_user: - if access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): + for user, details in access_manager_users.items(): + if user == current_user: + if details.get('access_token') or details.get('refresh_token'): users.append( ui.color('limegreen', - ' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) + ' '.join([details.get('name', localize('user.unnamed')), '*'])) ) else: - users.append(' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) - elif access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): - users.append(ui.color('limegreen', access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])))) + users.append(' '.join([details.get('name', localize('user.unnamed')), '*'])) + elif details.get('access_token') or details.get('refresh_token'): + users.append(ui.color('limegreen', details.get('name', localize('user.unnamed')))) else: - users.append(access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed']))) - user_index_map.append(k) - result = ui.on_select(context.localize(self.LOCAL_MAP['youtube.switch.user']), users) + users.append(details.get('name', localize('user.unnamed'))) + user_index_map.append(user) + result = ui.on_select(localize('user.switch'), users) if result == -1: return True if result == 0: @@ -888,8 +759,8 @@ def switch_to_user(_user): elif action == 'add': user = add_user(access_manager.get_users()) if user: - user_name = access_manager.get_users()[user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) - result = ui.on_yes_no_input(context.localize(self.LOCAL_MAP['youtube.switch.user']), context.localize(self.LOCAL_MAP['youtube.switch.user.now']) % user_name) + user_name = access_manager.get_users()[user].get('name', localize('user.unnamed')) + result = ui.on_yes_no_input(localize('user.switch'), localize('user.switch.now') % user_name) if result: switch_to_user(user) @@ -898,81 +769,84 @@ def switch_to_user(_user): users = [] user_index_map = [] current_user = access_manager.get_user() - current_user_dict = access_manager_users[current_user] - for k in list(access_manager_users.keys()): - if k == current_user: - if access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): + current_user_idx = '0' + for user, details in access_manager_users.items(): + if user == current_user: + current_user_idx = str(len(user_index_map)) + if details.get('access_token') or details.get('refresh_token'): users.append( ui.color('limegreen', - ' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) + ' '.join([details.get('name', localize('user.unnamed')), '*'])) ) else: - users.append(' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) - elif access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): - users.append(ui.color('limegreen', access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])))) + users.append(' '.join([details.get('name', localize('user.unnamed')), '*'])) + elif details.get('access_token') or details.get('refresh_token'): + users.append(ui.color('limegreen', details.get('name', localize('user.unnamed')))) else: - users.append(access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed']))) - user_index_map.append(k) - result = ui.on_select(context.localize(self.LOCAL_MAP['youtube.remove.a.user']), users) + users.append(details.get('name', localize('user.unnamed'))) + user_index_map.append(user) + result = ui.on_select(localize('user.remove'), users) if result == -1: return True - else: - user = user_index_map[result] - user_name = access_manager_users[user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) - result = ui.on_remove_content(user_name) - if result: - if user == current_user: - access_manager.set_user('0', switch_to=True) - del access_manager_users[user] - new_users = {} - for i, u in enumerate(list(access_manager_users.keys())): - if access_manager_users[u] == current_user_dict: - access_manager.set_user(str(i), switch_to=True) - new_users[str(i)] = access_manager_users[u] - - if not new_users.get(access_manager.get_user()): - access_manager.set_user('0', switch_to=True) - - access_manager.set_users(new_users) - ui.show_notification(context.localize(self.LOCAL_MAP['youtube.removed']) % user_name, - context.localize(self.LOCAL_MAP['youtube.remove'])) + + user = user_index_map[result] + user_name = access_manager_users[user].get('name', localize('user.unnamed')) + result = ui.on_remove_content(user_name) + if result: + if user == current_user: + access_manager.set_user('0', switch_to=True) + del access_manager_users[user] + + new_users = { + str(idx): user + for idx, user in enumerate(access_manager_users.values()) + } + + if current_user_idx in new_users: + access_manager.set_user(current_user_idx, switch_to=True) + else: + access_manager.set_user('0', switch_to=True) + + access_manager.set_users(new_users) + ui.show_notification(localize('removed') % user_name, + localize('remove')) elif action == 'rename': access_manager_users = access_manager.get_users() users = [] user_index_map = [] current_user = access_manager.get_user() - for k in list(access_manager_users.keys()): - if k == current_user: - if access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): + for user, details in access_manager_users.items(): + if user == current_user: + if details.get('access_token') or details.get('refresh_token'): users.append( ui.color('limegreen', - ' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) + ' '.join([details.get('name', localize('user.unnamed')), '*'])) ) else: - users.append(' '.join([access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])), '*'])) - elif access_manager_users[k].get('access_token') or access_manager_users[k].get('refresh_token'): - users.append(ui.color('limegreen', access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])))) + users.append(' '.join([details.get('name', localize('user.unnamed')), '*'])) + elif details.get('access_token') or details.get('refresh_token'): + users.append(ui.color('limegreen', details.get('name', localize('user.unnamed')))) else: - users.append(access_manager_users[k].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed']))) - user_index_map.append(k) - result = ui.on_select(context.localize(self.LOCAL_MAP['youtube.rename.a.user']), users) + users.append(details.get('name', localize('user.unnamed'))) + user_index_map.append(user) + result = ui.on_select(localize('user.rename'), users) if result == -1: return True - else: - user = user_index_map[result] - old_user_name = access_manager_users[user].get('name', context.localize(self.LOCAL_MAP['youtube.user.unnamed'])) - results = ui.on_keyboard_input(context.localize(self.LOCAL_MAP['youtube.enter.user.name']), default=old_user_name) - if results[0] is False: - return True - new_user_name = results[1] - if not new_user_name.strip() or (old_user_name == new_user_name): - return True - - access_manager_users[user]['name'] = new_user_name - access_manager.set_users(access_manager_users) - ui.show_notification(context.localize(self.LOCAL_MAP['youtube.renamed']) % (old_user_name, new_user_name), - context.localize(self.LOCAL_MAP['youtube.rename'])) + + user = user_index_map[result] + old_user_name = access_manager_users[user].get('name', localize('user.unnamed')) + results = ui.on_keyboard_input(localize('user.enter_name'), default=old_user_name) + if results[0] is False: + return True + new_user_name = results[1] + if not new_user_name.strip() or (old_user_name == new_user_name): + return True + + access_manager_users[user]['name'] = new_user_name + access_manager.set_users(access_manager_users) + ui.show_notification(localize('renamed') % (old_user_name, new_user_name), + localize('rename')) return True @@ -985,8 +859,8 @@ def _on_sign(self, context, re_match): if (not sign_out_confirmed and mode == 'out' and context.get_ui().on_yes_no_input( - context.localize(self.LOCAL_MAP['youtube.sign.out']), - context.localize(self.LOCAL_MAP['youtube.are.you.sure']))): + context.localize('sign.out'), + context.localize('are_you_sure'))): sign_out_confirmed = True if mode == 'in' or (mode == 'out' and sign_out_confirmed): @@ -995,7 +869,7 @@ def _on_sign(self, context, re_match): @RegisterProviderPath('^/search/$') def endpoint_search(self, context, re_match): - query = context.get_param('q', '') + query = context.get_param('q') if not query: return [] @@ -1011,7 +885,7 @@ def _search_channel_or_playlist(self, context, id_string): elif re.match(r'[OP]L[0-9a-zA-Z_\-]{30,40}', id_string): json_data = self.get_client(context).get_playlists(id_string) - if not json_data or not v3.handle_error(self, context, json_data): + if not json_data or not v3.handle_error(context, json_data): return [] result.extend(v3.response_to_items(self, context, json_data)) @@ -1022,17 +896,18 @@ def on_search(self, search_text, context, re_match): if result: # found a channel or playlist matching search_text return result - channel_id = context.get_param('channel_id', '') - event_type = context.get_param('event_type', '') - hide_folders = context.get_param('hide_folders', False) - location = context.get_param('location', False) - page = context.get_param('page', 1) - page_token = context.get_param('page_token', '') - search_type = context.get_param('search_type', 'video') + context.set_param('q', search_text) - safe_search = context.get_settings().safe_search() + params = context.get_params() + channel_id = params.get('channel_id') + event_type = params.get('event_type') + hide_folders = params.get('hide_folders') + location = params.get('location') + page = params.get('page', 1) + page_token = params.get('page_token', '') + search_type = params.get('search_type', 'video') - context.set_param('q', search_text) + safe_search = context.get_settings().safe_search() if search_type == 'video': self.set_content_type(context, constants.content_type.VIDEOS) @@ -1042,18 +917,19 @@ def on_search(self, search_text, context, re_match): if page == 1 and search_type == 'video' and not event_type and not hide_folders: if not channel_id and not location: channel_params = {} - channel_params.update(context.get_params()) + channel_params.update(params) channel_params['search_type'] = 'channel' - channel_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.channels'])), + channel_item = DirectoryItem(context.get_ui().bold(context.localize('channels')), context.create_uri([context.get_path()], channel_params), image=context.create_resource_path('media', 'channels.png')) channel_item.set_fanart(self.get_fanart(context)) result.append(channel_item) + if not location: playlist_params = {} - playlist_params.update(context.get_params()) + playlist_params.update(params) playlist_params['search_type'] = 'playlist' - playlist_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.playlists'])), + playlist_item = DirectoryItem(context.get_ui().bold(context.localize('playlists')), context.create_uri([context.get_path()], playlist_params), image=context.create_resource_path('media', 'playlist.png')) playlist_item.set_fanart(self.get_fanart(context)) @@ -1062,10 +938,10 @@ def on_search(self, search_text, context, re_match): if not channel_id: # live live_params = {} - live_params.update(context.get_params()) + live_params.update(params) live_params['search_type'] = 'video' live_params['event_type'] = 'live' - live_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.live'])), + live_item = DirectoryItem(context.get_ui().bold(context.localize('live')), context.create_uri([context.get_path().replace('input', 'query')], live_params), image=context.create_resource_path('media', 'live.png')) live_item.set_fanart(self.get_fanart(context)) @@ -1074,7 +950,7 @@ def on_search(self, search_text, context, re_match): json_data = context.get_function_cache().get(FunctionCache.ONE_MINUTE * 10, self.get_client(context).search, q=search_text, search_type=search_type, event_type=event_type, safe_search=safe_search, page_token=page_token, channel_id=channel_id, location=location) - if not v3.handle_error(self, context, json_data): + if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) return result @@ -1082,66 +958,79 @@ def on_search(self, search_text, context, re_match): @RegisterProviderPath('^/config/(?P[^/]+)/$') def configure_addon(self, context, re_match): switch = re_match.group('switch') + localize = context.localize settings = context.get_settings() + ui = context.get_ui() + if switch == 'youtube': context.addon().openSettings() - context.get_ui().refresh_container() + ui.refresh_container() elif switch == 'isa': if context.use_inputstream_adaptive(): xbmcaddon.Addon(id='inputstream.adaptive').openSettings() else: settings.set_bool('kodion.video.quality.isa', False) elif switch == 'subtitles': - yt_language = context.get_settings().get_string('youtube.language', 'en-US') - sub_setting = context.get_settings().subtitle_languages() + yt_language = settings.get_string('youtube.language', 'en-US') + sub_setting = settings.subtitle_languages() if yt_language.startswith('en'): - sub_opts = [context.localize(self.LOCAL_MAP['youtube.none']), context.localize(self.LOCAL_MAP['youtube.prompt']), - context.localize(self.LOCAL_MAP['youtube.subtitle._with_fallback']) % ('en', 'en-US/en-GB'), yt_language, - '%s (%s)' % (yt_language, context.localize(self.LOCAL_MAP['youtube.subtitle.no.auto.generated']))] + sub_opts = [localize('none'), + localize('prompt'), + localize('subtitles.with_fallback') % ('en', 'en-US/en-GB'), + yt_language, + '%s (%s)' % (yt_language, localize('subtitles.no_auto_generated'))] else: - sub_opts = [context.localize(self.LOCAL_MAP['youtube.none']), context.localize(self.LOCAL_MAP['youtube.prompt']), - context.localize(self.LOCAL_MAP['youtube.subtitle._with_fallback']) % (yt_language, 'en'), yt_language, - '%s (%s)' % (yt_language, context.localize(self.LOCAL_MAP['youtube.subtitle.no.auto.generated']))] + sub_opts = [localize('none'), + localize('prompt'), + localize('subtitles.with_fallback') % (yt_language, 'en'), + yt_language, + '%s (%s)' % (yt_language, localize('subtitles.no_auto_generated'))] - sub_opts[sub_setting] = context.get_ui().bold(sub_opts[sub_setting]) + sub_opts[sub_setting] = ui.bold(sub_opts[sub_setting]) - result = context.get_ui().on_select(context.localize(self.LOCAL_MAP['youtube.subtitle.language']), sub_opts) + result = ui.on_select(localize('subtitles.language'), sub_opts) if result > -1: - context.get_settings().set_subtitle_languages(result) + settings.set_subtitle_languages(result) - result = context.get_ui().on_yes_no_input( - context.localize(self.LOCAL_MAP['youtube.subtitles.download']), - context.localize(self.LOCAL_MAP['youtube.pre.download.subtitles']) + result = ui.on_yes_no_input( + localize('subtitles.download'), + localize('subtitles.download.pre') ) if result > -1: - context.get_settings().set_subtitle_download(result == 1) + settings.set_subtitle_download(result == 1) elif switch == 'listen_ip': local_ranges = ('10.', '172.16.', '192.168.') - addresses = [iface[4][0] for iface in socket.getaddrinfo(socket.gethostname(), None) if iface[4][0].startswith(local_ranges)] + ['127.0.0.1', '0.0.0.0'] - selected_address = context.get_ui().on_select(context.localize(self.LOCAL_MAP['youtube.select.listen.ip']), addresses) + addresses = [iface[4][0] + for iface in socket.getaddrinfo(socket.gethostname(), None) + if iface[4][0].startswith(local_ranges)] + addresses += ['127.0.0.1', '0.0.0.0'] + selected_address = ui.on_select(localize('select.listen.ip'), addresses) if selected_address != -1: - context.get_settings().set_httpd_listen(addresses[selected_address]) + settings.set_httpd_listen(addresses[selected_address]) return False # noinspection PyUnusedLocal @RegisterProviderPath('^/my_subscriptions/filter/$') def manage_my_subscription_filter(self, context, re_match): params = context.get_params() + settings = context.get_settings() + ui = context.get_ui() + action = params.get('action') channel = params.get('channel_name') if (not channel) or (not action): return - filter_enabled = context.get_settings().get_bool('youtube.folder.my_subscriptions_filtered.show', False) + filter_enabled = settings.get_bool('youtube.folder.my_subscriptions_filtered.show', False) if not filter_enabled: return channel_name = channel.lower() channel_name = channel_name.replace(',', '') - filter_string = context.get_settings().get_string('youtube.filter.my_subscriptions_filtered.list', '') + filter_string = settings.get_string('youtube.filter.my_subscriptions_filtered.list', '') filter_string = filter_string.replace(', ', ',') filter_list = filter_string.split(',') filter_list = [x.lower() for x in filter_list] @@ -1154,39 +1043,42 @@ def manage_my_subscription_filter(self, context, re_match): modified_string = ','.join(filter_list).lstrip(',') if filter_string != modified_string: - context.get_settings().set_string('youtube.filter.my_subscriptions_filtered.list', modified_string) + settings.set_string('youtube.filter.my_subscriptions_filtered.list', modified_string) message = '' if action == 'add': - message = context.localize(self.LOCAL_MAP['youtube.added.my_subscriptions.filter']) + message = context.localize('my_subscriptions.filter.added') elif action == 'remove': - message = context.localize(self.LOCAL_MAP['youtube.removed.my_subscriptions.filter']) + message = context.localize('my_subscriptions.filter.removed') if message: - context.get_ui().show_notification(message=message) - context.get_ui().refresh_container() + ui.show_notification(message=message) + ui.refresh_container() @RegisterProviderPath('^/maintain/(?P[^/]+)/(?P[^/]+)/$') def maintenance_actions(self, context, re_match): maint_type = re_match.group('maint_type') action = re_match.group('action') + + ui = context.get_ui() + localize = context.localize + if action == 'clear': if maint_type == 'function_cache': - if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.function.cache'])): + if ui.on_remove_content(localize('cache.function')): context.get_function_cache().clear() - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) + ui.show_notification(localize('succeeded')) elif maint_type == 'data_cache': - if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.data.cache'])): + if ui.on_remove_content(localize('cache.data')): context.get_data_cache().clear() - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) + ui.show_notification(localize('succeeded')) elif maint_type == 'search_cache': - if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.search.history'])): + if ui.on_remove_content(localize('search.history')): context.get_search_history().clear() - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) - elif maint_type == 'playback_history': - if context.get_ui().on_remove_content(context.localize(self.LOCAL_MAP['youtube.playback.history'])): - context.get_playback_history().clear() - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) + ui.show_notification(localize('succeeded')) + elif maint_type == 'playback_history' and ui.on_remove_content(localize('playback.history')): + context.get_playback_history().clear() + ui.show_notification(localize('succeeded')) elif action == 'reset': - if maint_type == 'access_manager' and context.get_ui().on_yes_no_input(context.get_name(), context.localize(self.LOCAL_MAP['youtube.reset.access.manager.confirm'])): + if maint_type == 'access_manager' and ui.on_yes_no_input(context.get_name(), localize('reset.access_manager.confirm')): try: context.get_function_cache().clear() access_manager = context.get_access_manager() @@ -1200,10 +1092,10 @@ def maintenance_actions(self, context, re_match): pass self.reset_client() access_manager.update_access_token(access_token='', refresh_token='') - context.get_ui().refresh_container() - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) + ui.refresh_container() + ui.show_notification(localize('succeeded')) except: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.failed'])) + ui.show_notification(localize('failed')) elif action == 'delete': _maint_files = {'function_cache': 'cache.sqlite', 'search_cache': 'search.sqlite', @@ -1225,7 +1117,7 @@ def maintenance_actions(self, context, re_match): _file_w_path = os.path.join(os.path.join(context.get_data_path(), 'playback'), _file) else: _file_w_path = os.path.join(context.get_data_path(), _file) - if context.get_ui().on_delete_content(_file): + if ui.on_delete_content(_file): if maint_type == 'temp_files': _trans_path = xbmc.translatePath(_file_w_path) try: @@ -1241,45 +1133,49 @@ def maintenance_actions(self, context, re_match): elif _file_w_path: success = xbmcvfs.delete(_file_w_path) if success: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.succeeded'])) + ui.show_notification(localize('succeeded')) else: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.failed'])) + ui.show_notification(localize('failed')) elif action == 'install' and maint_type == 'inputstreamhelper': if context.get_system_version().get_version()[0] >= 17: try: xbmcaddon.Addon('script.module.inputstreamhelper') - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.inputstreamhelper.is.installed'])) + ui.show_notification(localize('inputstreamhelper.is_installed')) except RuntimeError: context.execute('InstallAddon(script.module.inputstreamhelper)') else: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.requires.krypton'])) + ui.show_notification(localize('requires.krypton')) # noinspection PyUnusedLocal @RegisterProviderPath('^/api/update/$') def api_key_update(self, context, re_match): - settings = context.get_settings() + localize = context.localize params = context.get_params() + settings = context.get_settings() + ui = context.get_ui() + + api_key = params.get('api_key') client_id = params.get('client_id') client_secret = params.get('client_secret') - api_key = params.get('api_key') enable = params.get('enable') + updated_list = [] log_list = [] if api_key: settings.set_string('youtube.api.key', api_key) - updated_list.append(context.localize(self.LOCAL_MAP['youtube.api.key'])) + updated_list.append(localize('api.key')) log_list.append('Key') if client_id: settings.set_string('youtube.api.id', client_id) - updated_list.append(context.localize(self.LOCAL_MAP['youtube.api.id'])) + updated_list.append(localize('api.id')) log_list.append('Id') if client_secret: settings.set_string('youtube.api.secret', client_secret) - updated_list.append(context.localize(self.LOCAL_MAP['youtube.api.secret'])) + updated_list.append(localize('api.secret')) log_list.append('Secret') if updated_list: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.updated_']) % ', '.join(updated_list)) + ui.show_notification(localize('updated_') % ', '.join(updated_list)) context.log_debug('Updated API keys: %s' % ', '.join(log_list)) client_id = settings.get_string('youtube.api.id', '') @@ -1289,19 +1185,19 @@ def api_key_update(self, context, re_match): log_list = [] if enable and client_id and client_secret and api_key: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.api.personal.enabled'])) + ui.show_notification(localize('api.personal.enabled')) context.log_debug('Personal API keys enabled') elif enable: if not api_key: - missing_list.append(context.localize(self.LOCAL_MAP['youtube.api.key'])) + missing_list.append(localize('api.key')) log_list.append('Key') if not client_id: - missing_list.append(context.localize(self.LOCAL_MAP['youtube.api.id'])) + missing_list.append(localize('api.id')) log_list.append('Id') if not client_secret: - missing_list.append(context.localize(self.LOCAL_MAP['youtube.api.secret'])) + missing_list.append(localize('api.secret')) log_list.append('Secret') - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.api.personal.failed']) % ', '.join(missing_list)) + ui.show_notification(localize('api.personal.failed') % ', '.join(missing_list)) context.log_debug('Failed to enable personal API keys. Missing: %s' % ', '.join(log_list)) # noinspection PyUnusedLocal @@ -1312,11 +1208,11 @@ def show_client_ip(self, context, re_match): if is_httpd_live(port=port): client_ip = get_client_ip_address(port=port) if client_ip: - context.get_ui().on_ok(context.get_name(), context.localize(self.LOCAL_MAP['youtube.client.ip']) % client_ip) + context.get_ui().on_ok(context.get_name(), context.localize('client.ip') % client_ip) else: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.client.ip.failed'])) + context.get_ui().show_notification(context.localize('client.ip.failed')) else: - context.get_ui().show_notification(context.localize(self.LOCAL_MAP['youtube.httpd.not.running'])) + context.get_ui().show_notification(context.localize('httpd.not.running')) # noinspection PyUnusedLocal @RegisterProviderPath('^/playback_history/$') @@ -1362,11 +1258,16 @@ def on_root(self, context, re_match): """ Support old YouTube url calls, but also log a deprecation warnings. """ - old_action = context.get_param('action', '') + old_action = context.get_param('action') if old_action: return yt_old_actions.process_old_action(self, context, re_match) + create_path = context.create_resource_path + create_uri = context.create_uri + localize = context.localize settings = context.get_settings() + ui = context.get_ui() + _ = self.get_client(context) # required for self.is_logged_in() self.set_content_type(context, constants.content_type.FILES) @@ -1375,9 +1276,9 @@ def on_root(self, context, re_match): # sign in if not self.is_logged_in() and settings.get_bool('youtube.folder.sign.in.show', True): - sign_in_item = DirectoryItem(context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.sign.in'])), - context.create_uri(['sign', 'in']), - image=context.create_resource_path('media', 'sign_in.png')) + sign_in_item = DirectoryItem(ui.bold(localize('sign.in')), + create_uri(['sign', 'in']), + image=create_path('media', 'sign_in.png')) sign_in_item.set_action(True) sign_in_item.set_fanart(self.get_fanart(context)) result.append(sign_in_item) @@ -1385,23 +1286,23 @@ def on_root(self, context, re_match): if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions.show', True): # my subscription - #clear cache + # clear cache cache = context.get_data_cache() cache.set_item('my-subscriptions-items', '[]') my_subscriptions_item = DirectoryItem( - context.get_ui().bold(context.localize(self.LOCAL_MAP['youtube.my_subscriptions'])), - context.create_uri(['special', 'new_uploaded_videos_tv']), - context.create_resource_path('media', 'new_uploads.png')) + ui.bold(localize('my_subscriptions')), + create_uri(['special', 'new_uploaded_videos_tv']), + create_path('media', 'new_uploads.png')) my_subscriptions_item.set_fanart(self.get_fanart(context)) result.append(my_subscriptions_item) if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions_filtered.show', True): # my subscriptions filtered my_subscriptions_filtered_item = DirectoryItem( - context.localize(self.LOCAL_MAP['youtube.my_subscriptions_filtered']), - context.create_uri(['special', 'new_uploaded_videos_tv_filtered']), - context.create_resource_path('media', 'new_uploads.png')) + localize('my_subscriptions.filtered'), + create_uri(['special', 'new_uploaded_videos_tv_filtered']), + create_path('media', 'new_uploads.png')) my_subscriptions_filtered_item.set_fanart(self.get_fanart(context)) result.append(my_subscriptions_filtered_item) @@ -1412,44 +1313,46 @@ def on_root(self, context, re_match): watch_history_playlist_id = access_manager.get_watch_history_id() if watch_history_playlist_id != 'HL': recommendations_item = DirectoryItem( - context.localize(self.LOCAL_MAP['youtube.recommendations']), - context.create_uri(['special', 'recommendations']), - context.create_resource_path('media', 'popular.png')) + localize('recommendations'), + create_uri(['special', 'recommendations']), + create_path('media', 'popular.png')) recommendations_item.set_fanart(self.get_fanart(context)) result.append(recommendations_item) # what to watch if settings.get_bool('youtube.folder.popular_right_now.show', True): what_to_watch_item = DirectoryItem( - context.localize(self.LOCAL_MAP['youtube.popular_right_now']), - context.create_uri(['special', 'popular_right_now']), - context.create_resource_path('media', 'popular.png')) + localize('popular_right_now'), + create_uri(['special', 'popular_right_now']), + create_path('media', 'popular.png')) what_to_watch_item.set_fanart(self.get_fanart(context)) result.append(what_to_watch_item) # search if settings.get_bool('youtube.folder.search.show', True): - search_item = SearchItem(context, image=context.create_resource_path('media', 'search.png'), + search_item = SearchItem(context, image=create_path('media', 'search.png'), fanart=self.get_fanart(context)) result.append(search_item) if settings.get_bool('youtube.folder.quick_search.show', True): - quick_search_item = NewSearchItem(context, alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search']), + quick_search_item = NewSearchItem(context, + alt_name=localize('search.quick'), fanart=self.get_fanart(context)) result.append(quick_search_item) if settings.get_bool('youtube.folder.quick_search_incognito.show', True): - quick_search_incognito_item = NewSearchItem(context, alt_name=context.localize(self.LOCAL_MAP['youtube.quick.search.incognito']), - image=context.create_resource_path('media', 'search.png'), + quick_search_incognito_item = NewSearchItem(context, + alt_name=localize('search.quick.incognito'), + image=create_path('media', 'search.png'), fanart=self.get_fanart(context), incognito=True) result.append(quick_search_incognito_item) # my location if settings.get_bool('youtube.folder.my_location.show', True) and settings.get_location(): - my_location_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.my_location']), - context.create_uri(['location', 'mine']), - image=context.create_resource_path('media', 'channel.png')) + my_location_item = DirectoryItem(localize('my_location'), + create_uri(['location', 'mine']), + image=create_path('media', 'channel.png')) my_location_item.set_fanart(self.get_fanart(context)) result.append(my_location_item) @@ -1457,22 +1360,21 @@ def on_root(self, context, re_match): if self.is_logged_in(): # my channel if settings.get_bool('youtube.folder.my_channel.show', True): - my_channel_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.my_channel']), - context.create_uri(['channel', 'mine']), - image=context.create_resource_path('media', 'channel.png')) + my_channel_item = DirectoryItem(localize('my_channel'), + create_uri(['channel', 'mine']), + image=create_path('media', 'channel.png')) my_channel_item.set_fanart(self.get_fanart(context)) result.append(my_channel_item) # watch later watch_later_playlist_id = access_manager.get_watch_later_id() if settings.get_bool('youtube.folder.watch_later.show', True) and watch_later_playlist_id: - watch_later_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.watch_later']), - context.create_uri( - ['channel', 'mine', 'playlist', watch_later_playlist_id]), - context.create_resource_path('media', 'watch_later.png')) + watch_later_item = DirectoryItem(localize('watch_later'), + create_uri(['channel', 'mine', 'playlist', watch_later_playlist_id]), + create_path('media', 'watch_later.png')) watch_later_item.set_fanart(self.get_fanart(context)) context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, self, context, watch_later_playlist_id) + yt_context_menu.append_play_all_from_playlist(context_menu, context, watch_later_playlist_id) watch_later_item.set_context_menu(context_menu) result.append(watch_later_item) @@ -1481,21 +1383,20 @@ def on_root(self, context, re_match): resource_manager = self.get_resource_manager(context) playlists = resource_manager.get_related_playlists(channel_id='mine') if 'likes' in playlists: - liked_videos_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.video.liked']), - context.create_uri( - ['channel', 'mine', 'playlist', playlists['likes']]), - context.create_resource_path('media', 'likes.png')) + liked_videos_item = DirectoryItem(localize('video.liked'), + create_uri(['channel', 'mine', 'playlist', playlists['likes']]), + create_path('media', 'likes.png')) liked_videos_item.set_fanart(self.get_fanart(context)) context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, self, context, playlists['likes']) + yt_context_menu.append_play_all_from_playlist(context_menu, context, playlists['likes']) liked_videos_item.set_context_menu(context_menu) result.append(liked_videos_item) # disliked videos if settings.get_bool('youtube.folder.disliked_videos.show', True): - disliked_videos_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.video.disliked']), - context.create_uri(['special', 'disliked_videos']), - context.create_resource_path('media', 'dislikes.png')) + disliked_videos_item = DirectoryItem(localize('video.disliked'), + create_uri(['special', 'disliked_videos']), + create_path('media', 'dislikes.png')) disliked_videos_item.set_fanart(self.get_fanart(context)) result.append(disliked_videos_item) @@ -1503,95 +1404,94 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.history.show', False): watch_history_playlist_id = access_manager.get_watch_history_id() if watch_history_playlist_id != 'HL': - watch_history_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.history']), - context.create_uri( - ['channel', 'mine', 'playlist', watch_history_playlist_id]), - context.create_resource_path('media', 'history.png')) + watch_history_item = DirectoryItem(localize('history'), + create_uri(['channel', 'mine', 'playlist', watch_history_playlist_id]), + create_path('media', 'history.png')) watch_history_item.set_fanart(self.get_fanart(context)) context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, self, context, watch_history_playlist_id) + yt_context_menu.append_play_all_from_playlist(context_menu, context, watch_history_playlist_id) watch_history_item.set_context_menu(context_menu) result.append(watch_history_item) # (my) playlists if settings.get_bool('youtube.folder.playlists.show', True): - playlists_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.playlists']), - context.create_uri(['channel', 'mine', 'playlists']), - context.create_resource_path('media', 'playlist.png')) + playlists_item = DirectoryItem(localize('playlists'), + create_uri(['channel', 'mine', 'playlists']), + create_path('media', 'playlist.png')) playlists_item.set_fanart(self.get_fanart(context)) result.append(playlists_item) # saved playlists if settings.get_bool('youtube.folder.saved.playlists.show', True): - playlists_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.saved.playlists']), - context.create_uri(['special', 'saved_playlists']), - context.create_resource_path('media', 'playlist.png')) + playlists_item = DirectoryItem(localize('saved.playlists'), + create_uri(['special', 'saved_playlists']), + create_path('media', 'playlist.png')) playlists_item.set_fanart(self.get_fanart(context)) result.append(playlists_item) # subscriptions if settings.get_bool('youtube.folder.subscriptions.show', True): - subscriptions_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.subscriptions']), - context.create_uri(['subscriptions', 'list']), - image=context.create_resource_path('media', 'channels.png')) + subscriptions_item = DirectoryItem(localize('subscriptions'), + create_uri(['subscriptions', 'list']), + image=create_path('media', 'channels.png')) subscriptions_item.set_fanart(self.get_fanart(context)) result.append(subscriptions_item) # browse channels if settings.get_bool('youtube.folder.browse_channels.show', True): - browse_channels_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.browse_channels']), - context.create_uri(['special', 'browse_channels']), - image=context.create_resource_path('media', 'browse_channels.png')) + browse_channels_item = DirectoryItem(localize('browse_channels'), + create_uri(['special', 'browse_channels']), + image=create_path('media', 'browse_channels.png')) browse_channels_item.set_fanart(self.get_fanart(context)) result.append(browse_channels_item) # completed live events if settings.get_bool('youtube.folder.completed.live.show', True): - live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.completed.live']), - context.create_uri(['special', 'completed_live']), - image=context.create_resource_path('media', 'live.png')) + live_events_item = DirectoryItem(localize('live.completed'), + create_uri(['special', 'completed_live']), + image=create_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # upcoming live events if settings.get_bool('youtube.folder.upcoming.live.show', True): - live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.upcoming.live']), - context.create_uri(['special', 'upcoming_live']), - image=context.create_resource_path('media', 'live.png')) + live_events_item = DirectoryItem(localize('live.upcoming'), + create_uri(['special', 'upcoming_live']), + image=create_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # live events if settings.get_bool('youtube.folder.live.show', True): - live_events_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.live']), - context.create_uri(['special', 'live']), - image=context.create_resource_path('media', 'live.png')) + live_events_item = DirectoryItem(localize('live'), + create_uri(['special', 'live']), + image=create_path('media', 'live.png')) live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # switch user if settings.get_bool('youtube.folder.switch.user.show', True): - switch_user_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.switch.user']), - context.create_uri(['users', 'switch']), - image=context.create_resource_path('media', 'channel.png')) + switch_user_item = DirectoryItem(localize('user.switch'), + create_uri(['users', 'switch']), + image=create_path('media', 'channel.png')) switch_user_item.set_action(True) switch_user_item.set_fanart(self.get_fanart(context)) result.append(switch_user_item) # sign out if self.is_logged_in() and settings.get_bool('youtube.folder.sign.out.show', True): - sign_out_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.sign.out']), - context.create_uri(['sign', 'out']), - image=context.create_resource_path('media', 'sign_out.png')) + sign_out_item = DirectoryItem(localize('sign.out'), + create_uri(['sign', 'out']), + image=create_path('media', 'sign_out.png')) sign_out_item.set_action(True) sign_out_item.set_fanart(self.get_fanart(context)) result.append(sign_out_item) if settings.get_bool('youtube.folder.settings.show', True): - settings_menu_item = DirectoryItem(context.localize(self.LOCAL_MAP['youtube.settings']), - context.create_uri(['config', 'youtube']), - image=context.create_resource_path('media', 'settings.png')) + settings_menu_item = DirectoryItem(localize('settings'), + create_uri(['config', 'youtube']), + image=create_path('media', 'settings.png')) settings_menu_item.set_action(True) settings_menu_item.set_fanart(self.get_fanart(context)) result.append(settings_menu_item) @@ -1647,16 +1547,16 @@ def handle_exception(self, context, exception_to_handle): context.log_error('%s: %s' % (title, log_message)) if error == 'deleted_client': - message = context.localize(self.LOCAL_MAP['youtube.key.requirement.notification']) + message = context.localize('key.requirement.notification') context.get_access_manager().update_access_token(access_token='', refresh_token='') ok_dialog = True if error == 'invalid_client': if message == 'The OAuth client was not found.': - message = context.localize(self.LOCAL_MAP['youtube.client.id.incorrect']) + message = context.localize('client.id.incorrect') message_timeout = 7000 elif message == 'Unauthorized': - message = context.localize(self.LOCAL_MAP['youtube.client.secret.incorrect']) + message = context.localize('client.secret.incorrect') message_timeout = 7000 if ok_dialog: From 4e4e2e6e1b0912f5a9be80b321784b921ecf7ce9 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 25 Nov 2023 18:30:12 +1100 Subject: [PATCH 040/141] Remove references to xbmc.translatePath - Also remove unnecessary decode to unicode - Future backports for Kodi 18 to use six/future --- .../kodion/context/xbmc/xbmc_context.py | 13 ++----------- .../youtube_plugin/kodion/json_store/json_store.py | 14 ++------------ .../youtube_plugin/kodion/network/http_server.py | 10 +--------- .../lib/youtube_plugin/kodion/utils/methods.py | 9 +-------- .../lib/youtube_plugin/kodion/utils/monitor.py | 11 +---------- resources/lib/youtube_plugin/youtube/provider.py | 9 +-------- 6 files changed, 8 insertions(+), 58 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 0ab4d9236..1e51d39c2 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -28,12 +28,6 @@ from ... import utils -try: - xbmc.translatePath = xbmcvfs.translatePath -except AttributeError: - pass - - class XbmcContext(AbstractContext): LOCAL_MAP = { 'api.id': 30202, @@ -286,16 +280,13 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override self._plugin_id = plugin_id or self._addon.getAddonInfo('id') self._plugin_name = plugin_name or self._addon.getAddonInfo('name') self._version = self._addon.getAddonInfo('version') - self._native_path = xbmc.translatePath(self._addon.getAddonInfo('path')) + self._native_path = xbmcvfs.translatePath(self._addon.getAddonInfo('path')) self._settings = XbmcPluginSettings(self._addon) """ Set the data path for this addon and create the folder """ - try: - self._data_path = xbmc.translatePath(self._addon.getAddonInfo('profile')).decode('utf-8') - except AttributeError: - self._data_path = xbmc.translatePath(self._addon.getAddonInfo('profile')) + self._data_path = xbmcvfs.translatePath(self._addon.getAddonInfo('profile')) if not xbmcvfs.exists(self._data_path): xbmcvfs.mkdir(self._data_path) diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 001397737..e1e0b196f 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -7,32 +7,22 @@ See LICENSES/GPL-2.0-only for more information. """ -import os import json +import os import xbmcaddon import xbmcvfs -import xbmc from ..logger import log_debug, log_error from ..utils import make_dirs -try: - xbmc.translatePath = xbmcvfs.translatePath -except AttributeError: - pass - - class JSONStore(object): def __init__(self, filename): addon_id = 'plugin.video.youtube' addon = xbmcaddon.Addon(addon_id) - try: - self.base_path = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') - except AttributeError: - self.base_path = xbmc.translatePath(addon.getAddonInfo('profile')) + self.base_path = xbmcvfs.translatePath(addon.getAddonInfo('profile')) if not xbmcvfs.exists(self.base_path) and not make_dirs(self.base_path): log_error('JSONStore.__init__ |{path}| invalid path'.format( diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 591810d84..910fb9a89 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -25,11 +25,6 @@ from ..settings import Settings -try: - xbmc.translatePath = xbmcvfs.translatePath -except AttributeError: - pass - _addon_id = 'plugin.video.youtube' _settings = Settings(Addon(id=_addon_id)) @@ -42,10 +37,7 @@ def __init__(self, request, client_address, server): self.whitelist_ips = whitelist_ips.split(',') self.local_ranges = ('10.', '172.16.', '192.168.', '127.0.0.1', 'localhost', '::1') self.chunk_size = 1024 * 64 - try: - self.base_path = xbmc.translatePath('special://temp/%s' % _addon_id).decode('utf-8') - except AttributeError: - self.base_path = xbmc.translatePath('special://temp/%s' % _addon_id) + self.base_path = xbmcvfs.translatePath('special://temp/%s' % _addon_id) super(YouTubeProxyRequestHandler, self).__init__(request, client_address, server) def connection_allowed(self): diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index e3fc6d72d..92b695431 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -14,7 +14,6 @@ from math import floor, log from urllib.parse import quote -import xbmc import xbmcvfs @@ -35,12 +34,6 @@ ) -try: - xbmc.translatePath = xbmcvfs.translatePath -except AttributeError: - pass - - def loose_version(v): filled = [] for point in v.split("."): @@ -242,7 +235,7 @@ def print_items(items): def make_dirs(path): if not path.endswith('/'): path = ''.join([path, '/']) - path = xbmc.translatePath(path) + path = xbmcvfs.translatePath(path) if not xbmcvfs.exists(path): try: _ = xbmcvfs.mkdirs(path) diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index 363109250..96cb77bea 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -21,12 +21,6 @@ from .. import logger -try: - xbmc.translatePath = xbmcvfs.translatePath -except AttributeError: - pass - - class YouTubeMonitor(xbmc.Monitor): # noinspection PyUnusedLocal,PyMissingConstructor @@ -150,10 +144,7 @@ def ping_httpd(self): return is_httpd_live(port=self.httpd_port()) def remove_temp_dir(self): - try: - path = xbmc.translatePath('special://temp/%s' % self.addon_id).decode('utf-8') - except AttributeError: - path = xbmc.translatePath('special://temp/%s' % self.addon_id) + path = xbmcvfs.translatePath('special://temp/%s' % self._addon_id) if os.path.isdir(path): try: diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 7cedf1085..6379f30d8 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -41,19 +41,12 @@ from ..kodion.utils import find_video_id, strip_html_from_text, FunctionCache from ..youtube.helper import yt_subscriptions -import xbmc import xbmcaddon import xbmcvfs import xbmcgui import xbmcplugin -try: - xbmc.translatePath = xbmcvfs.translatePath -except AttributeError: - pass - - class Provider(AbstractProvider): def __init__(self): super(Provider, self).__init__() @@ -1119,7 +1112,7 @@ def maintenance_actions(self, context, re_match): _file_w_path = os.path.join(context.get_data_path(), _file) if ui.on_delete_content(_file): if maint_type == 'temp_files': - _trans_path = xbmc.translatePath(_file_w_path) + _trans_path = xbmcvfs.translatePath(_file_w_path) try: xbmcvfs.rmdir(_trans_path, force=True) except: From c9851f62703495413de86edaef2577ae72e1a062 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 27 Nov 2023 23:51:19 +1100 Subject: [PATCH 041/141] Update make_comment_item - Use ui label formatting methods - Fix date and dateadded --- .../youtube_plugin/youtube/helper/utils.py | 103 +++++++++++------- 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 62d5790f5..615ef7cc9 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -47,53 +47,80 @@ def get_thumb_timestamp(minutes=15): def make_comment_item(context, snippet, uri, total_replies=0): - author = '[B]{}[/B]'.format(utils.to_str(snippet['authorDisplayName'])) - body = utils.to_str(snippet['textOriginal']) - - label_props = None - plot_props = None - is_edited = (snippet['publishedAt'] != snippet['updatedAt']) - - str_likes = ('%.1fK' % (snippet['likeCount'] / 1000.0)) if snippet['likeCount'] > 1000 else str(snippet['likeCount']) - str_replies = ('%.1fK' % (total_replies / 1000.0)) if total_replies > 1000 else str(total_replies) - - if snippet['likeCount'] and total_replies: - label_props = '[COLOR lime][B]+%s[/B][/COLOR]|[COLOR cyan][B]%s[/B][/COLOR]' % (str_likes, str_replies) - plot_props = '[COLOR lime][B]%s %s[/B][/COLOR]|[COLOR cyan][B]%s %s[/B][/COLOR]' % (str_likes, - context.localize('video.comments.likes'), str_replies, - context.localize('video.comments.replies')) - elif snippet['likeCount']: - label_props = '[COLOR lime][B]+%s[/B][/COLOR]' % str_likes - plot_props = '[COLOR lime][B]%s %s[/B][/COLOR]' % (str_likes, - context.localize('video.comments.likes')) - elif total_replies: - label_props = '[COLOR cyan][B]%s[/B][/COLOR]' % str_replies - plot_props = '[COLOR cyan][B]%s %s[/B][/COLOR]' % (str_replies, - context.localize('video.comments.replies')) - else: - pass # The comment has no likes or replies. + ui = context.get_ui() + + author = ui.bold(snippet['authorDisplayName']) + body = snippet['textOriginal'] + + label_props = [] + plot_props = [] + + like_count = snippet['likeCount'] + if like_count: + like_count = utils.friendly_number(like_count) + label_likes = ui.color('lime', ui.bold(like_count)) + plot_likes = ui.color('lime', ui.bold(' '.join(( + like_count, context.localize('video.comments.likes') + )))) + label_props.append(label_likes) + plot_props.append(plot_likes) + + if total_replies: + total_replies = utils.friendly_number(total_replies) + label_replies = ui.color('cyan', ui.bold(total_replies)) + plot_replies = ui.color('cyan', ui.bold(' '.join(( + total_replies, context.localize('video.comments.replies') + )))) + label_props.append(label_replies) + plot_props.append(plot_replies) + + published_at = snippet['publishedAt'] + updated_at = snippet['updatedAt'] + edited = published_at != updated_at + if edited: + label_props.append('*') + plot_props.append(context.localize('video.comments.edited')) # Format the label of the comment item. - edited = '[B]*[/B]' if is_edited else '' if label_props: - label = '{author} ({props}){edited} {body}'.format(author=author, props=label_props, edited=edited, - body=body.replace('\n', ' ')) + label = '{author} ({props}) {body}'.format( + author=author, + props='|'.join(label_props), + body=body.replace('\n', ' ') + ) else: - label = '{author}{edited} {body}'.format(author=author, edited=edited, body=body.replace('\n', ' ')) + label = '{author} {body}'.format( + author=author, body=body.replace('\n', ' ') + ) # Format the plot of the comment item. - edited = ' (%s)' % context.localize('video.comments.edited') if is_edited else '' if plot_props: - plot = '{author} ({props}){edited}[CR][CR]{body}'.format(author=author, props=plot_props, - edited=edited, body=body) + plot = '{author} ({props}){body}'.format( + author=author, + props='|'.join(plot_props), + body=ui.new_line(body, cr_before=2) + ) else: - plot = '{author}{edited}[CR][CR]{body}'.format(author=author, edited=edited, body=body) + plot = '{author}{body}'.format( + author=author, body=ui.new_line(body, cr_before=2) + ) comment_item = DirectoryItem(label, uri) comment_item.set_plot(plot) - comment_item.set_date_from_datetime(utils.datetime_parser.parse(snippet['publishedAt'])) + + datetime = utils.datetime_parser.parse(published_at, as_utc=True) + comment_item.set_added_utc(datetime) + local_datetime = utils.datetime_parser.utc_to_local(datetime) + comment_item.set_dateadded_from_datetime(local_datetime) + if edited: + datetime = utils.datetime_parser.parse(updated_at, as_utc=True) + local_datetime = utils.datetime_parser.utc_to_local(datetime) + comment_item.set_date_from_datetime(local_datetime) + if not uri: - comment_item.set_action(True) # Cosmetic, makes the item not a folder. + # Cosmetic, makes the item not a folder. + comment_item.set_action(True) + return comment_item @@ -456,7 +483,7 @@ def update_video_infos(provider, context, video_id_dict, # 'play with...' (external player) if alternate_player: - yt_context_menu.append_play_with(context_menu, provider, context) + yt_context_menu.append_play_with(context_menu, context) if logged_in: # add 'Watch Later' only if we are not in my 'Watch Later' list @@ -466,7 +493,7 @@ def update_video_infos(provider, context, video_id_dict, # provide 'remove' for videos in my playlists if video_id in playlist_item_id_dict: - playlist_match = re.match('^/channel/mine/playlist/(?P[^/]+)/$', context.get_path()) + playlist_match = re.match('^/channel/mine/playlist/(?P[^/]+)/$', path) if playlist_match: playlist_id = playlist_match.group('playlist_id') # we support all playlist except 'Watch History' @@ -482,7 +509,7 @@ def update_video_infos(provider, context, video_id_dict, 'video_name': video_item.get_name()} ))) - is_history = re.match('^/special/watch_history_tv/$', context.get_path()) + is_history = re.match('^/special/watch_history_tv/$', path) if is_history: yt_context_menu.append_clear_watch_history(context_menu, context) From 6d20795d4b07737678f683af32634f11a2260d7e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 27 Nov 2023 23:58:46 +1100 Subject: [PATCH 042/141] Update provider._on_users - Remove duplicated code - Change to int user indexing rather than integers cast as str - Fix issues with incorrect sorting of user (dict retaining insertion ordering requires Python 3.7+) --- .../kodion/context/xbmc/xbmc_context.py | 1 + .../kodion/json_store/login_tokens.py | 54 ++++-- .../kodion/utils/access_manager.py | 73 ++++++- .../youtube/client/__config__.py | 2 +- .../lib/youtube_plugin/youtube/provider.py | 178 +++++++----------- 5 files changed, 172 insertions(+), 136 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 1e51d39c2..ae87b3d1a 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -195,6 +195,7 @@ class XbmcContext(AbstractContext): 'updated_': 30597, 'uploads': 30726, 'user.changed': 30659, + 'user.default': 30532, 'user.enter_name': 30658, 'user.new': 30656, 'user.remove': 30662, diff --git a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py index e6eb27745..dedd70a94 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py +++ b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py @@ -13,22 +13,37 @@ # noinspection PyTypeChecker class LoginTokenStore(JSONStore): + DEFAULT_NEW_USER = { + 'access_token': '', + 'refresh_token': '', + 'token_expires': -1, + 'last_key_hash': '', + 'name': 'Default', + 'watch_later': ' WL', + 'watch_history': 'HL' + } + def __init__(self): super(LoginTokenStore, self).__init__('access_manager.json') def set_defaults(self, reset=False): data = {} if reset else self.get_data() if 'access_manager' not in data: - data = {'access_manager': {'users': {'0': {'access_token': '', 'refresh_token': '', 'token_expires': -1, - 'last_key_hash': '', 'name': 'Default', 'watch_later': ' WL', 'watch_history': 'HL'}}}} + data = { + 'access_manager': { + 'users': { + 0: self.DEFAULT_NEW_USER.copy() + } + } + } if 'users' not in data['access_manager']: - data['access_manager']['users'] = {'0': {'access_token': '', 'refresh_token': '', 'token_expires': -1, - 'last_key_hash': '', 'name': 'Default', 'watch_later': ' WL', 'watch_history': 'HL'}} - if '0' not in data['access_manager']['users']: - data['access_manager']['users']['0'] = {'access_token': '', 'refresh_token': '', 'token_expires': -1, - 'last_key_hash': '', 'name': 'Default', 'watch_later': ' WL', 'watch_history': 'HL'} + data['access_manager']['users'] = { + 0: self.DEFAULT_NEW_USER.copy() + } + if 0 not in data['access_manager']['users']: + data['access_manager']['users'][0] = self.DEFAULT_NEW_USER.copy() if 'current_user' not in data['access_manager']: - data['access_manager']['current_user'] = '0' + data['access_manager']['current_user'] = 0 if 'last_origin' not in data['access_manager']: data['access_manager']['last_origin'] = 'plugin.video.youtube' if 'developers' not in data['access_manager']: @@ -36,7 +51,7 @@ def set_defaults(self, reset=False): # clean up if data['access_manager']['current_user'] == 'default': - data['access_manager']['current_user'] = '0' + data['access_manager']['current_user'] = 0 if 'access_token' in data['access_manager']: del data['access_manager']['access_token'] if 'refresh_token' in data['access_manager']: @@ -44,13 +59,13 @@ def set_defaults(self, reset=False): if 'token_expires' in data['access_manager']: del data['access_manager']['token_expires'] if 'default' in data['access_manager']: - if (data['access_manager']['default'].get('access_token') or - data['access_manager']['default'].get('refresh_token')) and \ - (not data['access_manager']['users']['0'].get('access_token') and - not data['access_manager']['users']['0'].get('refresh_token')): + if ((data['access_manager']['default'].get('access_token') + or data['access_manager']['default'].get('refresh_token')) + and not data['access_manager']['users'][0].get('access_token') + and not data['access_manager']['users'][0].get('refresh_token')): if 'name' not in data['access_manager']['default']: data['access_manager']['default']['name'] = 'Default' - data['access_manager']['users']['0'] = data['access_manager']['default'] + data['access_manager']['users'][0] = data['access_manager']['default'] del data['access_manager']['default'] # end clean up @@ -71,3 +86,14 @@ def set_defaults(self, reset=False): # end uuid check self.save(data) + + def get_data(self): + data = super(LoginTokenStore, self).get_data() + # process users, change str keys to int + users = data['access_manager']['users'] + if '0' in users: + data['access_manager']['users'] = { + int(key): value + for key, value in users.items() + } + return data diff --git a/resources/lib/youtube_plugin/kodion/utils/access_manager.py b/resources/lib/youtube_plugin/kodion/utils/access_manager.py index f542aa747..c2f2d4d34 100644 --- a/resources/lib/youtube_plugin/kodion/utils/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/utils/access_manager.py @@ -23,7 +23,7 @@ def __init__(self, context): self._settings = context.get_settings() self._jstore = LoginTokenStore() self._json = self._jstore.get_data() - self._user = self._json['access_manager'].get('current_user', '0') + self._user = self._json['access_manager'].get('current_user', 0) self._last_origin = self._json['access_manager'].get('last_origin', 'plugin.video.youtube') def get_current_user_id(self): @@ -34,9 +34,9 @@ def get_current_user_id(self): self._json = self._jstore.get_data() return self._json['access_manager']['users'][self.get_user()]['id'] - def get_new_user(self, user_name=''): + def get_new_user(self, username=''): """ - :param user_name: string, users name + :param username: string, users name :return: a new user dict """ uuids = [ @@ -46,9 +46,16 @@ def get_new_user(self, user_name=''): new_uuid = None while not new_uuid or new_uuid in uuids: new_uuid = uuid.uuid4().hex - - return {'access_token': '', 'refresh_token': '', 'token_expires': -1, 'last_key_hash': '', - 'name': user_name, 'id': new_uuid, 'watch_later': ' WL', 'watch_history': 'HL'} + return { + 'access_token': '', + 'refresh_token': '', + 'token_expires': -1, + 'last_key_hash': '', + 'name': username, + 'id': new_uuid, + 'watch_later': ' WL', + 'watch_history': 'HL' + } def get_users(self): """ @@ -57,9 +64,36 @@ def get_users(self): """ return self._json['access_manager'].get('users', {}) + def add_user(self, username='', user=None): + """ + Add single new user to users collection + :param username: str, chosen name of new user + :param user: int, optional index for new user + :return: tuple, (index, details) of newly added user + """ + users = self._json['access_manager'].get('users', {}) + new_user_details = self.get_new_user(username) + new_user = max(users) + 1 if users and user is None else user or 0 + users[new_user] = new_user_details + self._json['access_manager']['users'] = users + self._jstore.save(self._json) + return new_user, new_user_details + + def remove_user(self, user): + """ + Remove user from collection of current users + :param user: int, user index + :return: + """ + users = self._json['access_manager'].get('users', {}) + if user in users: + del users[user] + self._json['access_manager']['users'] = users + self._jstore.save(self._json) + def set_users(self, users): """ - Updates the users + Updates all users :param users: dict, users :return: """ @@ -87,6 +121,31 @@ def get_user(self): """ return self._user + def get_username(self, user=None): + """ + Returns the username of the current or nominated user + :return: username + """ + if user is None: + user = self._user + users = self._json['access_manager'].get('users', {}) + if user in users: + return users[user].get('name') + return '' + + def set_username(self, user, username): + """ + Sets the username of the nominated user + :return: True if username was set, false otherwise + """ + users = self._json['access_manager'].get('users', {}) + if user in users: + users[user]['name'] = username + self._json['access_manager']['users'] = users + self._jstore.save(self._json) + return True + return False + def get_watch_later_id(self): """ Returns the current users watch later playlist id diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index a7ee4818b..ed780b264 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -114,7 +114,7 @@ def get_current_switch(self): def get_current_user(self): self._json_am = self._am_jstore.get_data() - return self._json_am['access_manager'].get('current_user', '0') + return self._json_am['access_manager'].get('current_user', 0) def has_own_api_keys(self): self._json_api = self._api_jstore.get_data() diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 6379f30d8..59acb5d3c 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -695,151 +695,101 @@ def _on_users(self, context, re_match): access_manager = context.get_access_manager() ui = context.get_ui() - def add_user(_access_manager_users): - _results = ui.on_keyboard_input(localize('user.enter_name')) - if _results[0] is False: - return None - _new_user_name = _results[1] - if not _new_user_name.strip(): - _new_user_name = localize('user.unnamed') - _new_users = { - str(idx): user - for idx, user in enumerate(_access_manager_users.values()) - } - _new_users[str(len(_new_users))] = access_manager.get_new_user(_new_user_name) - access_manager.set_users(_new_users) - return str(len(_new_users) - 1) - - def switch_to_user(_user): - _user_name = access_manager.get_users()[_user].get('name', localize('user.unnamed')) - access_manager.set_user(_user, switch_to=True) - ui.show_notification(localize('user.changed') % _user_name, - localize('user.switch')) + def select_user(reason, new_user=False): + current_users = access_manager.get_users() + current_user = access_manager.get_user() + usernames = [] + for user, details in sorted(current_users.items()): + username = details.get('name') or localize('user.unnamed') + if user == current_user: + username = '> ' + ui.bold(username) + if details.get('access_token') or details.get('refresh_token'): + username = ui.color('limegreen', username) + usernames.append(username) + if new_user: + usernames.append(ui.italic(localize('user.new'))) + return ui.on_select(reason, usernames), sorted(current_users.keys()) + + def add_user(): + results = ui.on_keyboard_input(localize('user.enter_name')) + if results[0] is False: + return None, None + new_username = results[1].strip() + if not new_username: + new_username = localize('user.unnamed') + return access_manager.add_user(new_username) + + def switch_to_user(user): + access_manager.set_user(user, switch_to=True) + ui.show_notification( + localize('user.changed') % access_manager.get_username(user), + localize('user.switch') + ) self.get_resource_manager(context).clear() if refresh: ui.refresh_container() if action == 'switch': - access_manager_users = access_manager.get_users() - current_user = access_manager.get_user() - users = [ui.bold(localize('user.new'))] - user_index_map = [] - for user, details in access_manager_users.items(): - if user == current_user: - if details.get('access_token') or details.get('refresh_token'): - users.append( - ui.color('limegreen', - ' '.join([details.get('name', localize('user.unnamed')), '*'])) - ) - else: - users.append(' '.join([details.get('name', localize('user.unnamed')), '*'])) - elif details.get('access_token') or details.get('refresh_token'): - users.append(ui.color('limegreen', details.get('name', localize('user.unnamed')))) - else: - users.append(details.get('name', localize('user.unnamed'))) - user_index_map.append(user) - result = ui.on_select(localize('user.switch'), users) + result, user_index_map = select_user(localize('user.switch'), + new_user=True) if result == -1: return True - if result == 0: - user = add_user(access_manager_users) + if result == len(user_index_map): + user, _ = add_user() else: - user = user_index_map[result - 1] + user = user_index_map[result] - if user and (user != access_manager.get_user()): + if user is not None and user != access_manager.get_user(): switch_to_user(user) elif action == 'add': - user = add_user(access_manager.get_users()) - if user: - user_name = access_manager.get_users()[user].get('name', localize('user.unnamed')) - result = ui.on_yes_no_input(localize('user.switch'), localize('user.switch.now') % user_name) + user, details = add_user() + if user is not None: + result = ui.on_yes_no_input( + localize('user.switch'), + localize('user.switch.now') % details.get('name') + ) if result: switch_to_user(user) elif action == 'remove': - access_manager_users = access_manager.get_users() - users = [] - user_index_map = [] - current_user = access_manager.get_user() - current_user_idx = '0' - for user, details in access_manager_users.items(): - if user == current_user: - current_user_idx = str(len(user_index_map)) - if details.get('access_token') or details.get('refresh_token'): - users.append( - ui.color('limegreen', - ' '.join([details.get('name', localize('user.unnamed')), '*'])) - ) - else: - users.append(' '.join([details.get('name', localize('user.unnamed')), '*'])) - elif details.get('access_token') or details.get('refresh_token'): - users.append(ui.color('limegreen', details.get('name', localize('user.unnamed')))) - else: - users.append(details.get('name', localize('user.unnamed'))) - user_index_map.append(user) - result = ui.on_select(localize('user.remove'), users) + result, user_index_map = select_user(localize('user.remove')) if result == -1: return True user = user_index_map[result] - user_name = access_manager_users[user].get('name', localize('user.unnamed')) - result = ui.on_remove_content(user_name) - if result: - if user == current_user: - access_manager.set_user('0', switch_to=True) - del access_manager_users[user] - - new_users = { - str(idx): user - for idx, user in enumerate(access_manager_users.values()) - } - - if current_user_idx in new_users: - access_manager.set_user(current_user_idx, switch_to=True) - else: - access_manager.set_user('0', switch_to=True) - - access_manager.set_users(new_users) - ui.show_notification(localize('removed') % user_name, + username = access_manager.get_username(user) + if ui.on_remove_content(username): + access_manager.remove_user(user) + if user == 0: + access_manager.add_user(username=localize('user.default'), + user=0) + if user == access_manager.get_user(): + access_manager.set_user(0, switch_to=True) + ui.show_notification(localize('removed') % username, localize('remove')) elif action == 'rename': - access_manager_users = access_manager.get_users() - users = [] - user_index_map = [] - current_user = access_manager.get_user() - for user, details in access_manager_users.items(): - if user == current_user: - if details.get('access_token') or details.get('refresh_token'): - users.append( - ui.color('limegreen', - ' '.join([details.get('name', localize('user.unnamed')), '*'])) - ) - else: - users.append(' '.join([details.get('name', localize('user.unnamed')), '*'])) - elif details.get('access_token') or details.get('refresh_token'): - users.append(ui.color('limegreen', details.get('name', localize('user.unnamed')))) - else: - users.append(details.get('name', localize('user.unnamed'))) - user_index_map.append(user) - result = ui.on_select(localize('user.rename'), users) + result, user_index_map = select_user(localize('user.rename')) if result == -1: return True user = user_index_map[result] - old_user_name = access_manager_users[user].get('name', localize('user.unnamed')) - results = ui.on_keyboard_input(localize('user.enter_name'), default=old_user_name) + old_username = access_manager.get_username(user) + results = ui.on_keyboard_input(localize('user.enter_name'), + default=old_username) if results[0] is False: return True - new_user_name = results[1] - if not new_user_name.strip() or (old_user_name == new_user_name): + new_username = results[1].strip() + if not new_username: + new_username = localize('user.unnamed') + if old_username == new_username: return True - access_manager_users[user]['name'] = new_user_name - access_manager.set_users(access_manager_users) - ui.show_notification(localize('renamed') % (old_user_name, new_user_name), - localize('rename')) + if access_manager.set_username(user, new_username): + ui.show_notification(localize('renamed') % (old_username, + new_username), + localize('rename')) return True From af09b7bec91b1b0034a4bcdecf4b6a8bdec13c8a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:36:56 +1100 Subject: [PATCH 043/141] Fix issues with (non)playable items - Fix UriItem always using setResolvedUril rather than RunPlugin - Ensure ListItem is created with path - Standardise naming of ListItem creation methods - Shorten name of (get|set|clear)_home_window_property to (get|set|clear)_property - Use string type return rather than None - Fix setting isPlayable based on item type - Fix filtering shorts based on duration of playable items - Remove filtering shorts on list of non playable items - Fix routing of /play/ with playlist_id/playlist_ids params - Fixes #115 and #480 - Possibly fixes #485 --- .../kodion/context/xbmc/xbmc_context.py | 2 +- .../youtube_plugin/kodion/items/__init__.py | 18 +- .../youtube_plugin/kodion/items/audio_item.py | 4 +- .../youtube_plugin/kodion/items/base_item.py | 6 + .../youtube_plugin/kodion/items/uri_item.py | 4 +- .../youtube_plugin/kodion/items/video_item.py | 4 +- .../kodion/player/xbmc/xbmc_playlist.py | 2 +- .../kodion/plugin/xbmc/xbmc_runner.py | 58 ++-- .../lib/youtube_plugin/kodion/service.py | 4 +- .../kodion/ui/xbmc/xbmc_context_ui.py | 8 +- .../kodion/ui/xbmc/xbmc_items.py | 197 ++++++----- .../lib/youtube_plugin/kodion/utils/player.py | 7 +- .../youtube/helper/subtitles.py | 7 +- .../lib/youtube_plugin/youtube/helper/tv.py | 2 - .../youtube/helper/url_to_item_converter.py | 26 +- .../youtube_plugin/youtube/helper/utils.py | 20 +- .../youtube_plugin/youtube/helper/yt_play.py | 320 +++++++++--------- .../lib/youtube_plugin/youtube/provider.py | 93 +++-- 18 files changed, 410 insertions(+), 372 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index ae87b3d1a..66fdf6a94 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -556,4 +556,4 @@ def inputstream_adaptive_auto_stream_selection(): return False def abort_requested(self): - return str(self.get_ui().get_home_window_property('abort_requested')).lower() == 'true' + return self.get_ui().get_property('abort_requested').lower() == 'true' diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index 536a4ce38..be32dbd74 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -24,6 +24,18 @@ from .image_item import ImageItem -__all__ = ['BaseItem', 'AudioItem', 'DirectoryItem', 'VideoItem', 'ImageItem', 'WatchLaterItem', 'FavoritesItem', - 'SearchItem', 'NewSearchItem', 'SearchHistoryItem', 'NextPageItem', 'UriItem', - 'from_json', 'to_json', 'to_jsons'] +__all__ = ('AudioItem', + 'BaseItem', + 'DirectoryItem', + 'FavoritesItem', + 'ImageItem', + 'NewSearchItem', + 'NextPageItem', + 'SearchHistoryItem', + 'SearchItem', + 'UriItem', + 'VideoItem', + 'WatchLaterItem', + 'from_json', + 'to_json', + 'to_jsons') diff --git a/resources/lib/youtube_plugin/kodion/items/audio_item.py b/resources/lib/youtube_plugin/kodion/items/audio_item.py index bd0c674e5..a7170fc5a 100644 --- a/resources/lib/youtube_plugin/kodion/items/audio_item.py +++ b/resources/lib/youtube_plugin/kodion/items/audio_item.py @@ -14,9 +14,11 @@ class AudioItem(BaseItem): + _playable = True + def __init__(self, name, uri, image='', fanart=''): super(AudioItem, self).__init__(name, uri, image, fanart) - self._duration = None + self._duration = -1 self._track_number = None self._year = None self._genre = None diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 02c96ff79..abd5adb8e 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -19,6 +19,8 @@ class BaseItem(object): VERSION = 3 INFO_DATE = 'date' # (string) iso 8601 + _playable = False + def __init__(self, name, uri, image='', fanart=''): self._version = BaseItem.VERSION @@ -150,3 +152,7 @@ def next_page(self): @next_page.setter def next_page(self, value): self._next_page = bool(value) + + @property + def playable(cls): + return cls._playable diff --git a/resources/lib/youtube_plugin/kodion/items/uri_item.py b/resources/lib/youtube_plugin/kodion/items/uri_item.py index a3a80b702..2c3e2141d 100644 --- a/resources/lib/youtube_plugin/kodion/items/uri_item.py +++ b/resources/lib/youtube_plugin/kodion/items/uri_item.py @@ -12,5 +12,7 @@ class UriItem(BaseItem): - def __init__(self, uri): + def __init__(self, uri, playable=None): super(UriItem, self).__init__(name='', uri=uri) + if playable is not None: + self._playable = playable diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 5aca523a6..c605d3d4d 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -19,12 +19,14 @@ class VideoItem(BaseItem): + _playable = True + def __init__(self, name, uri, image='', fanart=''): super(VideoItem, self).__init__(name, uri, image, fanart) self._genre = None self._aired = None self._scheduled_start_utc = None - self._duration = None + self._duration = -1 self._director = None self._premiered = None self._episode = None diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py index 7eb8a5a1e..69c603e04 100644 --- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py @@ -30,7 +30,7 @@ def clear(self): self._playlist.clear() def add(self, base_item): - item = xbmc_items.to_video_item(self._context, base_item) + item = xbmc_items.video_listitem(self._context, base_item) if item: self._playlist.add(base_item.get_uri(), listitem=item) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index 4b84d1cab..37fe89365 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -14,7 +14,6 @@ from ..abstract_provider_runner import AbstractProviderRunner from ...exceptions import KodionException from ...items import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem -from ... import AbstractProvider from ...ui.xbmc import info_labels, xbmc_items @@ -43,9 +42,9 @@ def run(self, provider, context): options = {} options.update(results[1]) - if isinstance(result, bool) and not result: - xbmcplugin.endOfDirectory(self.handle, succeeded=False) - return False + if isinstance(result, bool): + xbmcplugin.endOfDirectory(self.handle, succeeded=result) + return result if isinstance(result, (VideoItem, AudioItem, UriItem)): return self._set_resolved_url(context, result) @@ -75,27 +74,28 @@ def run(self, provider, context): xbmcplugin.endOfDirectory( self.handle, succeeded=succeeded, - updateListing=options.get(AbstractProvider.RESULT_UPDATE_LISTING, False), - cacheToDisc=options.get(AbstractProvider.RESULT_CACHE_TO_DISC, True) + updateListing=options.get(provider.RESULT_UPDATE_LISTING, False), + cacheToDisc=options.get(provider.RESULT_CACHE_TO_DISC, True) ) return succeeded - def _set_resolved_url(self, context, base_item, succeeded=True): - item = xbmc_items.to_playback_item(context, base_item) - item.setPath(base_item.get_uri()) - xbmcplugin.setResolvedUrl(self.handle, succeeded=succeeded, listitem=item) - """ - # just to be sure :) - if not isLiveStream: - tries = 100 - while tries>0: - xbmc.sleep(50) - if xbmc.Player().isPlaying() and xbmc.getCondVisibility("Player.Paused"): - xbmc.Player().pause() - break - tries-=1 - """ - return succeeded + def _set_resolved_url(self, context, base_item): + if base_item.playable: + item = xbmc_items.to_playback_item(context, base_item) + xbmcplugin.setResolvedUrl(self.handle, + succeeded=True, + listitem=item) + return True + + uri = base_item.get_uri() + if uri.startswith('plugin://'): + context.log_debug('Redirecting to |{0}|'.format(uri)) + context.execute('RunPlugin({0})'.format(uri)) + xbmcplugin.endOfDirectory(self.handle, + succeeded=False, + updateListing=False, + cacheToDisc=False) + return False @staticmethod def _add_directory(directory_item, show_fanart=False): @@ -103,9 +103,8 @@ def _add_directory(directory_item, show_fanart=False): 'thumb': directory_item.get_image()} item = xbmcgui.ListItem(label=directory_item.get_name(), offscreen=True) - - info = info_labels.create_from_item(directory_item) - xbmc_items.set_info_tag(item, info, 'video') + item_info = info_labels.create_from_item(directory_item) + xbmc_items.set_info_tag(item, item_info, 'video') # only set fanart is enabled if show_fanart: @@ -121,10 +120,7 @@ def _add_directory(directory_item, show_fanart=False): item.setPath(directory_item.get_uri()) - is_folder = True - if directory_item.is_action(): - is_folder = False - item.setProperty('isPlayable', 'false') + is_folder = not directory_item.is_action() if directory_item.next_page: item.setProperty('specialSort', 'bottom') @@ -136,7 +132,7 @@ def _add_directory(directory_item, show_fanart=False): @staticmethod def _add_video(context, video_item): - item = xbmc_items.to_video_item(context, video_item) + item = xbmc_items.video_listitem(context, video_item) item.setPath(video_item.get_uri()) return video_item.get_uri(), item, False @@ -165,6 +161,6 @@ def _add_image(image_item, show_fanart=False): @staticmethod def _add_audio(context, audio_item): - item = xbmc_items.to_audio_item(context, audio_item) + item = xbmc_items.audio_listitem(context, audio_item) item.setPath(audio_item.get_uri()) return audio_item.get_uri(), item, False diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index 5c75b7d3b..29d2a90e3 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -66,7 +66,7 @@ def run(): # prevent service to failing due to cache related issues pass - context.get_ui().clear_home_window_property('abort_requested') + context.get_ui().clear_property('abort_requested') while not monitor.abortRequested(): @@ -84,7 +84,7 @@ def run(): if monitor.waitForAbort(sleep_time): break - context.get_ui().set_home_window_property('abort_requested', 'true') + context.get_ui().set_property('abort_requested', 'true') player.cleanup_threads(only_ended=False) # clean up any/all playback monitoring threads diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 460683607..fb47f7684 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -153,17 +153,17 @@ def get_info_label(value): return xbmc.getInfoLabel(value) @staticmethod - def set_home_window_property(property_id, value): + def set_property(property_id, value): property_id = ''.join(['plugin.video.youtube-', property_id]) xbmcgui.Window(10000).setProperty(property_id, value) @staticmethod - def get_home_window_property(property_id): + def get_property(property_id): property_id = ''.join(['plugin.video.youtube-', property_id]) - return xbmcgui.Window(10000).getProperty(property_id) or None + return xbmcgui.Window(10000).getProperty(property_id) @staticmethod - def clear_home_window_property(property_id): + def clear_property(property_id): property_id = ''.join(['plugin.video.youtube-', property_id]) xbmcgui.Window(10000).clearProperty(property_id) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index e58372b02..95c51a123 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -10,15 +10,20 @@ from xbmcgui import ListItem +from . import info_labels +from ...items import VideoItem, AudioItem, UriItem +from ...utils import datetime_parser + try: from infotagger.listitem import set_info_tag except ImportError: - def set_info_tag(listitem, info, tag_type, *_args, **_kwargs): - listitem.setInfo(tag_type, info) + def set_info_tag(listitem, infolabels, tag_type, *_args, **_kwargs): + listitem.setInfo(tag_type, infolabels) return ListItemInfoTag(listitem, tag_type) + class ListItemInfoTag(object): - __slots__ = ('__li__', ) + __slots__ = ('__li__',) def __init__(self, listitem, *_args, **_kwargs): self.__li__ = listitem @@ -26,51 +31,57 @@ def __init__(self, listitem, *_args, **_kwargs): def add_stream_info(self, *args, **kwargs): return self.__li__.addStreamInfo(*args, **kwargs) - def set_resume_point(self, *_args, **_kwargs): - pass + def set_resume_point(self, + infoproperties, + *_args, + resume_key='ResumeTime', + total_key='TotalTime', + **_kwargs): + if resume_key in infoproperties: + infoproperties[resume_key] = str(infoproperties[resume_key]) + if total_key in infoproperties: + infoproperties[total_key] = str(infoproperties[total_key]) -from . import info_labels -from ...items import VideoItem, AudioItem, UriItem -from ...utils import datetime_parser - -def to_play_item(context, play_item): - uri = play_item.get_uri() - context.log_debug('Converting PlayItem |%s|' % uri) +def video_playback_item(context, video_item): + uri = video_item.get_uri() + context.log_debug('Converting VideoItem |%s|' % uri) settings = context.get_settings() - headers = play_item.get_headers() - license_key = play_item.get_license_key() + headers = video_item.get_headers() + license_key = video_item.get_license_key() alternative_player = settings.is_support_alternative_player_enabled() is_strm = context.get_param('strm') mime_type = None kwargs = { 'label': (None if is_strm - else (play_item.get_title() or play_item.get_name())), - 'label2': None if is_strm else play_item.get_short_details(), + else (video_item.get_title() or video_item.get_name())), + 'label2': None if is_strm else video_item.get_short_details(), + 'path': uri, 'offscreen': True, } props = { - 'isPlayable': 'true', + 'isPlayable': str(video_item.playable).lower(), } if (alternative_player and settings.alternative_player_web_urls() and not license_key): - play_item.set_uri('https://www.youtube.com/watch?v={video_id}'.format( - video_id=play_item.video_id + video_item.set_uri('https://www.youtube.com/watch?v={video_id}'.format( + video_id=video_item.video_id )) - - elif (play_item.use_isa_video() + elif (video_item.use_isa_video() and context.addon_enabled('inputstream.adaptive')): - if play_item.use_mpd_video(): + if video_item.use_mpd_video(): manifest_type = 'mpd' mime_type = 'application/xml+dash' + """ # MPD manifest update is currently broken - # Following line will force a full update but restart live stream from start - # if play_item.live: - # props['inputstream.adaptive.manifest_update_parameter'] = 'full' + # Following line will force a full update but restart live stream + if video_item.live: + props['inputstream.adaptive.manifest_update_parameter'] = 'full' + """ if 'auto' in settings.stream_select(): props['inputstream.adaptive.stream_selection_type'] = 'adaptive' else: @@ -90,10 +101,11 @@ def to_play_item(context, play_item): else: if 'mime=' in uri: - mime_type = uri.partition('mime=')[2].partition('&')[0].replace('%2F', '/') + mime_type = uri.split('mime=', 1)[1].split('&', 1)[0] + mime_type = mime_type.replace('%2F', '/') if not alternative_player and headers and uri.startswith('http'): - play_item.set_uri('|'.join([uri, headers])) + video_item.set_uri('|'.join([uri, headers])) list_item = ListItem(**kwargs) if mime_type: @@ -107,40 +119,86 @@ def to_play_item(context, play_item): if 'ResumeTime' in props: del props['ResumeTime'] - prop_value = play_item.get_duration() + prop_value = video_item.get_duration() if prop_value: - props['TotalTime'] = str(prop_value) + props['TotalTime'] = prop_value - fanart = settings.show_fanart() and play_item.get_fanart() or '' - thumb = play_item.get_image() or 'DefaultVideo.png' + fanart = settings.show_fanart() and video_item.get_fanart() or '' + thumb = video_item.get_image() or 'DefaultVideo.png' list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) - if play_item.subtitles: - list_item.setSubtitles(play_item.subtitles) + if video_item.subtitles: + list_item.setSubtitles(video_item.subtitles) - info = info_labels.create_from_item(play_item) - info_tag = set_info_tag(list_item, info, 'video') + item_info = info_labels.create_from_item(video_item) + info_tag = set_info_tag(list_item, item_info, 'video') info_tag.set_resume_point(props) # This should work for all versions of XBMC/KODI. - if 'duration' in info: - info_tag.add_stream_info('video', {'duration': info['duration']}) + if 'duration' in item_info: + info_tag.add_stream_info('video', {'duration': item_info['duration']}) list_item.setProperties(props) return list_item -def to_video_item(context, video_item): - context.log_debug('Converting VideoItem |%s|' % video_item.get_uri()) +def audio_listitem(context, audio_item): + uri = audio_item.get_uri() + context.log_debug('Converting AudioItem |%s|' % uri) + + kwargs = { + 'label': audio_item.get_title() or audio_item.get_name(), + 'label2': audio_item.get_short_details(), + 'path': uri, + 'offscreen': True, + } + props = { + 'isPlayable': str(audio_item.playable).lower(), + } + + list_item = ListItem(**kwargs) + + fanart = (context.get_settings().show_fanart() + and audio_item.get_fanart() + or '') + thumb = audio_item.get_image() or 'DefaultAudio.png' + list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) + + item_info = info_labels.create_from_item(audio_item) + set_info_tag(list_item, item_info, 'music') + + list_item.setProperties(props) + + context_menu = audio_item.get_context_menu() + if context_menu: + list_item.addContextMenuItems( + context_menu, replaceItems=audio_item.replace_context_menu() + ) + + return list_item + + +def uri_listitem(context, base_item): + uri = base_item.get_uri() + context.log_debug('Converting UriItem |%s|' % uri) + item = ListItem(path=uri, offscreen=True) + item.setProperty('IsPlayable', str(base_item.playable).lower()) + return item + + +def video_listitem(context, video_item): + uri = video_item.get_uri() + context.log_debug('Converting VideoItem |%s|' % uri) kwargs = { 'label': video_item.get_title() or video_item.get_name(), 'label2': video_item.get_short_details(), + 'path': uri, 'offscreen': True, } props = { - 'isPlayable': 'true', + 'isPlayable': str(video_item.playable).lower(), } list_item = ListItem(**kwargs) @@ -161,11 +219,11 @@ def to_video_item(context, video_item): prop_value = video_item.get_start_time() if prop_value: - props['ResumeTime'] = str(prop_value) + props['ResumeTime'] = prop_value prop_value = video_item.get_duration() if prop_value: - props['TotalTime'] = str(prop_value) + props['TotalTime'] = prop_value # make channel_id property available for keymapping prop_value = video_item.get_channel_id() @@ -196,13 +254,13 @@ def to_video_item(context, video_item): if video_item.subtitles: list_item.setSubtitles(video_item.subtitles) - info = info_labels.create_from_item(video_item) - info_tag = set_info_tag(list_item, info, 'video') + item_info = info_labels.create_from_item(video_item) + info_tag = set_info_tag(list_item, item_info, 'video') info_tag.set_resume_point(props) # This should work for all versions of XBMC/KODI. - if 'duration' in info: - info_tag.add_stream_info('video', {'duration': info['duration']}) + if 'duration' in item_info: + info_tag.add_stream_info('video', {'duration': item_info['duration']}) list_item.setProperties(props) @@ -215,55 +273,14 @@ def to_video_item(context, video_item): return list_item -def to_audio_item(context, audio_item): - context.log_debug('Converting AudioItem |%s|' % audio_item.get_uri()) - - kwargs = { - 'label': audio_item.get_title() or audio_item.get_name(), - 'label2': audio_item.get_short_details(), - 'offscreen': True, - } - props = { - 'isPlayable': 'true', - } - - list_item = ListItem(**kwargs) - - fanart = (context.get_settings().show_fanart() - and audio_item.get_fanart() - or '') - thumb = audio_item.get_image() or 'DefaultAudio.png' - list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) - - info = info_labels.create_from_item(audio_item) - set_info_tag(list_item, info, 'music') - - list_item.setProperties(props) - - context_menu = audio_item.get_context_menu() - if context_menu: - list_item.addContextMenuItems( - context_menu, replaceItems=audio_item.replace_context_menu() - ) - - return list_item - - -def to_uri_item(context, base_item): - context.log_debug('Converting UriItem') - item = ListItem(path=base_item.get_uri(), offscreen=True) - item.setProperty('IsPlayable', 'true') - return item - - def to_playback_item(context, base_item): if isinstance(base_item, UriItem): - return to_uri_item(context, base_item) + return uri_listitem(context, base_item) if isinstance(base_item, AudioItem): - return to_audio_item(context, base_item) + return audio_listitem(context, base_item) if isinstance(base_item, VideoItem): - return to_play_item(context, base_item) + return video_playback_item(context, base_item) return None diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index 0374d98da..de21377ea 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -399,9 +399,10 @@ def cleanup_threads(self, only_ended=True): self.threads = active_threads def onPlayBackStarted(self): - if self.ui.get_home_window_property('playback_json'): - playback_json = json.loads(self.ui.get_home_window_property('playback_json')) - self.ui.clear_home_window_property('playback_json') + playback_json = self.ui.get_property('playback_json') + if playback_json: + playback_json = json.loads(playback_json) + self.ui.clear_property('playback_json') self.cleanup_threads() self.threads.append(PlaybackMonitorThread(self.provider, self.context, playback_json)) diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 0e3c767be..af604d8d6 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -40,10 +40,9 @@ def __init__(self, context, video_id, captions, headers=None): self.headers = headers ui = self.context.get_ui() - self.prompt_override = ( - ui.get_home_window_property('prompt_for_subtitles') == video_id - ) - ui.clear_home_window_property('prompt_for_subtitles') + self.prompt_override = (ui.get_property('prompt_for_subtitles') + == video_id) + ui.clear_property('prompt_for_subtitles') self.renderer = captions.get('playerCaptionsTracklistRenderer', {}) self.caption_tracks = self.renderer.get('captionTracks', []) diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index a78b39dde..c280beb54 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -148,8 +148,6 @@ def saved_playlists_to_items(provider, context, json_data): utils.update_playlist_infos(provider, context, playlist_id_dict, channel_items_dict) utils.update_fanarts(provider, context, channel_items_dict) - result = utils.filter_short_videos(context, result) - # next page next_page_token = json_data.get('next_page_token', '') if next_page_token or json_data.get('continue', False): diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index 192c7c225..20f0d4452 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -9,11 +9,10 @@ """ import re -from urllib.parse import urlparse -from urllib.parse import parse_qsl +from urllib.parse import parse_qsl, urlparse -from ...kodion.items import VideoItem, DirectoryItem from . import utils +from ...kodion.items import DirectoryItem, UriItem, VideoItem class UrlToItemConverter(object): @@ -128,8 +127,9 @@ def get_items(self, provider, context, title_required=True): channels_item = DirectoryItem( context.get_ui().bold(context.localize('channels')), - context.create_uri(['special', 'description_links'], - {'channel_ids': ','.join(self._channel_ids)}), + context.create_uri(['special', 'description_links'], { + 'channel_ids': ','.join(self._channel_ids), + }), context.create_resource_path('media', 'playlist.png') ) channels_item.set_fanart(provider.get_fanart(context)) @@ -139,11 +139,19 @@ def get_items(self, provider, context, title_required=True): # remove duplicates self._playlist_ids = list(set(self._playlist_ids)) - playlists_item = DirectoryItem( + playlists_item = UriItem( + context.create_uri(['play'], { + 'playlist_ids': ','.join(self._playlist_ids), + 'play': True, + }), + playable=True + ) if context.get_param('uri') else DirectoryItem( context.get_ui().bold(context.localize('playlists')), - context.create_uri(['special', 'description_links'], - {'playlist_ids': ','.join(self._playlist_ids)}), - context.create_resource_path('media', 'playlist.png')) + context.create_uri(['special', 'description_links'], { + 'playlist_ids': ','.join(self._playlist_ids), + }), + context.create_resource_path('media', 'playlist.png') + ) playlists_item.set_fanart(provider.get_fanart(context)) result.append(playlists_item) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 615ef7cc9..90b19f3c5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -589,8 +589,8 @@ def update_play_info(provider, context, video_id, video_item, video_stream, ISHelper('mpd', drm='com.widevine.alpha').check_inputstream() video_item.set_license_key(license_proxy) - ui.set_home_window_property('license_url', license_url) - ui.set_home_window_property('license_token', license_token) + ui.set_property('license_url', license_url) + ui.set_property('license_token', license_token) def update_fanarts(provider, context, channel_items_dict, data=None): @@ -694,15 +694,9 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id): def filter_short_videos(context, items): if context.get_settings().hide_short_videos(): - shorts_filtered = [] - - for item in items: - if hasattr(item, '_duration'): - item_duration = 0 if item.get_duration() is None else item.get_duration() - if 0 < item_duration <= 60: - continue - shorts_filtered += [item] - - return shorts_filtered - + items = [ + item + for item in items + if item.playable and not 0 <= item.get_duration() <= 60 + ] return items diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 65ada0f74..1df2af008 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -10,193 +10,193 @@ import json import random -import re import traceback -import xbmcplugin - from ... import kodion from ...kodion.items import VideoItem -from ...kodion.ui.xbmc.xbmc_items import to_playback_item -from ...youtube.youtube_exceptions import YouTubeException from ...youtube.helper import utils, v3 +from ...youtube.youtube_exceptions import YouTubeException def play_video(provider, context): + params = context.get_params() + video_id = params.get('video_id') + + client = provider.get_client(context) + settings = context.get_settings() + ui = context.get_ui() + + ask_for_quality = None + if video_id and ui.get_property('ask_for_quality') == video_id: + ask_for_quality = True + ui.clear_property('ask_for_quality') + + screensaver = False + if params.get('screensaver'): + ask_for_quality = False + screensaver = True + + audio_only = None + if video_id and ui.get_property('audio_only') == video_id: + ask_for_quality = False + audio_only = True + ui.clear_property('audio_only') + try: - video_id = context.get_param('video_id') + video_streams = client.get_video_streams(context, video_id) + except YouTubeException as e: + ui.show_notification(message=e.get_message()) + context.log_error(traceback.print_exc()) + return False - client = provider.get_client(context) - settings = context.get_settings() + if not video_streams: + message = context.localize('error.no_video_streams_found') + ui.show_notification(message, time_milliseconds=5000) + return False - ask_for_quality = None - if video_id and context.get_ui().get_home_window_property('ask_for_quality') == video_id: - ask_for_quality = True - context.get_ui().clear_home_window_property('ask_for_quality') + video_stream = kodion.utils.select_stream( + context, + video_streams, + ask_for_quality=ask_for_quality, + audio_only=audio_only + ) - screensaver = False - if context.get_param('screensaver'): - ask_for_quality = False - screensaver = True + if video_stream is None: + return False - audio_only = None - if video_id and context.get_ui().get_home_window_property('audio_only') == video_id: - ask_for_quality = False - audio_only = True - context.get_ui().clear_home_window_property('audio_only') + is_video = video_stream.get('video') + is_live = video_stream.get('Live') - try: - video_streams = client.get_video_streams(context, video_id) - except YouTubeException as e: - context.get_ui().show_notification(message=e.get_message()) - context.log_error(traceback.print_exc()) - return False - - if not video_streams: - message = context.localize('error.no_video_streams_found') - context.get_ui().show_notification(message, time_milliseconds=5000) - return False - - video_stream = kodion.utils.select_stream(context, video_streams, ask_for_quality=ask_for_quality, audio_only=audio_only) - - if video_stream is None: - return False - - is_video = video_stream.get('video') - is_live = video_stream.get('Live') - - if is_video and video_stream['video'].get('rtmpe', False): - message = context.localize('error.rtmpe_not_supported') - context.get_ui().show_notification(message, time_milliseconds=5000) - return False - - play_suggested = settings.get_bool('youtube.suggested_videos', False) - if play_suggested and not screensaver: - utils.add_related_video_to_playlist(provider, context, client, v3, video_id) - - metadata = video_stream.get('meta', {}) - - title = metadata.get('video', {}).get('title', '') - video_item = VideoItem(title, video_stream['url']) - - incognito = context.get_param('incognito', False) - use_history = not is_live and not screensaver and not incognito - use_remote_history = use_history and settings.use_remote_history() - use_play_data = use_history and settings.use_local_history() - - utils.update_play_info(provider, context, video_id, video_item, - video_stream, use_play_data=use_play_data) - - seek_time = 0.0 - play_count = 0 - playback_stats = video_stream.get('playback_stats') - - if not context.get_param('resume'): - try: - seek_time = context.get_param('seek', 0.0) - except (ValueError, TypeError): - pass - - if use_play_data: - play_count = video_item.get_play_count() or 0 - - item = to_playback_item(context, video_item) - item.setPath(video_item.get_uri()) - - playback_json = { - "video_id": video_id, - "channel_id": metadata.get('channel', {}).get('id', ''), - "video_status": metadata.get('video', {}).get('status', {}), - "playing_file": video_item.get_uri(), - "play_count": play_count, - "use_remote_history": use_remote_history, - "use_local_history": use_play_data, - "playback_stats": playback_stats, - "seek_time": seek_time, - "refresh_only": screensaver - } - - context.get_ui().set_home_window_property('playback_json', json.dumps(playback_json)) - context.send_notification('PlaybackInit', { - 'video_id': video_id, - 'channel_id': playback_json.get('channel_id', ''), - 'status': playback_json.get('video_status', {}) - }) - xbmcplugin.setResolvedUrl(handle=context.get_handle(), succeeded=True, listitem=item) - - except YouTubeException as ex: - message = ex.get_message() - message = kodion.utils.strip_html_from_text(message) - context.get_ui().show_notification(message, time_milliseconds=15000) + if is_video and video_stream['video'].get('rtmpe', False): + message = context.localize('error.rtmpe_not_supported') + ui.show_notification(message, time_milliseconds=5000) + return False + play_suggested = settings.get_bool('youtube.suggested_videos', False) + if play_suggested and not screensaver: + utils.add_related_video_to_playlist(provider, + context, + client, + v3, + video_id) -def play_playlist(provider, context): - videos = [] + metadata = video_stream.get('meta', {}) - def _load_videos(_page_token='', _progress_dialog=None): - if _progress_dialog is None: - _progress_dialog = context.get_ui().create_progress_dialog( - context.localize('playlist.progress.updating'), - context.localize('please_wait'), background=True) - json_data = client.get_playlist_items(playlist_id, page_token=_page_token) - if not v3.handle_error(context, json_data): - return None - _progress_dialog.set_total(int(json_data.get('pageInfo', {}).get('totalResults', 0))) + title = metadata.get('video', {}).get('title', '') + video_item = VideoItem(title, video_stream['url']) - result = v3.response_to_items(provider, context, json_data, process_next_page=False) - videos.extend(result) - progress_text = '%s %d/%d' % ( - context.localize('please_wait'), len(videos), _progress_dialog.get_total()) - _progress_dialog.update(steps=len(result), text=progress_text) + incognito = params.get('incognito', False) + use_history = not is_live and not screensaver and not incognito + use_remote_history = use_history and settings.use_remote_history() + use_play_data = use_history and settings.use_local_history() - next_page_token = json_data.get('nextPageToken', '') - if next_page_token: - _load_videos(_page_token=next_page_token, _progress_dialog=_progress_dialog) + utils.update_play_info(provider, context, video_id, video_item, + video_stream, use_play_data=use_play_data) - return _progress_dialog + seek_time = 0.0 + play_count = 0 + playback_stats = video_stream.get('playback_stats') - # select order - video_id = context.get_param('video_id', '') - order = context.get_param('order', '') - if not order: - order_list = ['default', 'reverse'] - # we support shuffle only without a starting video position - if not video_id: - order_list.append('shuffle') - items = [] - for order in order_list: - items.append((context.localize('playlist.play.%s' % order), order)) - - order = context.get_ui().on_select(context.localize('playlist.play.select'), items) - if order not in order_list: - return False + if not params.get('resume'): + try: + seek_time = params.get('seek', 0.0) + except (ValueError, TypeError): + pass + + if use_play_data: + play_count = video_item.get_play_count() or 0 + + playback_json = { + "video_id": video_id, + "channel_id": metadata.get('channel', {}).get('id', ''), + "video_status": metadata.get('video', {}).get('status', {}), + "playing_file": video_item.get_uri(), + "play_count": play_count, + "use_remote_history": use_remote_history, + "use_local_history": use_play_data, + "playback_stats": playback_stats, + "seek_time": seek_time, + "refresh_only": screensaver + } + + ui.set_property('playback_json', json.dumps(playback_json)) + context.send_notification('PlaybackInit', { + 'video_id': video_id, + 'channel_id': playback_json.get('channel_id', ''), + 'status': playback_json.get('video_status', {}) + }) + return video_item + + +def play_playlist(provider, context): + videos = [] + params = context.get_params() player = context.get_video_player() player.stop() - playlist_id = context.get_param('playlist_id') + playlist_ids = params.get('playlist_ids') + if not playlist_ids: + playlist_ids = [params.get('playlist_id')] + client = provider.get_client(context) + ui = context.get_ui() + + progress_dialog = ui.create_progress_dialog( + context.localize('playlist.progress.updating'), + context.localize('please_wait'), + background=True + ) # start the loop and fill the list with video items - progress_dialog = _load_videos() + total = 0 + for playlist_id in playlist_ids: + page_token = 0 + while page_token is not None: + json_data = client.get_playlist_items(playlist_id, page_token) + if not v3.handle_error(context, json_data): + return None + + if page_token == 0: + total += int(json_data.get('pageInfo', {}) + .get('totalResults', 0)) + progress_dialog.set_total(total) + + result = v3.response_to_items(provider, + context, + json_data, + process_next_page=False) + videos.extend(result) + + progress_dialog.update( + steps=len(result), + text='{wait} {current}/{total}'.format( + wait=context.localize('please_wait'), + current=len(videos), + total=total + ) + ) + + page_token = json_data.get('nextPageToken') or None + + # select order + order = params.get('order', '') + if not order: + order_list = ['default', 'reverse', 'shuffle'] + items = [(context.localize('playlist.play.%s' % order), order) + for order in order_list] + order = ui.on_select(context.localize('playlist.play.select'), items) + if order not in order_list: + order = 'default' # reverse the list if order == 'reverse': videos = videos[::-1] elif order == 'shuffle': - # we have to shuffle the playlist by our self. The implementation of XBMC/KODI is quite weak :( + # we have to shuffle the playlist by our self. + # The implementation of XBMC/KODI is quite weak :( random.shuffle(videos) - playlist_position = 0 - # check if we have a video as starting point for the playlist - if video_id: - find_video_id = re.compile(r'video_id=(?P[^&]+)') - for video in videos: - video_id_match = find_video_id.search(video.get_uri()) - if video_id_match and video_id_match.group('video_id') == video_id: - break - playlist_position += 1 - # clear the playlist playlist = context.get_video_playlist() playlist.clear() @@ -205,9 +205,14 @@ def _load_videos(_page_token='', _progress_dialog=None): if order == 'shuffle': playlist.unshuffle() + # check if we have a video as starting point for the playlist + video_id = params.get('video_id', '') # add videos to playlist - for video in videos: + playlist_position = 0 + for idx, video in enumerate(videos): playlist.add(video) + if video_id and not playlist_position and video_id in video.get_uri(): + playlist_position = idx # we use the shuffle implementation of the playlist """ @@ -218,13 +223,12 @@ def _load_videos(_page_token='', _progress_dialog=None): if progress_dialog: progress_dialog.close() - if context.get_param('play') and context.get_handle() == -1: + if not params.get('play'): + return videos + if context.get_handle() == -1: player.play(playlist_index=playlist_position) return False - if context.get_param('play'): - return videos[playlist_position] - - return True + return videos[playlist_position] def play_channel_live(provider, context): diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 59acb5d3c..a945f673b 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -69,11 +69,11 @@ def is_logged_in(self): @staticmethod def get_dev_config(context, addon_id, dev_configs): - _dev_config = context.get_ui().get_home_window_property('configs') - context.get_ui().clear_home_window_property('configs') + _dev_config = context.get_ui().get_property('configs') + context.get_ui().clear_property('configs') dev_config = {} - if _dev_config is not None: + if _dev_config: context.log_debug('Using window property for developer keys is deprecated, instead use the youtube_registration module.') try: dev_config = json.loads(_dev_config) @@ -569,15 +569,15 @@ def _on_my_location(self, context, re_match): @RegisterProviderPath('^/play/$') def on_play(self, context, re_match): ui = context.get_ui() - listitem_path = ui.get_info_label('Container.ListItem(0).FileNameAndPath') + path = ui.get_info_label('Container.ListItem(0).FileNameAndPath') redirect = False params = context.get_params() - if 'video_id' not in params and 'playlist_id' not in params and \ - 'channel_id' not in params and 'live' not in params: - if context.is_plugin_path(listitem_path, 'play'): - video_id = find_video_id(listitem_path) + if ({'channel_id', 'live', 'playlist_id', 'playlist_ids', 'video_id'} + .isdisjoint(params.keys())): + if context.is_plugin_path(path, 'play'): + video_id = find_video_id(path) if video_id: context.set_param('video_id', video_id) params = context.get_params() @@ -586,61 +586,58 @@ def on_play(self, context, re_match): else: return False - if ui.get_home_window_property('prompt_for_subtitles') != params.get('video_id'): - ui.clear_home_window_property('prompt_for_subtitles') + video_id = params.get('video_id') + playlist_id = params.get('playlist_id') + + if ui.get_property('prompt_for_subtitles') != video_id: + ui.clear_property('prompt_for_subtitles') - if ui.get_home_window_property('audio_only') != params.get('video_id'): - ui.clear_home_window_property('audio_only') + if ui.get_property('audio_only') != video_id: + ui.clear_property('audio_only') - if ui.get_home_window_property('ask_for_quality') != params.get('video_id'): - ui.clear_home_window_property('ask_for_quality') + if ui.get_property('ask_for_quality') != video_id: + ui.clear_property('ask_for_quality') - if 'prompt_for_subtitles' in params: - prompt_subtitles = params['prompt_for_subtitles'] - del params['prompt_for_subtitles'] - if prompt_subtitles and 'video_id' in params and 'playlist_id' not in params: - # redirect to builtin after setting home window property, so playback url matches playable listitems - ui.set_home_window_property('prompt_for_subtitles', params['video_id']) + if video_id and not playlist_id: + if params.pop('prompt_for_subtitles', None): + # redirect to builtin after setting home window property, + # so playback url matches playable listitems + ui.set_property('prompt_for_subtitles', video_id) context.log_debug('Redirecting playback with subtitles') redirect = True - elif 'audio_only' in params: - audio_only = params['audio_only'] - del params['audio_only'] - if audio_only and 'video_id' in params and 'playlist_id' not in params: - # redirect to builtin after setting home window property, so playback url matches playable listitems - ui.set_home_window_property('audio_only', params['video_id']) + if params.pop('audio_only', None): + # redirect to builtin after setting home window property, + # so playback url matches playable listitems + ui.set_property('audio_only', video_id) context.log_debug('Redirecting audio only playback') redirect = True - elif 'ask_for_quality' in params: - ask_for_quality = params['ask_for_quality'] - del params['ask_for_quality'] - if ask_for_quality and 'video_id' in params and 'playlist_id' not in params: - # redirect to builtin after setting home window property, so playback url matches playable listitems - ui.set_home_window_property('ask_for_quality', params['video_id']) - context.log_debug('Redirecting audio only playback') + if params.pop('ask_for_quality', None): + # redirect to builtin after setting home window property, + # so playback url matches playable listitems + ui.set_property('ask_for_quality', video_id) + context.log_debug('Redirecting ask quality playback') redirect = True - if 'playlist_id' not in params and 'video_id' in params and (context.get_handle() == -1 or redirect): - builtin = 'PlayMedia(%s)' if context.get_handle() == -1 else 'RunPlugin(%s)' - if not redirect: + builtin = None + if context.get_handle() == -1: + builtin = 'PlayMedia({0})' context.log_debug('Redirecting playback, handle is -1') - context.execute(builtin % context.create_uri(['play'], {'video_id': params['video_id']})) - return False + elif redirect: + builtin = 'RunPlugin({0})' - if 'playlist_id' in params and (context.get_handle() != -1): - builtin = 'RunPlugin(%s)' - stream_url = context.create_uri(['play'], params) - xbmcplugin.setResolvedUrl(handle=context.get_handle(), succeeded=False, listitem=xbmcgui.ListItem(path=stream_url)) - context.execute(builtin % context.create_uri(['play'], params)) - return False - - if 'video_id' in params and 'playlist_id' not in params: + if builtin: + context.execute(builtin.format( + context.create_uri(['play'], {'video_id': video_id}) + )) + return False return yt_play.play_video(self, context) - if 'playlist_id' in params: + + if playlist_id or 'playlist_ids' in params: return yt_play.play_playlist(self, context) - if 'channel_id' in params and 'live' in params and params['live'] > 0: + + if 'channel_id' in params and params.get('live', 0) > 0: return yt_play.play_channel_live(self, context) return False From a5eef47ceac8b621e57f29f0da836c2df1e04f3e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:00:08 +1100 Subject: [PATCH 044/141] Misc tidy up - No (intended) functional changes --- .../lib/youtube_plugin/kodion/__init__.py | 11 +- .../youtube_plugin/kodion/context/__init__.py | 2 +- .../youtube_plugin/kodion/items/base_item.py | 1 - .../kodion/json_store/__init__.py | 3 +- .../kodion/json_store/json_store.py | 2 +- .../youtube_plugin/kodion/player/__init__.py | 2 +- .../youtube_plugin/kodion/plugin/__init__.py | 2 +- .../kodion/settings/__init__.py | 2 +- .../kodion/settings/abstract_settings.py | 5 +- .../lib/youtube_plugin/kodion/ui/__init__.py | 2 +- .../youtube_plugin/kodion/utils/__init__.py | 2 +- .../lib/youtube_plugin/youtube/__init__.py | 3 +- .../youtube_plugin/youtube/client/__init__.py | 3 +- .../youtube/helper/ratebypass/__init__.py | 3 +- .../youtube/helper/resource_manager.py | 20 +-- .../youtube/helper/signature/__init__.py | 3 +- .../youtube/helper/signature/cipher.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 114 +++++++++++++----- .../youtube/helper/video_info.py | 2 +- resources/settings.xml | 2 +- 20 files changed, 126 insertions(+), 60 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/__init__.py b/resources/lib/youtube_plugin/kodion/__init__.py index 869364a9b..9df0afa43 100644 --- a/resources/lib/youtube_plugin/kodion/__init__.py +++ b/resources/lib/youtube_plugin/kodion/__init__.py @@ -22,6 +22,15 @@ from . import logger -__all__ = ['KodionException', 'RegisterProviderPath', 'AbstractProvider', 'Context', 'utils', 'json_store', 'logger'] + +__all__ = ( + 'AbstractProvider', + 'Context', + 'KodionException', + 'RegisterProviderPath', + 'json_store', + 'logger', + 'utils', +) __version__ = '1.5.4' diff --git a/resources/lib/youtube_plugin/kodion/context/__init__.py b/resources/lib/youtube_plugin/kodion/context/__init__.py index 9b2b1d889..89d69a6ec 100644 --- a/resources/lib/youtube_plugin/kodion/context/__init__.py +++ b/resources/lib/youtube_plugin/kodion/context/__init__.py @@ -10,4 +10,4 @@ from .xbmc.xbmc_context import XbmcContext as Context -__all__ = ('Context', ) \ No newline at end of file +__all__ = ('Context',) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index abd5adb8e..832d0fa6b 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -8,7 +8,6 @@ See LICENSES/GPL-2.0-only for more information. """ - import hashlib import datetime diff --git a/resources/lib/youtube_plugin/kodion/json_store/__init__.py b/resources/lib/youtube_plugin/kodion/json_store/__init__.py index 278ded74a..1b8044317 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/__init__.py +++ b/resources/lib/youtube_plugin/kodion/json_store/__init__.py @@ -11,4 +11,5 @@ from .api_keys import APIKeyStore from .login_tokens import LoginTokenStore -__all__ = ['JSONStore', 'APIKeyStore', 'LoginTokenStore'] + +__all__ = ('APIKeyStore', 'JSONStore', 'LoginTokenStore',) diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index e1e0b196f..577f431d1 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -41,7 +41,7 @@ def set_defaults(self, reset=False): def save(self, data): if data == self._data: log_debug('JSONStore.save |{filename}| data unchanged'.format( - filename=self.filename + filename=self.filename )) return log_debug('JSONStore.save |{filename}|'.format( diff --git a/resources/lib/youtube_plugin/kodion/player/__init__.py b/resources/lib/youtube_plugin/kodion/player/__init__.py index 705b491c8..e30f5d82f 100644 --- a/resources/lib/youtube_plugin/kodion/player/__init__.py +++ b/resources/lib/youtube_plugin/kodion/player/__init__.py @@ -11,4 +11,4 @@ from .xbmc.xbmc_playlist import XbmcPlaylist as Playlist -__all__ = ('Player', 'Playlist', ) \ No newline at end of file +__all__ = ('Player', 'Playlist',) diff --git a/resources/lib/youtube_plugin/kodion/plugin/__init__.py b/resources/lib/youtube_plugin/kodion/plugin/__init__.py index 37fe21cbe..19ec2ef97 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/__init__.py +++ b/resources/lib/youtube_plugin/kodion/plugin/__init__.py @@ -10,4 +10,4 @@ from .xbmc.xbmc_runner import XbmcRunner as Runner -__all__ = ('Runner', ) +__all__ = ('Runner',) diff --git a/resources/lib/youtube_plugin/kodion/settings/__init__.py b/resources/lib/youtube_plugin/kodion/settings/__init__.py index 61a1c1394..06ed1a6ba 100644 --- a/resources/lib/youtube_plugin/kodion/settings/__init__.py +++ b/resources/lib/youtube_plugin/kodion/settings/__init__.py @@ -10,4 +10,4 @@ from .xbmc.xbmc_plugin_settings import XbmcPluginSettings as Settings -__all__ = ('Settings', ) +__all__ = ('Settings',) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 5a461224f..f2b6cd36c 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -288,9 +288,8 @@ def get_mpd_video_qualities(self): return [] selected = self.get_int(SETTINGS.MPD_QUALITY_SELECTION, 4) return [quality - for key, quality in sorted( - self._QUALITY_SELECTIONS.items(), reverse=True - ) + for key, quality in sorted(self._QUALITY_SELECTIONS.items(), + reverse=True) if selected >= key] def stream_features(self): diff --git a/resources/lib/youtube_plugin/kodion/ui/__init__.py b/resources/lib/youtube_plugin/kodion/ui/__init__.py index 20669bc59..b173ebd14 100644 --- a/resources/lib/youtube_plugin/kodion/ui/__init__.py +++ b/resources/lib/youtube_plugin/kodion/ui/__init__.py @@ -10,4 +10,4 @@ from .xbmc.xbmc_context_ui import XbmcContextUI as ContextUI -__all__ = ('ContextUI', ) +__all__ = ('ContextUI',) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index fe5c07d99..ff5c0cc44 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -56,5 +56,5 @@ 'SystemVersion', 'WatchLaterList', 'YouTubeMonitor', - 'YouTubePlayer' + 'YouTubePlayer', ) diff --git a/resources/lib/youtube_plugin/youtube/__init__.py b/resources/lib/youtube_plugin/youtube/__init__.py index 76a20e1cf..597a18f32 100644 --- a/resources/lib/youtube_plugin/youtube/__init__.py +++ b/resources/lib/youtube_plugin/youtube/__init__.py @@ -10,4 +10,5 @@ from .provider import Provider -__all__ = ['Provider'] + +__all__ = ('Provider',) diff --git a/resources/lib/youtube_plugin/youtube/client/__init__.py b/resources/lib/youtube_plugin/youtube/client/__init__.py index 804d1f7ab..a97c2a226 100644 --- a/resources/lib/youtube_plugin/youtube/client/__init__.py +++ b/resources/lib/youtube_plugin/youtube/client/__init__.py @@ -10,4 +10,5 @@ from .youtube import YouTube -__all__ = ['YouTube'] + +__all__ = ('YouTube',) diff --git a/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py b/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py index ecbe3188e..8afb79d19 100644 --- a/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py @@ -9,4 +9,5 @@ from ....youtube.helper.ratebypass import ratebypass -__all__ = ['ratebypass'] + +__all__ = ('ratebypass',) diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 5f74d5824..21896f83a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -9,13 +9,13 @@ """ from ..youtube_exceptions import YouTubeException -from ...kodion.utils import FunctionCache, DataCache, strip_html_from_text +from ...kodion.utils import strip_html_from_text class ResourceManager(object): - def __init__(self, context, youtube_client): + def __init__(self, context, client): self._context = context - self._youtube_client = youtube_client + self._client = client self._channel_data = {} self._video_data = {} self._playlist_data = {} @@ -41,7 +41,7 @@ def _update_channels(self, channel_ids): for channel_id in channel_ids: if channel_id == 'mine': - json_data = function_cache.get(FunctionCache.ONE_DAY, self._youtube_client.get_channel_by_username, channel_id) + json_data = function_cache.get(function_cache.ONE_DAY, self._client.get_channel_by_username, channel_id) items = json_data.get('items', [{'id': 'mine'}]) try: @@ -58,7 +58,7 @@ def _update_channels(self, channel_ids): channel_ids = updated_channel_ids data_cache = self._context.get_data_cache() - channel_data = data_cache.get_items(DataCache.ONE_MONTH, channel_ids) + channel_data = data_cache.get_items(data_cache.ONE_MONTH, channel_ids) channel_ids = set(channel_ids) channel_ids_cached = set(channel_data) @@ -72,7 +72,7 @@ def _update_channels(self, channel_ids): if channel_ids_to_update: self._context.log_debug('No data for channels |%s| cached' % ', '.join(channel_ids_to_update)) json_data = [ - self._youtube_client.get_channels(list_of_50) + self._client.get_channels(list_of_50) for list_of_50 in self._list_batch(channel_ids_to_update, n=50) ] channel_data = { @@ -92,7 +92,7 @@ def _update_channels(self, channel_ids): def _update_videos(self, video_ids, live_details=False, suppress_errors=False): json_data = None data_cache = self._context.get_data_cache() - video_data = data_cache.get_items(DataCache.ONE_MONTH, video_ids) + video_data = data_cache.get_items(data_cache.ONE_MONTH, video_ids) video_ids = set(video_ids) video_ids_cached = set(video_data) @@ -105,7 +105,7 @@ def _update_videos(self, video_ids, live_details=False, suppress_errors=False): if video_ids_to_update: self._context.log_debug('No data for videos |%s| cached' % ', '.join(video_ids_to_update)) - json_data = self._youtube_client.get_videos(video_ids_to_update, live_details) + json_data = self._client.get_videos(video_ids_to_update, live_details) video_data = { yt_item['id']: yt_item for yt_item in json_data.get('items', []) @@ -143,7 +143,7 @@ def get_videos(self, video_ids, live_details=False, suppress_errors=False): def _update_playlists(self, playlists_ids): json_data = None data_cache = self._context.get_data_cache() - playlist_data = data_cache.get_items(DataCache.ONE_MONTH, playlists_ids) + playlist_data = data_cache.get_items(data_cache.ONE_MONTH, playlists_ids) playlists_ids = set(playlists_ids) playlists_ids_cached = set(playlist_data) @@ -156,7 +156,7 @@ def _update_playlists(self, playlists_ids): if playlist_ids_to_update: self._context.log_debug('No data for playlists |%s| cached' % ', '.join(playlist_ids_to_update)) - json_data = self._youtube_client.get_playlists(playlist_ids_to_update) + json_data = self._client.get_playlists(playlist_ids_to_update) playlist_data = { yt_item['id']: yt_item for yt_item in json_data.get('items', []) diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py index a1da71f71..886aaaa2e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py @@ -10,4 +10,5 @@ from ....youtube.helper.signature.cipher import Cipher -__all__ = ['Cipher'] + +__all__ = ('Cipher',) diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py index ae89edf24..15c7d4028 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py @@ -121,7 +121,7 @@ def _find_signature_function_name(javascript): r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P[a-zA-Z0-9$]+)\(', r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P[a-zA-Z0-9$]+)\(', r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P[a-zA-Z0-9$]+)\(', - r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P[a-zA-Z0-9$]+)\(' + r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P[a-zA-Z0-9$]+)\(' ) for pattern in match_patterns: diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 90b19f3c5..0680b342e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -146,13 +146,19 @@ def update_channel_infos(provider, context, channel_id_dict, logged_in = provider.is_logged_in() path = context.get_path() if path == '/subscriptions/list/': - filter_string = context.get_settings().get_string('youtube.filter.my_subscriptions_filtered.list', '') + filter_string = context.get_settings().get_string( + 'youtube.filter.my_subscriptions_filtered.list', '' + ) filter_string = filter_string.replace(', ', ',') filter_list = filter_string.split(',') filter_list = [x.lower() for x in filter_list] thumb_size = context.get_settings().use_thumbnail_size - banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', 'bannerTvImageUrl'] + banners = [ + 'bannerTvMediumImageUrl', + 'bannerTvLowImageUrl', + 'bannerTvImageUrl' + ] for channel_id, yt_item in data.items(): channel_item = channel_id_dict[channel_id] @@ -173,17 +179,25 @@ def update_channel_infos(provider, context, channel_id_dict, subscription_id = subscription_id_dict.get(channel_id, '') if subscription_id: channel_item.set_channel_subscription_id(subscription_id) - yt_context_menu.append_unsubscribe_from_channel(context_menu, context, subscription_id) + yt_context_menu.append_unsubscribe_from_channel( + context_menu, context, subscription_id + ) # -- subscribe to the channel if logged_in and path != '/subscriptions/list/': - yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id) + yt_context_menu.append_subscribe_to_channel( + context_menu, context, channel_id + ) if path == '/subscriptions/list/': channel = title.lower().replace(',', '') if channel in filter_list: - yt_context_menu.append_remove_my_subscriptions_filter(context_menu, context, title) + yt_context_menu.append_remove_my_subscriptions_filter( + context_menu, context, title + ) else: - yt_context_menu.append_add_my_subscriptions_filter(context_menu, context, title) + yt_context_menu.append_add_my_subscriptions_filter( + context_menu, context, title + ) channel_item.set_context_menu(context_menu) fanart_images = yt_item.get('brandingSettings', {}).get('image', {}) @@ -239,32 +253,47 @@ def update_playlist_infos(provider, context, playlist_id_dict, channel_name = snippet.get('channelTitle', '') context_menu = [] # play all videos of the playlist - yt_context_menu.append_play_all_from_playlist(context_menu, context, playlist_id) + yt_context_menu.append_play_all_from_playlist( + context_menu, context, playlist_id + ) if logged_in: if channel_id != 'mine': # subscribe to the channel via the playlist item - yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id, - channel_name) + yt_context_menu.append_subscribe_to_channel( + context_menu, context, channel_id, channel_name + ) else: # remove my playlist - yt_context_menu.append_delete_playlist(context_menu, context, playlist_id, title) + yt_context_menu.append_delete_playlist( + context_menu, context, playlist_id, title + ) # rename playlist - yt_context_menu.append_rename_playlist(context_menu, context, playlist_id, title) + yt_context_menu.append_rename_playlist( + context_menu, context, playlist_id, title + ) # remove as my custom watch later playlist if playlist_id == custom_watch_later_id: - yt_context_menu.append_remove_as_watchlater(context_menu, context, playlist_id, title) + yt_context_menu.append_remove_as_watchlater( + context_menu, context, playlist_id, title + ) # set as my custom watch later playlist else: - yt_context_menu.append_set_as_watchlater(context_menu, context, playlist_id, title) + yt_context_menu.append_set_as_watchlater( + context_menu, context, playlist_id, title + ) # remove as custom history playlist if playlist_id == custom_history_id: - yt_context_menu.append_remove_as_history(context_menu, context, playlist_id, title) + yt_context_menu.append_remove_as_history( + context_menu, context, playlist_id, title + ) # set as custom history playlist else: - yt_context_menu.append_set_as_history(context_menu, context, playlist_id, title) + yt_context_menu.append_set_as_history( + context_menu, context, playlist_id, title + ) if context_menu: playlist_item.set_context_menu(context_menu) @@ -478,8 +507,12 @@ def update_video_infos(provider, context, video_id_dict, replace_context_menu = True playlist_id = some_playlist_match.group('playlist_id') - yt_context_menu.append_play_all_from_playlist(context_menu, context, playlist_id, video_id) - yt_context_menu.append_play_all_from_playlist(context_menu, context, playlist_id) + yt_context_menu.append_play_all_from_playlist( + context_menu, context, playlist_id, video_id + ) + yt_context_menu.append_play_all_from_playlist( + context_menu, context, playlist_id + ) # 'play with...' (external player) if alternate_player: @@ -517,37 +550,58 @@ def update_video_infos(provider, context, video_id_dict, if (channel_id and channel_name and utils.create_path('channel', channel_id) != path): video_item.set_channel_id(channel_id) - yt_context_menu.append_go_to_channel(context_menu, context, channel_id, channel_name) + yt_context_menu.append_go_to_channel( + context_menu, context, channel_id, channel_name + ) if logged_in: # subscribe to the channel of the video video_item.set_subscription_id(channel_id) - yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id, channel_name) + yt_context_menu.append_subscribe_to_channel( + context_menu, context, channel_id, channel_name + ) if not video_item.live and play_data: if not play_data.get('play_count'): - yt_context_menu.append_mark_watched(context_menu, context, video_id) + yt_context_menu.append_mark_watched( + context_menu, context, video_id + ) else: - yt_context_menu.append_mark_unwatched(context_menu, context, video_id) + yt_context_menu.append_mark_unwatched( + context_menu, context, video_id + ) - if play_data.get('played_percent', 0) > 0 or play_data.get('played_time', 0) > 0: - yt_context_menu.append_reset_resume_point(context_menu, context, video_id) + if (play_data.get('played_percent', 0) > 0 + or play_data.get('played_time', 0) > 0): + yt_context_menu.append_reset_resume_point( + context_menu, context, video_id + ) # more... refresh_container = (path.startswith('/channel/mine/playlist/LL') or path == '/special/disliked_videos/') - yt_context_menu.append_more_for_video(context_menu, context, video_id, - is_logged_in=logged_in, - refresh_container=refresh_container) + yt_context_menu.append_more_for_video( + context_menu, context, video_id, + is_logged_in=logged_in, + refresh_container=refresh_container + ) if not video_item.live: - yt_context_menu.append_play_with_subtitles(context_menu, context, video_id) - yt_context_menu.append_play_audio_only(context_menu, context, video_id) + yt_context_menu.append_play_with_subtitles( + context_menu, context, video_id + ) + yt_context_menu.append_play_audio_only( + context_menu, context, video_id + ) - yt_context_menu.append_play_ask_for_quality(context_menu, context, video_id) + yt_context_menu.append_play_ask_for_quality( + context_menu, context, video_id + ) if context_menu: - video_item.set_context_menu(context_menu, replace=replace_context_menu) + video_item.set_context_menu( + context_menu, replace=replace_context_menu + ) def update_play_info(provider, context, video_id, video_item, video_stream, diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 28637160b..193245d9f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -837,7 +837,7 @@ def _load_hls_manifest(self, url, live_type=None, meta_info=None, # Capture the URL of a .m3u8 playlist and the itag value from that URL. re_playlist_data = re.compile( r'#EXT-X-STREAM-INF[^#]+' - r'(?Phttp[^\s]+/itag/(?P\d+)[^\s]+)' + r'(?Phttp\S+/itag/(?P\d+)\S+)' ) for match in re_playlist_data.finditer(result.text): playlist_url = match.group('url') diff --git a/resources/settings.xml b/resources/settings.xml index e6ee7a6e6..dfa126b51 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -317,7 +317,7 @@ 2 - + From 4a46dcdf25e270a80f71a3bff6181e3cf8020aaa Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:01:41 +1100 Subject: [PATCH 045/141] Fix missing utils.to_str in __all__ --- resources/lib/youtube_plugin/kodion/utils/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index ff5c0cc44..5b9e8df41 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -19,6 +19,7 @@ make_dirs, select_stream, strip_html_from_text, + to_str, to_unicode, to_utf8, ) @@ -45,6 +46,7 @@ 'make_dirs', 'select_stream', 'strip_html_from_text', + 'to_str', 'to_unicode', 'to_utf8', 'AccessManager', From 20d7d33231833d4a2fcfde78c08cbfdede34f9cd Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:09:35 +1100 Subject: [PATCH 046/141] Minor improvement to update_video_infos - Fix watch later context menu item on items already in WL - Update regexps - Minimise work in loop --- .../youtube_plugin/youtube/helper/utils.py | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 0680b342e..20f608d65 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -21,29 +21,34 @@ ISHelper = None +__RE_HISTORY_MATCH = re.compile(r'^/special/watch_history_tv/$') + +__RE_PLAYLIST_MATCH = re.compile( + r'^(/channel/(?P[^/]+))/playlist/(?P[^/]+)/$' +) + __RE_SEASON_EPISODE_MATCHES__ = [ re.compile(r'Part (?P\d+)'), re.compile(r'#(?P\d+)'), - re.compile(r'Ep.[^\w]?(?P\d+)'), - re.compile(r'\[(?P\d+)\]'), + re.compile(r'Ep.\W?(?P\d+)'), + re.compile(r'\[(?P\d+)]'), re.compile(r'S(?P\d+)E(?P\d+)'), re.compile(r'Season (?P\d+)(.+)Episode (?P\d+)'), re.compile(r'Episode (?P\d+)'), ] +__RE_URL = re.compile(r'(https?://\S+)') -def extract_urls(text): - result = [] - re_url = re.compile(r'(https?://[^\s]+)') - matches = re_url.findall(text) - result = matches or result - - return result +def extract_urls(text): + return __RE_URL.findall(text) def get_thumb_timestamp(minutes=15): - return str(time.mktime(time.gmtime(minutes * 60 * (round(time.time() / (minutes * 60)))))) + seconds = minutes * 60 + return str(time.mktime(time.gmtime( + seconds * (round(time.time() / seconds)) + ))) def make_comment_item(context, snippet, uri, total_replies=0): @@ -335,6 +340,7 @@ def update_video_infos(provider, context, video_id_dict, thumb_size = settings.use_thumbnail_size() thumb_stamp = get_thumb_timestamp() ui = context.get_ui() + watch_later_playlist_id = context.get_access_manager().get_watch_later_id() for video_id, yt_item in data.items(): video_item = video_id_dict[video_id] @@ -502,10 +508,12 @@ def update_video_infos(provider, context, video_id_dict, /channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/ /playlist/[PLAYLIST_ID]/ """ - some_playlist_match = re.match(r'^(/channel/([^/]+))/playlist/(?P[^/]+)/$', path) - if some_playlist_match: + playlist_match = __RE_PLAYLIST_MATCH.match(path) + playlist_id = playlist_channel_id = '' + if playlist_match: replace_context_menu = True - playlist_id = some_playlist_match.group('playlist_id') + playlist_id = playlist_match.group('playlist_id') + playlist_channel_id = playlist_match.group('channel_id') yt_context_menu.append_play_all_from_playlist( context_menu, context, playlist_id, video_id @@ -520,31 +528,34 @@ def update_video_infos(provider, context, video_id_dict, if logged_in: # add 'Watch Later' only if we are not in my 'Watch Later' list - watch_later_playlist_id = context.get_access_manager().get_watch_later_id() - if watch_later_playlist_id: - yt_context_menu.append_watch_later(context_menu, context, watch_later_playlist_id, video_id) + if (watch_later_playlist_id + and watch_later_playlist_id != playlist_id): + yt_context_menu.append_watch_later( + context_menu, context, watch_later_playlist_id, video_id + ) # provide 'remove' for videos in my playlists - if video_id in playlist_item_id_dict: - playlist_match = re.match('^/channel/mine/playlist/(?P[^/]+)/$', path) - if playlist_match: - playlist_id = playlist_match.group('playlist_id') - # we support all playlist except 'Watch History' - if playlist_id and playlist_id != 'HL' and playlist_id.strip().lower() != 'wl': - playlist_item_id = playlist_item_id_dict[video_id] - video_item.set_playlist_id(playlist_id) - video_item.set_playlist_item_id(playlist_item_id) - context_menu.append((context.localize('remove'), - 'RunPlugin(%s)' % context.create_uri( - ['playlist', 'remove', 'video'], - {'playlist_id': playlist_id, - 'video_id': playlist_item_id, - 'video_name': video_item.get_name()} - ))) - - is_history = re.match('^/special/watch_history_tv/$', path) - if is_history: - yt_context_menu.append_clear_watch_history(context_menu, context) + # we support all playlist except 'Watch History' + if (video_id in playlist_item_id_dict and playlist_id + and playlist_channel_id == 'mine' + and playlist_id.strip().lower() not in ('hl', 'wl')): + playlist_item_id = playlist_item_id_dict[video_id] + video_item.set_playlist_id(playlist_id) + video_item.set_playlist_item_id(playlist_item_id) + context_menu.append(( + context.localize('remove'), + 'RunPlugin(%s)' % context.create_uri( + ['playlist', 'remove', 'video'], + {'playlist_id': playlist_id, + 'video_id': playlist_item_id, + 'video_name': video_item.get_name()} + ) + )) + + if __RE_HISTORY_MATCH.match(path): + yt_context_menu.append_clear_watch_history( + context_menu, context + ) # got to [CHANNEL], only if we are not directly in the channel provide a jump to the channel if (channel_id and channel_name and From cb38bb5ab5716c4a549b02ef3895cec6a292314c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 28 Nov 2023 18:08:03 +1100 Subject: [PATCH 047/141] Update video stats display - Uses same formatting style as comment details --- .../kodion/context/xbmc/xbmc_context.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 48 ++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 66fdf6a94..873c42f9a 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -167,7 +167,7 @@ class XbmcContext(AbstractContext): 'sign.twice.text': 30547, 'sign.twice.title': 30546, 'stats.commentCount': 30732, - 'stats.favoriteCount': 30100, + # 'stats.favoriteCount': 30100, 'stats.likeCount': 30733, 'stats.viewCount': 30767, 'stream.alternate': 30747, diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 20f608d65..27a2db505 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -20,6 +20,12 @@ except ImportError: ISHelper = None +__COLOR_MAP = { + 'commentCount': 'cyan', + 'favoriteCount': 'gold', + 'likeCount': 'lime', + 'viewCount': 'lightblue', +} __RE_HISTORY_MATCH = re.compile(r'^/special/watch_history_tv/$') @@ -63,8 +69,9 @@ def make_comment_item(context, snippet, uri, total_replies=0): like_count = snippet['likeCount'] if like_count: like_count = utils.friendly_number(like_count) - label_likes = ui.color('lime', ui.bold(like_count)) - plot_likes = ui.color('lime', ui.bold(' '.join(( + color = __COLOR_MAP['likeCount'] + label_likes = ui.color(color, ui.bold(like_count)) + plot_likes = ui.color(color, ui.bold(' '.join(( like_count, context.localize('video.comments.likes') )))) label_props.append(label_likes) @@ -72,8 +79,9 @@ def make_comment_item(context, snippet, uri, total_replies=0): if total_replies: total_replies = utils.friendly_number(total_replies) - label_replies = ui.color('cyan', ui.bold(total_replies)) - plot_replies = ui.color('cyan', ui.bold(' '.join(( + color = __COLOR_MAP['commentCount'] + label_replies = ui.color(color, ui.bold(total_replies)) + plot_replies = ui.color(color, ui.bold(' '.join(( total_replies, context.localize('video.comments.replies') )))) label_props.append(label_replies) @@ -405,30 +413,36 @@ def update_video_infos(provider, context, video_id_dict, ) ) - # update and set the title - title = video_item.get_title() or snippet['title'] or '' - if video_item.upcoming: - title = ui.italic(title) - video_item.set_title(title) - + label_stats = [] stats = [] if 'statistics' in yt_item: for stat, value in yt_item['statistics'].items(): label = context.LOCAL_MAP.get('stats.' + stat) if label: - stats.append('{value} {name}'.format( - name=context.localize(label).lower(), - value=utils.friendly_number(value) - )) - stats = ', '.join(stats) + color = __COLOR_MAP.get(stat, 'white') + value = utils.friendly_number(value) + label_stats.append(ui.color(color, value)) + stats.append(ui.color(color, ui.bold(' '.join(( + value, context.localize(label) + ))))) + label_stats = '|'.join(label_stats) + stats = '|'.join(stats) # Used for label2, but is poorly supported in skins video_details = ' | '.join((detail for detail in ( - ui.light(stats) if stats else '', + stats if stats else '', ui.italic(start_at) if start_at else '', ) if detail)) video_item.set_short_details(video_details) + # update and set the title + title = video_item.get_title() or snippet['title'] or '' + if video_item.upcoming: + title = ui.italic(title) + if label_stats: + title = '{0} ({1})'.format(title, label_stats) + video_item.set_title(title) + """ This is experimental. We try to get the most information out of the title of a video. This is not based on any language. In some cases this won't work at all. @@ -452,7 +466,7 @@ def update_video_infos(provider, context, video_id_dict, if show_details: description = ''.join(( ui.bold(channel_name, cr_after=2) if channel_name else '', - ui.light(stats, cr_after=1) if stats else '', + ui.new_line(stats, cr_after=1) if stats else '', ui.italic(start_at, cr_after=1) if start_at else '', ui.new_line() if stats or start_at else '', description, From 1402511c5eb15bc15e83f435b421f026088f57be Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 28 Nov 2023 23:34:06 +1100 Subject: [PATCH 048/141] Update subtitles to use BaseRequestClass --- .../kodion/constants/const_settings.py | 1 + .../kodion/settings/abstract_settings.py | 3 + .../youtube/helper/subtitles.py | 134 +++++++++++------- 3 files changed, 84 insertions(+), 54 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 25a61cc0a..47dfeb3a0 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -19,6 +19,7 @@ SUBTITLE_LANGUAGE = 'kodion.subtitle.languages.num' # (int) SUBTITLE_DOWNLOAD = 'kodion.subtitle.download' # (bool) SETUP_WIZARD = 'kodion.setup_wizard' # (bool) +LANGUAGE = 'youtube.language' # (str) LOCATION = 'youtube.location' # (str) LOCATION_RADIUS = 'youtube.location.radius' # (int) PLAY_COUNT_MIN_PERCENT = 'kodion.play_count.percent' # (int) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index f2b6cd36c..79a32cd6b 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -316,3 +316,6 @@ def client_selection(self): def show_detailed_description(self): return self.get_bool(SETTINGS.DETAILED_DESCRIPTION, True) + + def get_language(self): + return self.get_string(SETTINGS.LANGUAGE, 'en_US').replace('_', '-') diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index af604d8d6..bcf7dd4d4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -10,7 +10,8 @@ from urllib.parse import parse_qs, urlsplit, urlunsplit, urlencode, urljoin import xbmcvfs -import requests + +from ...kodion.network import BaseRequestsClass from ...kodion.utils import make_dirs @@ -25,12 +26,13 @@ class Subtitles(object): SRT_FILE = ''.join([BASE_PATH, '%s.%s.srt']) def __init__(self, context, video_id, captions, headers=None): - self.context = context - self._verify = context.get_settings().verify_ssl() self.video_id = video_id - self.language = (context.get_settings() - .get_string('youtube.language', 'en_US') - .replace('_', '-')) + self.context = context + + settings = context.get_settings() + self.language = settings.get_language() + self.pre_download = settings.subtitle_download() + self.subtitle_languages = settings.subtitle_languages() if not headers and 'headers' in captions: headers = captions['headers'] @@ -66,11 +68,8 @@ def __init__(self, context, video_id, captions, headers=None): 'defaultTranslationSourceTrackIndices', [None] )[0] - if default_caption is None: - default_caption = ( - default_audio.get('hasDefaultTrack') - and default_audio.get('defaultCaptionTrackIndex') - ) + if default_caption is None and default_audio.get('hasDefaultTrack'): + default_caption = default_audio.get('defaultCaptionTrackIndex') if default_caption is None: try: @@ -92,25 +91,21 @@ def __init__(self, context, video_id, captions, headers=None): def srt_filename(self, sub_language): return self.SRT_FILE % (self.video_id, sub_language) - def _write_file(self, _file, contents): + def _write_file(self, filepath, contents): if not make_dirs(self.BASE_PATH): self.context.log_debug('Failed to create directories: %s' % self.BASE_PATH) return False - self.context.log_debug('Writing subtitle file: %s' % _file) + self.context.log_debug('Writing subtitle file: %s' % filepath) + try: - f = xbmcvfs.File(_file, 'w') - f.write(contents) - f.close() - return True - except: - self.context.log_debug('File write failed for: %s' % _file) + with xbmcvfs.File(filepath, 'w') as srt_file: + success = srt_file.write(contents) + except (IOError, OSError): + self.context.log_debug('File write failed for: %s' % filepath) return False + return success def _unescape(self, text): - try: - text = text.decode('utf8', 'ignore') - except: - self.context.log_debug('Subtitle unescape: failed to decode utf-8') try: text = unescape(text) except: @@ -127,7 +122,7 @@ def get_subtitles(self): if self.prompt_override: languages = self.LANG_PROMPT else: - languages = self.context.get_settings().subtitle_languages() + languages = self.subtitle_languages self.context.log_debug('Subtitle get_subtitles: for setting |%s|' % str(languages)) if languages == self.LANG_NONE: return [] @@ -154,30 +149,60 @@ def get_subtitles(self): self.context.log_debug('Unknown language_enum: %s for subtitles' % str(languages)) return [] - def _get_all(self): + def _get_all(self, download=False): list_of_subs = [] - for language in self.translation_langs: - list_of_subs.extend(self._get(language=language.get('languageCode'))) + for track in self.caption_tracks: + list_of_subs.extend(self._get(track.get('languageCode'), + self._get_language_name(track), + download=download)) + for track in self.translation_langs: + list_of_subs.extend(self._get(track.get('languageCode'), + self._get_language_name(track), + download=download)) return list(set(list_of_subs)) def _prompt(self): - tracks = [(track.get('languageCode'), self._get_language_name(track)) for track in self.caption_tracks] - translations = [(track.get('languageCode'), self._get_language_name(track)) for track in self.translation_langs] - languages = tracks + translations - if languages: - choice = self.context.get_ui().on_select(self.context.localize('subtitles.language'), [language for _, language in languages]) - if choice != -1: - return self._get(lang_code=languages[choice][0], language=languages[choice][1]) - self.context.log_debug('Subtitle selection cancelled') - return [] + captions = [(track.get('languageCode'), + self._get_language_name(track)) + for track in self.caption_tracks] + translations = [(track.get('languageCode'), + self._get_language_name(track)) + for track in self.translation_langs] + num_captions = len(captions) + num_translations = len(translations) + num_total = num_captions + num_translations + + if num_total: + choice = self.context.get_ui().on_select( + self.context.localize('subtitles.language'), + [name for _, name in captions] + + [name + ' *' for _, name in translations] + ) + if choice == -1: + self.context.log_debug('Subtitle selection cancelled') + return [] + + subtitle = None + if 0 <= choice < num_captions: + choice = captions[choice] + subtitle = self._get(lang_code=choice[0], language=choice[1]) + elif num_captions <= choice < num_total: + choice = translations[choice - num_captions] + subtitle = self._get(lang_code=choice[0], language=choice[1]) + + if subtitle: + return subtitle self.context.log_debug('No subtitles found for prompt') return [] - def _get(self, lang_code='en', language=None, no_asr=False): - fname = self.srt_filename(lang_code) - if xbmcvfs.exists(fname): - self.context.log_debug('Subtitle exists for: %s, filename: %s' % (lang_code, fname)) - return [fname] + def _get(self, lang_code='en', language=None, no_asr=False, download=None): + filename = self.srt_filename(lang_code) + if xbmcvfs.exists(filename): + self.context.log_debug('Subtitle exists for: %s, filename: %s' % (lang_code, filename)) + return [filename] + + if download is None: + download = self.pre_download caption_track = None asr_track = None @@ -229,21 +254,27 @@ def _get(self, lang_code='en', language=None, no_asr=False): if subtitle_url: self.context.log_debug('Subtitle url: %s' % subtitle_url) - if not self.context.get_settings().subtitle_download(): + if not download: return [subtitle_url] - result_auto = requests.get(subtitle_url, headers=self.headers, - verify=self._verify, allow_redirects=True) - if result_auto.text: + response = BaseRequestsClass().request(subtitle_url, + headers=self.headers) + if response.text: self.context.log_debug('Subtitle found for: %s' % lang_code) - self._write_file(fname, bytearray(self._unescape(result_auto.text), encoding='utf8', errors='ignore')) - return [fname] + self._write_file(filename, + bytearray(self._unescape(response.text), + encoding='utf8', + errors='ignore')) + return [filename] + self.context.log_debug('Failed to retrieve subtitles for: %s' % lang_code) return [] + self.context.log_debug('No subtitles found for: %s' % lang_code) return [] - def _get_language_name(self, track): + @staticmethod + def _get_language_name(track): key = 'languageName' if 'languageName' in track else 'name' lang_name = track.get(key, {}).get('simpleText') if not lang_name: @@ -252,14 +283,9 @@ def _get_language_name(self, track): lang_name = track_name[0].get('text') if lang_name: - return self._recode_language_name(lang_name) - + return lang_name return None - @staticmethod - def _recode_language_name(language_name): - return language_name - @staticmethod def _set_query_param(url, *pairs): if not url or not pairs: From 929fefc8170fb03810a0cd47210b9ab15342a72a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:34:07 +1100 Subject: [PATCH 049/141] Update url_resolver to use BaseRequestClass - Update user-agent - Use retry and error handling functionality of BaseRequestClass to fix #250 - Simplify/update resolver logic for new URLs and meta content - Needs testing for regressions --- .../youtube/helper/url_resolver.py | 297 +++++++++--------- .../youtube/helper/yt_specials.py | 2 - 2 files changed, 146 insertions(+), 153 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index ffca68a24..d82a1db84 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -9,20 +9,39 @@ """ import re -from urllib.parse import parse_qsl -from urllib.parse import urlparse -from urllib.parse import parse_qs -from urllib.parse import urlunsplit -from urllib.parse import urlencode +from urllib.parse import parse_qsl, urlencode, urlparse + +from ...kodion.network import BaseRequestsClass + + +class AbstractResolver(BaseRequestsClass): + _HEADERS = { + 'Cache-Control': 'max-age=0', + 'Accept': ('text/html,' + 'application/xhtml+xml,' + 'application/xml;q=0.9,' + 'image/webp,' + '*/*;q=0.8'), + # Desktop user agent + 'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/119.0.0.0 Safari/537.36'), + # Mobile user agent - for testing m.youtube.com redirect + # 'User-Agent': ('Mozilla/5.0 (Linux; Android 10; SM-G981B)' + # ' AppleWebKit/537.36 (KHTML, like Gecko)' + # ' Chrome/80.0.3987.162 Mobile Safari/537.36'), + # Old desktop user agent - for testing /supported_browsers redirect + # 'User-Agent': ('Mozilla/5.0 (Windows NT 6.1; WOW64)' + # ' AppleWebKit/537.36 (KHTML, like Gecko)' + # ' Chrome/41.0.2272.118 Safari/537.36'), + 'DNT': '1', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6' + } -from ...kodion.utils import FunctionCache -from ...kodion import Context as _Context -import requests - - -class AbstractResolver(object): - def __init__(self): - self._verify = _Context(plugin_id='plugin.video.youtube').get_settings().verify_ssl() + def __init__(self, context): + self._context = context + super(AbstractResolver, self).__init__() def supports_url(self, url, url_components): raise NotImplementedError() @@ -32,165 +51,141 @@ def resolve(self, url, url_components): class YouTubeResolver(AbstractResolver): - RE_USER_NAME = re.compile(r'http(s)?://(www.)?youtube.com/(?P[a-zA-Z0-9]+)$') - - def __init__(self): - super(YouTubeResolver, self).__init__() + def __init__(self, *args, **kwargs): + super(YouTubeResolver, self).__init__(*args, **kwargs) def supports_url(self, url, url_components): - if url_components.hostname == 'www.youtube.com' or url_components.hostname == 'youtube.com': - if url_components.path.lower() in ['/redirect', '/user']: - return True - - if url_components.path.lower().startswith('/user'): - return True - - re_match = self.RE_USER_NAME.match(url) - if re_match: - return True - - return False - - def resolve(self, url, url_components): - def _load_page(_url): - # we try to extract the channel id from the html content. With the channel id we can construct a url we - # already work with. - # https://www.youtube.com/channel/ - try: - headers = {'Cache-Control': 'max-age=0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36', - 'DNT': '1', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'} - response = requests.get(url, headers=headers, verify=self._verify) - if response.status_code == 200: - match = re.search(r'', response.text) - if match: - channel_id = match.group('channel_id') - return 'https://www.youtube.com/channel/%s' % channel_id - except: - # do nothing - pass - - return _url - - if url_components.path.lower() == '/redirect': + if url_components.hostname not in ( + 'www.youtube.com', + 'youtube.com', + 'm.youtube.com', + ): + return False + + path = url_components.path.lower() + if path.startswith(( + '/@', + '/c/', + '/channel/', + '/user/', + )): + return 'GET' + + if path.startswith(( + '/embed', + '/live', + '/redirect', + '/shorts', + '/supported_browsers', + '/watch', + )): + return 'HEAD' + + # user channel in the form of youtube.com/username + path = path.strip('/').split('/', 1) + return 'GET' if len(path) == 1 and path[0] else False + + def resolve(self, url, url_components, method='HEAD'): + path = url_components.path.lower() + if path == '/redirect': params = dict(parse_qsl(url_components.query)) - return params['q'] - - if url_components.path.lower().startswith('/user'): - return _load_page(url) + url = params['q'] + + # "sometimes", we get a redirect through a URL of the form + # https://.../supported_browsers?next_url=&further=parameters&stuck=here + # put together query string from both what's encoded inside + # next_url and the remaining parameters of this URL... + elif path == '/supported_browsers': + # top-level query string + params = dict(parse_qsl(url_components.query)) + # components of next_url + next_components = urlparse(params.pop('next_url', '')) + if not next_components.scheme or not next_components.netloc: + return url + # query string encoded inside next_url + next_params = dict(parse_qsl(next_components.query)) + # add/overwrite all other params from top level query string + next_params.update(params) + # build new URL from these components + return next_components._replace( + query=urlencode(next_params) + ).geturl() + + response = self.request(url, + method=method, + headers=self._HEADERS, + allow_redirects=True) + if response.status_code != 200: + return url - re_match = self.RE_USER_NAME.match(url) - if re_match: - return _load_page(url) + # we try to extract the channel id from the html content + # With the channel id we can construct a URL we already work with + # https://www.youtube.com/channel/ + if method == 'GET': + match = re.search( + r'', + response.text) + if match: + return match.group('channel_url') - return url + return response.url -class CommonResolver(AbstractResolver, list): - def __init__(self): - super(CommonResolver, self).__init__() +class CommonResolver(AbstractResolver): + def __init__(self, *args, **kwargs): + super(CommonResolver, self).__init__(*args, **kwargs) def supports_url(self, url, url_components): - return True - - def resolve(self, url, url_components): - def _loop(_url, tries=5): - if tries == 0: - return _url - - try: - headers = {'Cache-Control': 'max-age=0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36', - 'DNT': '1', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'en-US,en;q=0.8,de;q=0.6'} - response = requests.head(_url, headers=headers, verify=self._verify, allow_redirects=False) - if response.status_code == 304: - return url - - if response.status_code in [301, 302, 303]: - headers = response.headers - location = headers.get('location', '') - - # validate the location - some server returned garbage - _url_components = urlparse(location) - if not _url_components.scheme and not _url_components.hostname: - return url - - # some server return 301 for HEAD requests - # we just compare the new location - if it's equal we can return the url - if location == _url or ''.join([location, '/']) == _url or location == ''.join([_url, '/']): - return _url - - if location: - return _loop(location, tries=tries - 1) - - # just to be sure ;) - location = headers.get('Location', '') - if location: - return _loop(location, tries=tries - 1) - - if response.status_code == 200: - _url_components = urlparse(_url) - if _url_components.path == '/supported_browsers': - # "sometimes", we get a redirect through an URL of the form https://.../supported_browsers?next_url=&further=paramaters&stuck=here - # put together query string from both what's encoded inside next_url and the remaining paramaters of this URL... - _query = parse_qs(_url_components.query) # top-level query string - _nc = urlparse(_query['next_url'][0]) # components of next_url - _next_query = parse_qs(_nc.query) # query string encoded inside next_url - del _query['next_url'] # remove next_url from top level query string - _next_query.update(_query) # add/overwrite all other params from top level query string - _next_query = dict(map(lambda kv: (kv[0], kv[1][0]), _next_query.items())) # flatten to only use first argument of each param - _next_url = urlunsplit((_nc.scheme, _nc.netloc, _nc.path, urlencode(_next_query), _nc.fragment)) # build new URL from these components - return _next_url - - except: - # do nothing - pass - - return _url - - resolved_url = _loop(url) - - return resolved_url + return 'HEAD' + + def resolve(self, url, url_components, method='HEAD'): + response = self.request(url, + method=method, + headers=self._HEADERS, + allow_redirects=True) + if response.status_code != 200: + return url + return response.url class UrlResolver(object): def __init__(self, context): self._context = context - self._cache = {} - self._youtube_resolver = YouTubeResolver() - self._resolver = [ - self._youtube_resolver, - CommonResolver() + self._cache = context.get_function_cache() + self._resolver_map = { + 'common_resolver': CommonResolver(context), + 'youtube_resolver': YouTubeResolver(context), + } + self._resolvers = [ + 'common_resolver', + 'youtube_resolver', ] def clear(self): - self._context.get_function_cache().clear() + self._cache.clear() def _resolve(self, url): - # try one of the resolver - url_components = urlparse(url) - for resolver in self._resolver: - if resolver.supports_url(url, url_components): - resolved_url = resolver.resolve(url, url_components) - self._cache[url] = resolved_url - - # one last check...sometimes the resolved url is YouTube-specific and can be resolved again or - # simplified. - url_components = urlparse(resolved_url) - if resolver is not self._youtube_resolver and self._youtube_resolver.supports_url(resolved_url, url_components): - return self._youtube_resolver.resolve(resolved_url, url_components) - - return resolved_url + # try one of the resolvers + resolved_url = url + for resolver_name in self._resolvers: + resolver = self._resolver_map[resolver_name] + url_components = urlparse(resolved_url) + method = resolver.supports_url(resolved_url, url_components) + if not method: + continue + + self._context.log_debug('Resolving |{uri}| using |{name} {method}|' + .format(uri=resolved_url, + name=resolver_name, + method=method)) + resolved_url = resolver.resolve(resolved_url, + url_components, + method) + self._context.log_debug('Resolved to |{0}|'.format(resolved_url)) + return resolved_url def resolve(self, url): - function_cache = self._context.get_function_cache() - resolved_url = function_cache.get(FunctionCache.ONE_DAY, self._resolve, url) + resolved_url = self._cache.get(self._cache.ONE_DAY, self._resolve, url) if not resolved_url or resolved_url == '/': return url diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 3d8a56e55..4cfff9f26 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -164,10 +164,8 @@ def _extract_urls(_video_id): res_urls = [] for url in urls: - context.log_debug('Resolving url "%s"' % url) progress_dialog.update(steps=1, text=url) resolved_url = url_resolver.resolve(url) - context.log_debug('Resolved url "%s"' % resolved_url) res_urls.append(resolved_url) if progress_dialog.is_aborted(): From 680ab035928ba050853d41c9e68013b8de7c5f6a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:31:45 +1100 Subject: [PATCH 050/141] Improve UrlToItemConverter channels and playlists functionality - Add support for live channels - Add support for playlist starting video_id - Simplify regexp --- .../youtube/helper/url_to_item_converter.py | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index 20f0d4452..536e0e0d1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -16,9 +16,7 @@ class UrlToItemConverter(object): - RE_CHANNEL_ID = re.compile(r'^/channel/(?P.+)$', re.I) - RE_LIVE_VID = re.compile(r'^/live/(?P.+)$', re.I) - RE_SHORTS_VID = re.compile(r'^/shorts/(?P[^?/]+)$', re.I) + RE_PATH_ID = re.compile(r'/\w+/(?P[^/?#]+)', re.I) RE_SEEK_TIME = re.compile(r'\d+') VALID_HOSTNAMES = { 'youtube.com', @@ -48,25 +46,25 @@ def add_url(self, url, provider, context): )) return - params = dict(parse_qsl(parsed_url.query)) + url_params = dict(parse_qsl(parsed_url.query)) path = parsed_url.path.lower() - video_id = playlist_id = channel_id = seek_time = None + channel_id = live = playlist_id = seek_time = video_id = None if path == '/watch': - video_id = params.get('v') - playlist_id = params.get('list') - seek_time = params.get('t') + video_id = url_params.get('v') + playlist_id = url_params.get('list') + seek_time = url_params.get('t') elif path == '/playlist': - playlist_id = params.get('list') - elif path.startswith('/shorts/'): - re_match = self.RE_SHORTS_VID.match(parsed_url.path) - video_id = re_match.group('video_id') + playlist_id = url_params.get('list') elif path.startswith('/channel/'): - re_match = self.RE_CHANNEL_ID.match(parsed_url.path) - channel_id = re_match.group('channel_id') - elif path.startswith('/live/'): - re_match = self.RE_LIVE_VID.match(parsed_url.path) - video_id = re_match.group('video_id') + re_match = self.RE_PATH_ID.match(parsed_url.path) + channel_id = re_match.group('id') + if '/live' in path: + live = 1 + elif path.startswith(('/live/', '/shorts/')): + re_match = self.RE_PATH_ID.match(parsed_url.path) + video_id = re_match.group('id') + seek_time = url_params.get('t') else: context.log_debug('Unknown path "{0}" in url "{1}"'.format( parsed_url.path, url @@ -87,8 +85,11 @@ def add_url(self, url, provider, context): if number ) plugin_params['seek'] = seek_time - plugin_uri = context.create_uri(['play'], plugin_params) - video_item = VideoItem('', plugin_uri) + if playlist_id: + plugin_params['playlist_id'] = playlist_id + video_item = VideoItem( + '', context.create_uri(['play'], plugin_params) + ) self._video_id_dict[video_id] = video_item elif playlist_id: @@ -103,10 +104,13 @@ def add_url(self, url, provider, context): elif channel_id: if self._flatten: + if live: + context.set_param('live', live) self._channel_ids.append(channel_id) else: channel_item = DirectoryItem( - '', context.create_uri(['channel', channel_id]) + '', context.create_uri(['channel', channel_id], + {'live': live} if live else None) ) channel_item.set_fanart(provider.get_fanart(context)) self._channel_id_dict[channel_id] = channel_item From 0d31f4b0dc4ba088d9fe23c51306e169e1272b61 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 2 Dec 2023 10:10:13 +1100 Subject: [PATCH 051/141] Add option to disable HFR video at max resolution - Closes #539 --- resources/language/resource.language.en_au/strings.po | 4 ++++ resources/language/resource.language.en_gb/strings.po | 4 ++++ resources/language/resource.language.en_nz/strings.po | 4 ++++ resources/language/resource.language.en_us/strings.po | 4 ++++ resources/lib/youtube_plugin/youtube/helper/video_info.py | 6 +++++- resources/settings.xml | 1 + 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index 783ff9c92..de411ad0c 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -1368,3 +1368,7 @@ msgstr "" msgctxt "#30767" msgid "Views" msgstr "" + +msgctxt "#30768" +msgid "Disable high framerate video at maximum video quality" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index f8007413a..831ef1a2b 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1368,3 +1368,7 @@ msgstr "" msgctxt "#30767" msgid "Views" msgstr "" + +msgctxt "#30768" +msgid "Disable high framerate video at maximum video quality" +msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index b1e21fa84..15c435e76 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -1364,3 +1364,7 @@ msgstr "" msgctxt "#30767" msgid "Views" msgstr "" + +msgctxt "#30768" +msgid "Disable high framerate video at maximum video quality" +msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index 2248415ba..e33ce18a0 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -1369,3 +1369,7 @@ msgstr "" msgctxt "#30767" msgid "Views" msgstr "" + +msgctxt "#30768" +msgid "Disable high framerate video at maximum video quality" +msgstr "" diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 193245d9f..34dd9916b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1358,6 +1358,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): stream_features = _settings.stream_features() allow_hdr = 'hdr' in stream_features allow_hfr = 'hfr' in stream_features + disable_hfr_max = 'no_hfr_max' in stream_features allow_ssa = 'ssa' in stream_features stream_select = _settings.stream_select() @@ -1512,7 +1513,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): compare_width = width compare_height = height - bounded_quality = {} + bounded_quality = None for quality in qualities: if compare_width > quality['width']: if bounded_quality: @@ -1520,7 +1521,10 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): quality = bounded_quality elif compare_height < quality['height']: quality = qualities[-1] + if fps > 30 and disable_hfr_max: + bounded_quality = None break + disable_hfr_max = disable_hfr_max and not bounded_quality bounded_quality = quality if not bounded_quality: continue diff --git a/resources/settings.xml b/resources/settings.xml index dfa126b51..4c06c207f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -271,6 +271,7 @@ + From bf7b12a57d0288e9f1994339691d7030df0c34d8 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 3 Dec 2023 22:10:10 +1100 Subject: [PATCH 052/141] Improve video stats display - Fix incorrect duration of upcoming videos - Fix imdbnumber being saved to incorrect info label - Add view count to listitem info and sort methods - Add rating to listitem info and sort method based on made up ranking - Add label masking to sort methods --- .../kodion/abstract_provider.py | 6 +- .../kodion/constants/const_sort_methods.py | 101 ++++++++---------- .../kodion/context/xbmc/xbmc_context.py | 2 +- .../youtube_plugin/kodion/items/base_item.py | 7 ++ .../youtube_plugin/kodion/items/video_item.py | 21 ++-- .../kodion/ui/xbmc/info_labels.py | 19 +++- .../youtube_plugin/kodion/utils/__init__.py | 2 + .../youtube_plugin/kodion/utils/methods.py | 19 +++- .../youtube_plugin/youtube/helper/utils.py | 49 ++++++--- .../lib/youtube_plugin/youtube/provider.py | 21 ++-- 10 files changed, 150 insertions(+), 97 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 15c150ece..67aacfdc3 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -134,8 +134,6 @@ def _internal_root(self, context, re_match): @staticmethod def _internal_favorite(context, re_match): - context.add_sort_method(constants.sort_method.LABEL_IGNORE_THE) - params = context.get_params() command = re_match.group('command') @@ -197,8 +195,6 @@ def data_cache(self, context): self._data_cache = context.get_data_cache() def _internal_search(self, context, re_match): - context.add_sort_method(constants.sort_method.UNSORTED) - params = context.get_params() command = re_match.group('command') @@ -276,7 +272,7 @@ def _internal_search(self, context, re_match): query = query.decode('utf-8') return self.on_search(query, context, re_match) - context.set_content_type(constants.content_type.FILES) + context.set_content_type(constants.content_type.VIDEOS) result = [] location = context.get_param('location', False) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py b/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py index 2838ed93b..c8d2cdb8e 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py @@ -9,66 +9,57 @@ """ import sys + from xbmcplugin import __dict__ as xbmcplugin namespace = sys.modules[__name__] names = [ - # 'NONE', - 'LABEL', - 'LABEL_IGNORE_THE', - 'DATE', - 'SIZE', - 'FILE', - 'DRIVE_TYPE', - 'TRACKNUM', - 'DURATION', - 'TITLE', - 'TITLE_IGNORE_THE', - 'ARTIST', - # 'ARTIST_AND_YEAR', - 'ARTIST_IGNORE_THE', - 'ALBUM', - 'ALBUM_IGNORE_THE', - 'GENRE', - 'COUNTRY', - # 'YEAR', - 'VIDEO_YEAR', - 'VIDEO_RATING', - 'VIDEO_USER_RATING', - 'DATEADDED', - 'PROGRAM_COUNT', - 'PLAYLIST_ORDER', - 'EPISODE', - 'VIDEO_TITLE', - 'VIDEO_SORT_TITLE', - 'VIDEO_SORT_TITLE_IGNORE_THE', - 'PRODUCTIONCODE', - 'SONG_RATING', - 'SONG_USER_RATING', - 'MPAA_RATING', - 'VIDEO_RUNTIME', - 'STUDIO', - 'STUDIO_IGNORE_THE', - 'FULLPATH', - 'LABEL_IGNORE_FOLDERS', - 'LASTPLAYED', - 'PLAYCOUNT', - 'LISTENERS', - 'UNSORTED', - 'CHANNEL', - 'CHANNEL_NUMBER', - 'BITRATE', - 'DATE_TAKEN', - 'CLIENT_CHANNEL_ORDER', - 'TOTAL_DISCS', - 'ORIG_DATE', - 'BPM', - 'VIDEO_ORIGINAL_TITLE', - 'VIDEO_ORIGINAL_TITLE_IGNORE_THE', - 'PROVIDER', - 'USER_PREFERENCE', - # 'MAX', + 'NONE', # 0 + 'LABEL', # 1 + 'LABEL_IGNORE_THE', # 2 + 'DATE', # 3 + 'SIZE', # 4 + 'FILE', # 5 + 'DRIVE_TYPE', # 6 + 'TRACKNUM', # 7 + 'DURATION', # 8 + 'TITLE', # 9 + 'TITLE_IGNORE_THE', # 10 + 'ARTIST', # 11 + 'ARTIST_IGNORE_THE', # 13 + 'ALBUM', # 14 + 'ALBUM_IGNORE_THE', # 15 + 'GENRE', # 16 + 'COUNTRY', # 17 + 'VIDEO_YEAR', # 18 + 'VIDEO_RATING', # 19 + 'VIDEO_USER_RATING', # 20 + 'DATEADDED', # 21 + 'PROGRAM_COUNT', # 22 + 'PLAYLIST_ORDER', # 23 + 'EPISODE', # 24 + 'VIDEO_TITLE', # 25 + 'VIDEO_SORT_TITLE', # 26 + 'VIDEO_SORT_TITLE_IGNORE_THE', # 27 + 'PRODUCTIONCODE', # 28 + 'SONG_RATING', # 29 + 'SONG_USER_RATING', # 30 + 'MPAA_RATING', # 31 + 'VIDEO_RUNTIME', # 32 + 'STUDIO', # 33 + 'STUDIO_IGNORE_THE', # 34 + 'FULLPATH', # 35 + 'LABEL_IGNORE_FOLDERS', # 36 + 'LASTPLAYED', # 37 + 'PLAYCOUNT', # 38 + 'LISTENERS', # 39 + 'UNSORTED', # 40 + 'CHANNEL', # 41 + 'BITRATE', # 43 + 'DATE_TAKEN', # 44 + 'VIDEO_ORIGINAL_TITLE', # 49 + 'VIDEO_ORIGINAL_TITLE_IGNORE_THE', # 50 ] for name in names: diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 873c42f9a..9ea2bbe4c 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -425,7 +425,7 @@ def set_content_type(self, content_type): def add_sort_method(self, *sort_methods): for sort_method in sort_methods: - xbmcplugin.addSortMethod(self._plugin_handle, sort_method) + xbmcplugin.addSortMethod(self._plugin_handle, *sort_method) def clone(self, new_path=None, new_params=None): if not new_path: diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 832d0fa6b..92d70650a 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -37,6 +37,7 @@ def __init__(self, name, uri, image='', fanart=''): self._context_menu = None self._replace_context_menu = False self._added_utc = None + self._count = None self._date = None self._dateadded = None self._short_details = None @@ -144,6 +145,12 @@ def get_short_details(self): def set_short_details(self, details): self._short_details = details or '' + def get_count(self): + return self._count + + def set_count(self, count): + self._count = int(count or 0) + @property def next_page(self): return self._next_page diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index c605d3d4d..ba80baf5a 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -8,12 +8,13 @@ See LICENSES/GPL-2.0-only for more information. """ -import re import datetime +import re +from html import unescape from .base_item import BaseItem +from ..utils import duration_to_seconds -from html import unescape __RE_IMDB__ = re.compile(r'(http(s)?://)?www.imdb.(com|de)/title/(?P[t0-9]+)(/)?') @@ -56,6 +57,7 @@ def __init__(self, name, uri, image='', fanart=''): self._subscription_id = None self._playlist_id = None self._playlist_item_id = None + self._production_code = None def set_play_count(self, play_count): self._play_count = int(play_count or 0) @@ -169,10 +171,11 @@ def set_season(self, season): def get_season(self): return self._season - def set_duration(self, hours, minutes, seconds=0): - _seconds = seconds - _seconds += minutes * 60 - _seconds += hours * 60 * 60 + def set_duration(self, hours=0, minutes=0, seconds=0, duration=''): + if duration: + _seconds = duration_to_seconds(duration) + else: + _seconds = seconds + minutes * 60 + hours * 60 * 60 self.set_duration_from_seconds(_seconds) def set_duration_from_minutes(self, minutes): @@ -317,3 +320,9 @@ def get_playlist_item_id(self): def set_playlist_item_id(self, value): self._playlist_item_id = value + + def get_code(self): + return self._production_code + + def set_code(self, value): + self._production_code = value or '' diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index 13601ddf2..e78d98fc4 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -38,9 +38,8 @@ def _process_audio_rating(info_labels, param): rating = int(param) if rating > 5: rating = 5 - if rating < 0: + elif rating < 0: rating = 0 - info_labels['rating'] = rating @@ -59,7 +58,7 @@ def _process_video_rating(info_labels, param): rating = float(param) if rating > 10.0: rating = 10.0 - if rating < 0.0: + elif rating < 0.0: rating = 0.0 info_labels['rating'] = rating @@ -93,6 +92,11 @@ def create_from_item(base_item): # 'date' = '1982-03-09' _process_date(info_labels, base_item.get_date()) + # 'count' = 12 (integer) + # Can be used to store an id for later, or for sorting purposes + # Used for video view count + _process_int_value(info_labels, 'count', base_item.get_count()) + # Directory if isinstance(base_item, DirectoryItem): _process_string_value(info_labels, 'plot', base_item.get_plot()) @@ -160,12 +164,17 @@ def create_from_item(base_item): # 'plot' = '...' (string) _process_string_value(info_labels, 'plot', base_item.get_plot()) - # 'code' = 'tt3458353' (string) - imdb id - _process_string_value(info_labels, 'code', base_item.get_imdb_id()) + # 'imdbnumber' = 'tt3458353' (string) - imdb id + _process_string_value(info_labels, 'imdbnumber', base_item.get_imdb_id()) # 'cast' = [] (list) _process_list_value(info_labels, 'cast', base_item.get_cast()) + # 'code' = '101' (string) + # Production code, currently used to store misc video data for label + # formatting + _process_string_value(info_labels, 'code', base_item.get_code()) + # Audio and Video if isinstance(base_item, (AudioItem, VideoItem)): # 'title' = 'Blow Your Head Off' (string) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 5b9e8df41..763a92912 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -12,6 +12,7 @@ from .methods import ( create_path, create_uri_path, + duration_to_seconds, find_best_fit, find_video_id, friendly_number, @@ -39,6 +40,7 @@ 'create_path', 'create_uri_path', 'datetime_parser', + 'duration_to_seconds', 'find_best_fit', 'find_video_id', 'friendly_number', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 92b695431..b5482e6dd 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -20,6 +20,7 @@ __all__ = ( 'create_path', 'create_uri_path', + 'duration_to_seconds', 'find_best_fit', 'find_video_id', 'friendly_number', @@ -266,4 +267,20 @@ def friendly_number(number, precision=3, scale=('', 'K', 'M', 'B')): magnitude = 0 if _abs_input < 1000 else int(log(floor(_abs_input), 1000)) return '{output:f}'.format( output=_input / 1000 ** magnitude - ).rstrip('0').rstrip('.') + scale[magnitude] + ).rstrip('0').rstrip('.') + scale[magnitude], _input + + +_RE_PERIODS = re.compile(r'(\d+)(d|h|m|s)') +_SECONDS_IN_PERIODS = { + 's': 1, # 1 second + 'm': 60, # 1 minute + 'h': 3600, # 1 hour + 'd': 86400, # 1 day +} + + +def duration_to_seconds(duration): + return sum( + int(number) * _SECONDS_IN_PERIODS[period] + for number, period in re.findall(_RE_PERIODS, duration) + ) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 27a2db505..fd29f4b3d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -10,6 +10,7 @@ import re import time +from math import log10 from ...kodion import utils from ...kodion.items import DirectoryItem @@ -68,7 +69,7 @@ def make_comment_item(context, snippet, uri, total_replies=0): like_count = snippet['likeCount'] if like_count: - like_count = utils.friendly_number(like_count) + like_count, _ = utils.friendly_number(like_count) color = __COLOR_MAP['likeCount'] label_likes = ui.color(color, ui.bold(like_count)) plot_likes = ui.color(color, ui.bold(' '.join(( @@ -78,7 +79,7 @@ def make_comment_item(context, snippet, uri, total_replies=0): plot_props.append(plot_likes) if total_replies: - total_replies = utils.friendly_number(total_replies) + total_replies, _ = utils.friendly_number(total_replies) color = __COLOR_MAP['commentCount'] label_replies = ui.color(color, ui.bold(total_replies)) plot_replies = ui.color(color, ui.bold(' '.join(( @@ -366,13 +367,15 @@ def update_video_infos(provider, context, video_id_dict, video_item.upcoming = broadcast_type == 'upcoming' # duration - if not video_item.live and play_data and 'total_time' in play_data: + if (not (video_item.live or video_item.upcoming) + and play_data and 'total_time' in play_data): duration = play_data['total_time'] else: duration = yt_item.get('contentDetails', {}).get('duration') if duration: + duration = utils.datetime_parser.parse(duration) # subtract 1s because YouTube duration is +1s too long - duration = utils.datetime_parser.parse(duration).seconds - 1 + duration = (duration.seconds - 1) if duration.seconds else None if duration: video_item.set_duration_from_seconds(duration) @@ -415,33 +418,45 @@ def update_video_infos(provider, context, video_id_dict, label_stats = [] stats = [] + rating = [0, 0] if 'statistics' in yt_item: for stat, value in yt_item['statistics'].items(): label = context.LOCAL_MAP.get('stats.' + stat) if label: color = __COLOR_MAP.get(stat, 'white') - value = utils.friendly_number(value) - label_stats.append(ui.color(color, value)) + str_value, value = utils.friendly_number(value) + label_stats.append(ui.color(color, str_value)) stats.append(ui.color(color, ui.bold(' '.join(( - value, context.localize(label) + str_value, context.localize(label) ))))) + else: + continue + if stat == 'likeCount': + rating[0] = value + elif stat == 'viewCount': + rating[1] = value + video_item.set_count(value) label_stats = '|'.join(label_stats) stats = '|'.join(stats) + if 0 < rating[0] <= rating[1]: + if rating[0] == rating[1]: + rating = 10 + else: + # This is a completely made up, arbitrary ranking score + rating = (10 * (log10(rating[1]) * log10(rating[0])) + / (log10(rating[0] + rating[1]) ** 2)) + video_item.set_rating(rating) # Used for label2, but is poorly supported in skins - video_details = ' | '.join((detail for detail in ( - stats if stats else '', - ui.italic(start_at) if start_at else '', - ) if detail)) - video_item.set_short_details(video_details) + video_item.set_short_details(label_stats) + # Hack to force a custom label mask containing production code, + # activated on sort order selection, to display details + # Refer Provider.set_content_type for usage + video_item.set_code(label_stats) # update and set the title title = video_item.get_title() or snippet['title'] or '' - if video_item.upcoming: - title = ui.italic(title) - if label_stats: - title = '{0} ({1})'.format(title, label_stats) - video_item.set_title(title) + video_item.set_title(ui.italic(title) if video_item.upcoming else title) """ This is experimental. We try to get the most information out of the title of a video. diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index a945f673b..8eeaab1fc 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1441,13 +1441,20 @@ def on_root(self, context, re_match): @staticmethod def set_content_type(context, content_type): context.set_content_type(content_type) - if content_type == constants.content_type.VIDEOS: - context.add_sort_method(constants.sort_method.UNSORTED, - constants.sort_method.VIDEO_RUNTIME, - constants.sort_method.DATEADDED, - constants.sort_method.TRACKNUM, - constants.sort_method.VIDEO_TITLE, - constants.sort_method.DATE) + context.add_sort_method( + (constants.sort_method.UNSORTED, '%T', '%P | %J | %D'), + (constants.sort_method.LABEL_IGNORE_THE, '%T', '%P | %J | %D'), + ) + if content_type != constants.content_type.VIDEOS: + return + context.add_sort_method( + (constants.sort_method.PROGRAM_COUNT, '%T \u2022 %P | %J | %D', '%C'), + (constants.sort_method.VIDEO_RATING, '%T \u2022 %P | %J | %D', '%R'), + (constants.sort_method.DATE, '%T \u2022 %P | %D', '%J'), + (constants.sort_method.DATEADDED, '%T \u2022 %P | %D', '%a'), + (constants.sort_method.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'), + (constants.sort_method.TRACKNUM, '[%N. ]%T', '%P | %J | %D'), + ) def handle_exception(self, context, exception_to_handle): if isinstance(exception_to_handle, (InvalidGrant, LoginException)): From 0bb49d77d81f75bd66e6fd87771060d6699718b7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 3 Dec 2023 22:23:26 +1100 Subject: [PATCH 053/141] Fix incorrect usage of DataCache in VideoInfo - Move class variable to Storage and access via instances - Change parameter order for DataCache.getItem[s] and FunctionCache.get - first param is now item/func to - second param is now time in seconds - Optimise imports --- .../kodion/abstract_provider.py | 31 ++++++----- .../youtube_plugin/kodion/utils/data_cache.py | 17 ++---- .../kodion/utils/function_cache.py | 11 +--- .../youtube_plugin/kodion/utils/storage.py | 6 +++ .../youtube_plugin/youtube/client/youtube.py | 19 ++++--- .../youtube/helper/resource_manager.py | 10 ++-- .../youtube/helper/signature/cipher.py | 8 +-- .../youtube/helper/url_resolver.py | 2 +- .../youtube/helper/video_info.py | 54 +++++++++---------- .../youtube/helper/yt_playlist.py | 27 +++++----- .../youtube/helper/yt_specials.py | 54 +++++++++++-------- .../lib/youtube_plugin/youtube/provider.py | 52 +++++++++--------- 12 files changed, 152 insertions(+), 139 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 67aacfdc3..61fec5695 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -196,41 +196,46 @@ def data_cache(self, context): def _internal_search(self, context, re_match): params = context.get_params() + ui = context.get_ui() command = re_match.group('command') search_history = context.get_search_history() if command == 'remove': query = params['q'] search_history.remove(query) - context.get_ui().refresh_container() + ui.refresh_container() return True if command == 'rename': query = params['q'] - result, new_query = context.get_ui().on_keyboard_input(context.localize('search.rename'), - query) + result, new_query = ui.on_keyboard_input( + context.localize('search.rename'), query + ) if result: search_history.rename(query, new_query) - context.get_ui().refresh_container() + ui.refresh_container() return True if command == 'clear': search_history.clear() - context.get_ui().refresh_container() + ui.refresh_container() return True if command == 'input': self.data_cache = context - folder_path = context.get_ui().get_info_label('Container.FolderPath') + folder_path = ui.get_info_label('Container.FolderPath') query = None + # came from page 1 of search query by '..'/back + # user doesn't want to input on this path if (folder_path.startswith('plugin://%s' % context.get_id()) and re.match('.+/(?:query|input)/.*', folder_path)): - cached_query = self.data_cache.get_item(self.data_cache.ONE_DAY, 'search_query') - # came from page 1 of search query by '..'/back, user doesn't want to input on this path - if cached_query and cached_query.get('search_query', {}).get('query'): - query = cached_query.get('search_query', {}).get('query') - query = to_unicode(query) - query = unquote(query) + cached = self.data_cache.get_item('search_query', + self.data_cache.ONE_DAY) + cached = cached and cached.get('query') + if cached: + query = unquote(to_unicode(cached)) else: - result, input_query = context.get_ui().on_keyboard_input(context.localize('search.title')) + result, input_query = ui.on_keyboard_input( + context.localize('search.title') + ) if result: query = input_query diff --git a/resources/lib/youtube_plugin/kodion/utils/data_cache.py b/resources/lib/youtube_plugin/kodion/utils/data_cache.py index 7e986beeb..f3a29ed92 100644 --- a/resources/lib/youtube_plugin/kodion/utils/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/utils/data_cache.py @@ -15,12 +15,6 @@ class DataCache(Storage): - ONE_MINUTE = 60 - ONE_HOUR = 60 * ONE_MINUTE - ONE_DAY = 24 * ONE_HOUR - ONE_WEEK = 7 * ONE_DAY - ONE_MONTH = 4 * ONE_WEEK - def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 super(DataCache, self).__init__(filename, max_file_size_kb=max_file_size_kb) @@ -28,7 +22,7 @@ def __init__(self, filename, max_file_size_mb=5): def is_empty(self): return self._is_empty() - def get_items(self, seconds, content_ids): + def get_items(self, content_ids, seconds): query_result = self._get_by_ids(content_ids, process=json.loads) if not query_result: return {} @@ -41,18 +35,17 @@ def get_items(self, seconds, content_ids): } return result - def get_item(self, seconds, content_id): + def get_item(self, content_id, seconds): content_id = str(content_id) query_result = self._get(content_id) if not query_result: - return {} + return None current_time = datetime.now() if self.get_seconds_diff(query_result[1] or current_time) > seconds: - return {} + return None - result = {content_id: json.loads(query_result[0])} - return result + return json.loads(query_result[0]) def set_item(self, content_id, item): self._set(content_id, item) diff --git a/resources/lib/youtube_plugin/kodion/utils/function_cache.py b/resources/lib/youtube_plugin/kodion/utils/function_cache.py index ea0b7c03f..3929878dc 100644 --- a/resources/lib/youtube_plugin/kodion/utils/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/utils/function_cache.py @@ -15,12 +15,6 @@ class FunctionCache(Storage): - ONE_MINUTE = 60 - ONE_HOUR = 60 * ONE_MINUTE - ONE_DAY = 24 * ONE_HOUR - ONE_WEEK = 7 * ONE_DAY - ONE_MONTH = 4 * ONE_WEEK - def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 super(FunctionCache, self).__init__(filename, max_file_size_kb=max_file_size_kb) @@ -76,12 +70,11 @@ def get_cached_only(self, func, *args, **keywords): return None - def get(self, seconds, func, *args, **keywords): + def get(self, func, seconds, *args, **keywords): """ Returns the cached data of the given function. - :param partial_func: function to cache + :param func, function to cache :param seconds: time to live in seconds - :param return_cached_only: return only cached data and don't call the function :return: """ diff --git a/resources/lib/youtube_plugin/kodion/utils/storage.py b/resources/lib/youtube_plugin/kodion/utils/storage.py index 00efd4983..0306ee053 100644 --- a/resources/lib/youtube_plugin/kodion/utils/storage.py +++ b/resources/lib/youtube_plugin/kodion/utils/storage.py @@ -20,6 +20,12 @@ class Storage(object): + ONE_MINUTE = 60 + ONE_HOUR = 60 * ONE_MINUTE + ONE_DAY = 24 * ONE_HOUR + ONE_WEEK = 7 * ONE_DAY + ONE_MONTH = 4 * ONE_WEEK + _table_name = 'storage' _clear_query = 'DELETE FROM %s' % _table_name _create_table_query = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % _table_name diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index fdb04295c..c81a4fedd 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -303,16 +303,15 @@ def _get_recommendations_for_home(self): # Do we have a cached result? cache_home_key = 'get-activities-home' - cached = cache.get_item(cache.ONE_HOUR * 4, cache_home_key) - if cache_home_key in cached and cached[cache_home_key].get('items'): - return cached[cache_home_key] + cached = cache.get_item(cache_home_key, cache.ONE_HOUR * 4) + cached = cached and cached.get('items') + if cached: + return cached # Fetch existing list of items, if any - items = [] cache_items_key = 'get-activities-home-items' - cached = cache.get_item(cache.ONE_WEEK * 2, cache_items_key) - if cache_items_key in cached: - items = cached[cache_items_key] + cached = cache.get_item(cache_items_key, cache.ONE_WEEK * 2) + items = cached if cached else [] # Fetch history and recommended items. Use threads for faster execution. def helper(video_id, responses): @@ -761,9 +760,9 @@ def _perform(_page_token, _offset, _result): # if new uploads is cached cache_items_key = 'my-subscriptions-items' - cached = cache.get_item(cache.ONE_HOUR, cache_items_key) - if cache_items_key in cached: - _result['items'] = cached[cache_items_key] + cached = cache.get_item(cache_items_key, cache.ONE_HOUR) + if cached: + _result['items'] = cached """ no cache, get uploads data from web """ if not _result['items']: diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 21896f83a..0ec5e7e3e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -41,7 +41,9 @@ def _update_channels(self, channel_ids): for channel_id in channel_ids: if channel_id == 'mine': - json_data = function_cache.get(function_cache.ONE_DAY, self._client.get_channel_by_username, channel_id) + json_data = function_cache.get(self._client.get_channel_by_username, + function_cache.ONE_DAY, + channel_id) items = json_data.get('items', [{'id': 'mine'}]) try: @@ -58,7 +60,7 @@ def _update_channels(self, channel_ids): channel_ids = updated_channel_ids data_cache = self._context.get_data_cache() - channel_data = data_cache.get_items(data_cache.ONE_MONTH, channel_ids) + channel_data = data_cache.get_items(channel_ids, data_cache.ONE_MONTH) channel_ids = set(channel_ids) channel_ids_cached = set(channel_data) @@ -92,7 +94,7 @@ def _update_channels(self, channel_ids): def _update_videos(self, video_ids, live_details=False, suppress_errors=False): json_data = None data_cache = self._context.get_data_cache() - video_data = data_cache.get_items(data_cache.ONE_MONTH, video_ids) + video_data = data_cache.get_items(video_ids, data_cache.ONE_MONTH) video_ids = set(video_ids) video_ids_cached = set(video_data) @@ -143,7 +145,7 @@ def get_videos(self, video_ids, live_details=False, suppress_errors=False): def _update_playlists(self, playlists_ids): json_data = None data_cache = self._context.get_data_cache() - playlist_data = data_cache.get_items(data_cache.ONE_MONTH, playlists_ids) + playlist_data = data_cache.get_items(playlists_ids, data_cache.ONE_MONTH) playlists_ids = set(playlists_ids) playlists_ids_cached = set(playlist_data) diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py index 15c7d4028..fa52385c6 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py @@ -10,7 +10,6 @@ import re -from ....kodion.utils import FunctionCache from .json_script_engine import JsonScriptEngine @@ -24,9 +23,12 @@ def __init__(self, context, javascript): def get_signature(self, signature): function_cache = self._context.get_function_cache() - json_script = function_cache.get_cached_only(self._load_javascript, self._javascript) + json_script = function_cache.get_cached_only(self._load_javascript, + self._javascript) if not json_script: - json_script = function_cache.get(FunctionCache.ONE_DAY, self._load_javascript, self._javascript) + json_script = function_cache.get(self._load_javascript, + function_cache.ONE_DAY, + self._javascript) if json_script: json_script_engine = JsonScriptEngine(json_script) diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index d82a1db84..bed917bd6 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -185,7 +185,7 @@ def _resolve(self, url): return resolved_url def resolve(self, url): - resolved_url = self._cache.get(self._cache.ONE_DAY, self._resolve, url) + resolved_url = self._cache.get(self._resolve, self._cache.ONE_DAY, url) if not resolved_url or resolved_url == '/': return url diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 34dd9916b..580f722b3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -8,31 +8,31 @@ See LICENSES/GPL-2.0-only for more information. """ -import re import random +import re import traceback -from json import dumps as json_dumps, loads as json_loads from html import unescape +from json import dumps as json_dumps, loads as json_loads from urllib.parse import ( parse_qs, quote, unquote, - urlsplit, - urlunsplit, urlencode, urljoin, + urlsplit, + urlunsplit, ) import xbmcvfs -from ..client.request_client import YouTubeRequestClient -from ...kodion.network import is_httpd_live -from ...kodion.utils import make_dirs, DataCache -from ..youtube_exceptions import YouTubeException -from .subtitles import Subtitles from .ratebypass import ratebypass from .signature.cipher import Cipher +from .subtitles import Subtitles +from ..client.request_client import YouTubeRequestClient +from ..youtube_exceptions import YouTubeException +from ...kodion.network import is_httpd_live +from ...kodion.utils import make_dirs class VideoInfo(YouTubeRequestClient): @@ -711,13 +711,10 @@ def _get_player_config(page): return None def _get_player_js(self): - cached_url = self._data_cache.get_item( - DataCache.ONE_HOUR * 4, 'player_js_url' - ).get('url', '') - if cached_url not in {'', 'http://', 'https://'}: - js_url = cached_url - else: - js_url = None + cached = self._data_cache.get_item('player_js_url', + self._data_cache.ONE_HOUR * 4) + cached = cached and cached.get('url', '') + js_url = cached if cached not in {'', 'http://', 'https://'} else None if not js_url: player_page = self._get_player_page() @@ -739,12 +736,12 @@ def _get_player_js(self): js_url = self._normalize_url(js_url) self._data_cache.set_item('player_js_url', json_dumps({'url': js_url})) - cache_key = quote(js_url) - cached_js = self._data_cache.get_item( - DataCache.ONE_HOUR * 4, cache_key - ).get('js') - if cached_js: - return cached_js + js_cache_key = quote(js_url) + cached = self._data_cache.get_item(js_cache_key, + self._data_cache.ONE_HOUR * 4) + cached = cached and cached.get('js') + if cached: + return cached client = self.build_client('web') result = self.request( @@ -756,7 +753,7 @@ def _get_player_js(self): return '' javascript = result.text - self._data_cache.set_item(cache_key, json_dumps({'js': javascript})) + self._data_cache.set_item(js_cache_key, json_dumps({'js': javascript})) return javascript @staticmethod @@ -928,9 +925,9 @@ def _process_signature_cipher(self, stream_map): if not url or not encrypted_signature: return None - signature = self._data_cache.get_item( - DataCache.ONE_HOUR * 4, encrypted_signature - ).get('sig') + signature = self._data_cache.get_item(encrypted_signature, + self._data_cache.ONE_HOUR * 4) + signature = signature and signature.get('sig') if not signature: try: signature = self._cipher.get_signature(encrypted_signature) @@ -942,9 +939,8 @@ def _process_signature_cipher(self, stream_map): 'Failed to extract URL from signatureCipher' ) return None - self._data_cache.set_item( - encrypted_signature, json_dumps({'sig': signature}) - ) + self._data_cache.set_item(encrypted_signature, + json_dumps({'sig': signature})) if signature: url = '{0}&{1}={2}'.format(url, query_var, signature) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index d055238d1..2ecec12be 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -8,8 +8,6 @@ See LICENSES/GPL-2.0-only for more information. """ -from ...kodion.utils.function_cache import FunctionCache - from ... import kodion from ...youtube.helper import v3 @@ -135,9 +133,9 @@ def _process_remove_playlist(provider, context): def _process_select_playlist(provider, context): - listitem_path = context.get_ui().get_info_label('Container.ListItem(0).FileNameAndPath') # do this asap, relies on listitems focus - keymap_action = False ui = context.get_ui() + listitem_path = ui.get_info_label('Container.ListItem(0).FileNameAndPath') # do this asap, relies on listitems focus + keymap_action = False page_token = '' current_page = 0 @@ -151,16 +149,19 @@ def _process_select_playlist(provider, context): if not video_id: raise kodion.KodionException('Playlist/Select: missing video_id') + function_cache = context.get_function_cache() + client = provider.get_client(context) while True: current_page += 1 if not page_token: - json_data = context.get_function_cache().get((FunctionCache.ONE_MINUTE // 3), - provider.get_client(context).get_playlists_of_channel, - channel_id='mine') + json_data = function_cache.get(client.get_playlists_of_channel, + function_cache.ONE_MINUTE // 3, + channel_id='mine') else: - json_data = context.get_function_cache().get((FunctionCache.ONE_MINUTE // 3), - provider.get_client(context).get_playlists_of_channel, - channel_id='mine', page_token=page_token) + json_data = function_cache.get(client.get_playlists_of_channel, + function_cache.ONE_MINUTE // 3, + channel_id='mine', + page_token=page_token) playlists = json_data.get('items', []) page_token = json_data.get('nextPageToken', False) @@ -193,12 +194,12 @@ def _process_select_playlist(provider, context): items.append((ui.bold(context.localize('next_page')).replace('%d', str(current_page + 1)), '', 'playlist.next', 'DefaultFolder.png')) - result = context.get_ui().on_select(context.localize('playlist.select'), items) + result = ui.on_select(context.localize('playlist.select'), items) if result == 'playlist.create': - result, text = context.get_ui().on_keyboard_input( + result, text = ui.on_keyboard_input( context.localize('playlist.create')) if result and text: - json_data = provider.get_client(context).create_playlist(title=text) + json_data = client.create_playlist(title=text) if not v3.handle_error(context, json_data): break diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 4cfff9f26..77f9bfe72 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -8,14 +8,21 @@ See LICENSES/GPL-2.0-only for more information. """ -from ... import kodion -from ...kodion.items import DirectoryItem, UriItem -from ...youtube.helper import v3, tv, extract_urls, UrlResolver, UrlToItemConverter from . import utils +from ...kodion import KodionException, constants +from ...kodion.items import DirectoryItem, UriItem +from ...kodion.utils import strip_html_from_text +from ...youtube.helper import ( + UrlResolver, + UrlToItemConverter, + extract_urls, + tv, + v3, +) def _process_related_videos(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] page_token = context.get_param('page_token', '') @@ -30,7 +37,7 @@ def _process_related_videos(provider, context): def _process_parent_comments(provider, context): - provider.set_content_type(context, kodion.constants.content_type.FILES) + provider.set_content_type(context, constants.content_type.FILES) result = [] page_token = context.get_param('page_token', '') @@ -45,7 +52,7 @@ def _process_parent_comments(provider, context): def _process_child_comments(provider, context): - provider.set_content_type(context, kodion.constants.content_type.FILES) + provider.set_content_type(context, constants.content_type.FILES) result = [] page_token = context.get_param('page_token', '') @@ -60,7 +67,7 @@ def _process_child_comments(provider, context): def _process_recommendations(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] page_token = context.get_param('page_token', '') @@ -72,7 +79,7 @@ def _process_recommendations(provider, context): def _process_popular_right_now(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] page_token = context.get_param('page_token', '') @@ -85,7 +92,7 @@ def _process_popular_right_now(provider, context): def _process_browse_channels(provider, context): - provider.set_content_type(context, kodion.constants.content_type.FILES) + provider.set_content_type(context, constants.content_type.FILES) result = [] # page_token = context.get_param('page_token', '') @@ -98,7 +105,9 @@ def _process_browse_channels(provider, context): return False result.extend(v3.response_to_items(provider, context, json_data)) else: - json_data = context.get_function_cache().get(kodion.utils.FunctionCache.ONE_MONTH, client.get_guide_categories) + function_cache = context.get_function_cache() + json_data = function_cache.get(client.get_guide_categories, + function_cache.ONE_MONTH) if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(provider, context, json_data)) @@ -107,7 +116,7 @@ def _process_browse_channels(provider, context): def _process_disliked_videos(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] page_token = context.get_param('page_token', '') @@ -122,7 +131,7 @@ def _process_live_events(provider, context, event_type='live'): def _sort(x): return x.get_aired() - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] # TODO: cache result @@ -142,7 +151,7 @@ def _process_description_links(provider, context): addon_id = context.get_param('addon_id', '') def _extract_urls(_video_id): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) url_resolver = UrlResolver(context) result = [] @@ -156,9 +165,12 @@ def _extract_urls(_video_id): video_data = resource_manager.get_videos([_video_id]) yt_item = video_data[_video_id] snippet = yt_item['snippet'] # crash if not conform - description = kodion.utils.strip_html_from_text(snippet['description']) + description = strip_html_from_text(snippet['description']) - urls = context.get_function_cache().get(kodion.utils.FunctionCache.ONE_WEEK, extract_urls, description) + function_cache = context.get_function_cache() + urls = function_cache.get(extract_urls, + function_cache.ONE_WEEK, + description) progress_dialog.set_total(len(urls)) @@ -261,7 +273,7 @@ def _display_playlists(_playlist_ids): def _process_saved_playlists_tv(provider, context): - provider.set_content_type(context, kodion.constants.content_type.FILES) + provider.set_content_type(context, constants.content_type.FILES) result = [] next_page_token = context.get_param('next_page_token', '') @@ -273,7 +285,7 @@ def _process_saved_playlists_tv(provider, context): def _process_watch_history_tv(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] next_page_token = context.get_param('next_page_token', '') @@ -285,7 +297,7 @@ def _process_watch_history_tv(provider, context): def _process_purchases_tv(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] next_page_token = context.get_param('next_page_token', '') @@ -297,7 +309,7 @@ def _process_purchases_tv(provider, context): def _process_new_uploaded_videos_tv(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] next_page_token = context.get_param('next_page_token', '') @@ -309,7 +321,7 @@ def _process_new_uploaded_videos_tv(provider, context): def _process_new_uploaded_videos_tv_filtered(provider, context): - provider.set_content_type(context, kodion.constants.content_type.VIDEOS) + provider.set_content_type(context, constants.content_type.VIDEOS) result = [] next_page_token = context.get_param('next_page_token', '') @@ -353,4 +365,4 @@ def process(category, provider, context): return _process_child_comments(provider, context) if category == 'saved_playlists': return _process_saved_playlists_tv(provider, context) - raise kodion.KodionException("YouTube special category '%s' not found" % category) + raise KodionException("YouTube special category '%s' not found" % category) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 8eeaab1fc..a7c75ebfc 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -15,7 +15,13 @@ import socket from base64 import b64decode +import xbmcaddon +import xbmcvfs + from .helper import ( + ResourceManager, + UrlResolver, + UrlToItemConverter, v3, yt_context_menu, yt_login, @@ -25,27 +31,15 @@ yt_setup_wizard, yt_specials, yt_video, - ResourceManager, - UrlResolver, - UrlToItemConverter, ) from .youtube_exceptions import InvalidGrant, LoginException -from ..kodion import ( - constants, - AbstractProvider, - RegisterProviderPath, -) -from ..youtube.client import YouTube +from ..kodion import (AbstractProvider, RegisterProviderPath, constants) from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem from ..kodion.network import get_client_ip_address, is_httpd_live -from ..kodion.utils import find_video_id, strip_html_from_text, FunctionCache +from ..kodion.utils import find_video_id, strip_html_from_text +from ..youtube.client import YouTube from ..youtube.helper import yt_subscriptions -import xbmcaddon -import xbmcvfs -import xbmcgui -import xbmcplugin - class Provider(AbstractProvider): def __init__(self): @@ -397,6 +391,7 @@ def _on_channel_live(self, context, re_match): @RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$') def _on_channel(self, context, re_match): + client = self.get_client(context) localize = context.localize create_path = context.create_resource_path create_uri = context.create_uri @@ -424,14 +419,15 @@ def _on_channel(self, context, re_match): result = [] """ - This is a helper routine if we only have the username of a channel. This will retrieve the correct channel id - based on the username. + This is a helper routine if we only have the username of a channel. + This will retrieve the correct channel id based on the username. """ if method == 'user' or channel_id == 'mine': context.log_debug('Trying to get channel id for user "%s"' % channel_id) - json_data = function_cache.get(FunctionCache.ONE_DAY, - self.get_client(context).get_channel_by_username, channel_id) + json_data = function_cache.get(client.get_channel_by_username, + function_cache.ONE_DAY, + channel_id) if not v3.handle_error(context, json_data): return False @@ -490,8 +486,9 @@ def _on_channel(self, context, re_match): playlists = resource_manager.get_related_playlists(channel_id) upload_playlist = playlists.get('uploads', '') if upload_playlist: - json_data = function_cache.get(FunctionCache.ONE_MINUTE * 5, - self.get_client(context).get_playlist_items, upload_playlist, + json_data = function_cache.get(client.get_playlist_items, + function_cache.ONE_MINUTE * 5, + upload_playlist, page_token=page_token) if not v3.handle_error(context, json_data): return False @@ -887,9 +884,16 @@ def on_search(self, search_text, context, re_match): live_item.set_fanart(self.get_fanart(context)) result.append(live_item) - json_data = context.get_function_cache().get(FunctionCache.ONE_MINUTE * 10, self.get_client(context).search, - q=search_text, search_type=search_type, event_type=event_type, - safe_search=safe_search, page_token=page_token, channel_id=channel_id, location=location) + function_cache = context.get_function_cache() + json_data = function_cache.get(self.get_client(context).search, + function_cache.ONE_MINUTE * 10, + q=search_text, + search_type=search_type, + event_type=event_type, + safe_search=safe_search, + page_token=page_token, + channel_id=channel_id, + location=location) if not v3.handle_error(context, json_data): return False result.extend(v3.response_to_items(self, context, json_data)) From e16c7a0581aec2447da7898ecc29b570aff23c65 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:58:15 +1100 Subject: [PATCH 054/141] Rationalise item date and duration methods - use datetime objects for setting rather than parsing back and forth - date and dateadded values stored as datetime rather than str - date and dateadded getter can retrieve value as datetime, date str or datetime.isoformat - aired and premiered values stored as date rather than st - aired and premiered getter can retrieve value as date, or date.isoformat - datetime_parser.parse will always create a datetime object rather than a date object - the caller can determine whether datetime.date() needs to be used after that - duration getter can optionally retrieve value as hh:mm:ss string - listitem label2 now uses the same data as default label mask including date and duration --- .../youtube_plugin/kodion/items/base_item.py | 51 ++++++++++--------- .../youtube_plugin/kodion/items/video_item.py | 46 +++++++++-------- .../kodion/ui/xbmc/info_labels.py | 27 +++++----- .../kodion/ui/xbmc/xbmc_items.py | 33 +++++++++--- .../youtube_plugin/kodion/utils/__init__.py | 2 + .../kodion/utils/datetime_parser.py | 6 +-- .../youtube_plugin/kodion/utils/methods.py | 11 ++++ .../kodion/utils/watch_later_list.py | 3 +- 8 files changed, 107 insertions(+), 72 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 92d70650a..49da304e7 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -104,41 +104,44 @@ def replace_context_menu(self): return self._replace_context_menu def set_date(self, year, month, day, hour=0, minute=0, second=0): - date = datetime.datetime(year, month, day, hour, minute, second) - self._date = date.isoformat(sep=' ') + self._date = datetime.datetime(year, month, day, hour, minute, second) def set_date_from_datetime(self, date_time): - self.set_date(year=date_time.year, - month=date_time.month, - day=date_time.day, - hour=date_time.hour, - minute=date_time.minute, - second=date_time.second) + self._date = date_time + + def get_date(self, as_text=True, short=False): + if not self._date: + return '' + if short: + return self._date.date().isoformat() + if as_text: + return self._date.isoformat(sep=' ') + return self._date def set_dateadded(self, year, month, day, hour=0, minute=0, second=0): - date = datetime.datetime(year, month, day, hour, minute, second) - self._dateadded = date.isoformat(sep=' ') + self._dateadded = datetime.datetime(year, + month, + day, + hour, + minute, + second) def set_dateadded_from_datetime(self, date_time): - self.set_dateadded(year=date_time.year, - month=date_time.month, - day=date_time.day, - hour=date_time.hour, - minute=date_time.minute, - second=date_time.second) + self._dateadded = date_time + + def get_dateadded(self, as_text=True): + if not self._dateadded: + return '' + if as_text: + return self._dateadded.isoformat(sep=' ') + return self._dateadded - def set_added_utc(self, dt): - self._added_utc = dt + def set_added_utc(self, date_time): + self._added_utc = date_time def get_added_utc(self): return self._added_utc - def get_date(self): - return self._date - - def get_dateadded(self): - return self._dateadded - def get_short_details(self): return self._short_details diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index ba80baf5a..35521f098 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -13,7 +13,7 @@ from html import unescape from .base_item import BaseItem -from ..utils import duration_to_seconds +from ..utils import duration_to_seconds, seconds_to_duration __RE_IMDB__ = re.compile(r'(http(s)?://)?www.imdb.(com|de)/title/(?P[t0-9]+)(/)?') @@ -107,15 +107,16 @@ def get_year(self): return self._year def set_premiered(self, year, month, day): - date = datetime.date(year, month, day) - self._premiered = date.isoformat() + self._premiered = datetime.date(year, month, day) def set_premiered_from_datetime(self, date_time): - self.set_premiered(year=date_time.year, - month=date_time.month, - day=date_time.day) + self._premiered = date_time.date() - def get_premiered(self): + def get_premiered(self, as_text=True): + if not self._premiered: + return '' + if as_text: + return self._premiered.isoformat() return self._premiered def set_plot(self, plot): @@ -175,29 +176,35 @@ def set_duration(self, hours=0, minutes=0, seconds=0, duration=''): if duration: _seconds = duration_to_seconds(duration) else: - _seconds = seconds + minutes * 60 + hours * 60 * 60 - self.set_duration_from_seconds(_seconds) + _seconds = seconds + minutes * 60 + hours * 3600 + self._duration = _seconds or 0 def set_duration_from_minutes(self, minutes): - self.set_duration_from_seconds(int(minutes) * 60) + self._duration = int(minutes) * 60 def set_duration_from_seconds(self, seconds): self._duration = int(seconds or 0) - def get_duration(self): + def get_duration(self, as_text=False): + if as_text: + return seconds_to_duration(self._duration) return self._duration def set_aired(self, year, month, day): - date = datetime.date(year, month, day) - self._aired = date.isoformat() + self._aired = datetime.date(year, month, day) def set_aired_from_datetime(self, date_time): - self.set_aired(year=date_time.year, - month=date_time.month, - day=date_time.day) + self._aired = date_time.date() - def set_scheduled_start_utc(self, dt): - self._scheduled_start_utc = dt + def get_aired(self, as_text=True): + if not self._aired: + return '' + if as_text: + return self._aired.isoformat() + return self._aired + + def set_scheduled_start_utc(self, date_time): + self._scheduled_start_utc = date_time def get_scheduled_start_utc(self): return self._scheduled_start_utc @@ -218,9 +225,6 @@ def upcoming(self): def upcoming(self, value): self._upcoming = value - def get_aired(self): - return self._aired - def set_genre(self, genre): self._genre = genre diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index e78d98fc4..767f34d5a 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -12,10 +12,14 @@ from ...items import AudioItem, DirectoryItem, ImageItem, VideoItem -def _process_date(info_labels, param): +def _process_date_value(info_labels, name, param): + if param: + info_labels[name] = param.isoformat() + + +def _process_datetime_value(info_labels, name, param): if param: - datetime = utils.datetime_parser.parse(param) - info_labels['date'] = datetime.isoformat() + info_labels[name] = param.isoformat(' ') def _process_int_value(info_labels, name, param): @@ -43,11 +47,6 @@ def _process_audio_rating(info_labels, param): info_labels['rating'] = rating -def _process_video_dateadded(info_labels, param): - if param is not None and param: - info_labels['dateadded'] = param - - def _process_video_duration(info_labels, param): if param is not None: info_labels['duration'] = '%d' % param @@ -63,7 +62,7 @@ def _process_video_rating(info_labels, param): info_labels['rating'] = rating -def _process_date_value(info_labels, name, param): +def _process_date_string(info_labels, name, param): if param: date = utils.datetime_parser.parse(param) info_labels[name] = date.isoformat() @@ -89,8 +88,8 @@ def _process_last_played(info_labels, name, param): def create_from_item(base_item): info_labels = {} - # 'date' = '1982-03-09' - _process_date(info_labels, base_item.get_date()) + # 'date' = '1982-03-09' (string) + _process_datetime_value(info_labels, 'date', base_item.get_date(as_text=False)) # 'count' = 12 (integer) # Can be used to store an id for later, or for sorting purposes @@ -135,7 +134,7 @@ def create_from_item(base_item): _process_list_value(info_labels, 'artist', base_item.get_artist()) # 'dateadded' = '2014-08-11 13:08:56' (string) will be taken from 'dateadded' - _process_video_dateadded(info_labels, base_item.get_dateadded()) + _process_datetime_value(info_labels, 'dateadded', base_item.get_dateadded(as_text=False)) # TODO: starting with Helix this could be seconds # 'duration' = '3:18' (string) @@ -147,13 +146,13 @@ def create_from_item(base_item): _process_video_rating(info_labels, base_item.get_rating()) # 'aired' = '2013-12-12' (string) - _process_date_value(info_labels, 'aired', base_item.get_aired()) + _process_date_value(info_labels, 'aired', base_item.get_aired(as_text=False)) # 'director' = 'Steven Spielberg' (string) _process_string_value(info_labels, 'director', base_item.get_director()) # 'premiered' = '2013-12-12' (string) - _process_date_value(info_labels, 'premiered', base_item.get_premiered()) + _process_date_value(info_labels, 'premiered', base_item.get_premiered(as_text=False)) # 'episode' = 12 (int) _process_int_value(info_labels, 'episode', base_item.get_episode()) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index 95c51a123..01b73f171 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -54,13 +54,24 @@ def video_playback_item(context, video_item): is_strm = context.get_param('strm') mime_type = None - kwargs = { - 'label': (None if is_strm - else (video_item.get_title() or video_item.get_name())), - 'label2': None if is_strm else video_item.get_short_details(), - 'path': uri, - 'offscreen': True, - } + if is_strm: + kwargs = { + 'path': uri, + 'offscreen': True, + } + else: + kwargs = { + 'label': video_item.get_title() or video_item.get_name(), + 'label2': ' | '.join((part + for part in ( + video_item.get_code(), + video_item.get_date(short=True), + video_item.get_duration(as_text=True), + ) + if part)), + 'path': uri, + 'offscreen': True, + } props = { 'isPlayable': str(video_item.playable).lower(), } @@ -193,7 +204,13 @@ def video_listitem(context, video_item): kwargs = { 'label': video_item.get_title() or video_item.get_name(), - 'label2': video_item.get_short_details(), + 'label2': ' | '.join((part + for part in ( + video_item.get_code(), + video_item.get_date(short=True), + video_item.get_duration(as_text=True), + ) + if part)), 'path': uri, 'offscreen': True, } diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 763a92912..0cd6e0e08 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -18,6 +18,7 @@ friendly_number, loose_version, make_dirs, + seconds_to_duration, select_stream, strip_html_from_text, to_str, @@ -46,6 +47,7 @@ 'friendly_number', 'loose_version', 'make_dirs', + 'seconds_to_duration', 'select_stream', 'strip_html_from_text', 'to_str', diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index a9d0cb29d..1f6c4ea33 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -55,9 +55,9 @@ def _to_int(value): date_only_match = __RE_MATCH_DATE_ONLY__.match(datetime_string) if date_only_match: return utc_to_local( - dt=date(_to_int(date_only_match.group('year')), - _to_int(date_only_match.group('month')), - _to_int(date_only_match.group('day'))), + dt=datetime(_to_int(date_only_match.group('year')), + _to_int(date_only_match.group('month')), + _to_int(date_only_match.group('day'))), offset=offset ) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index b5482e6dd..e17d50b42 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -11,6 +11,7 @@ import os import copy import re +from datetime import timedelta from math import floor, log from urllib.parse import quote @@ -27,6 +28,7 @@ 'loose_version', 'make_dirs', 'print_items', + 'seconds_to_duration', 'select_stream', 'strip_html_from_text', 'to_str', @@ -280,7 +282,16 @@ def friendly_number(number, precision=3, scale=('', 'K', 'M', 'B')): def duration_to_seconds(duration): + if ':' in duration: + seconds = 0 + for part in duration.split(':'): + seconds = seconds * 60 + int(part, 10) + return seconds return sum( int(number) * _SECONDS_IN_PERIODS[period] for number, period in re.findall(_RE_PERIODS, duration) ) + + +def seconds_to_duration(seconds): + return str(timedelta(seconds=seconds)) diff --git a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py b/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py index e6a570960..20425d513 100644 --- a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py @@ -30,8 +30,7 @@ def get_items(self): return sorted(result, key=self._sort_item, reverse=False) def add(self, base_item): - now = datetime.datetime.now() - base_item.set_date(now.year, now.month, now.day, now.hour, now.minute, now.second) + base_item.set_date_from_datetime(datetime.datetime.now()) item_json_data = items.to_json(base_item) self._set(base_item.get_id(), item_json_data) From d11f9579449f3d6b4553755e9c8753abd055984e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:44:34 +1100 Subject: [PATCH 055/141] Add support for start and end parameters --- .../kodion/context/abstract_context.py | 2 + .../kodion/ui/xbmc/xbmc_items.py | 6 +- .../lib/youtube_plugin/kodion/utils/player.py | 65 +++++++++++++------ .../youtube_plugin/youtube/helper/yt_play.py | 41 ++++++------ 4 files changed, 74 insertions(+), 40 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index fc2595f18..d005f9c97 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -55,6 +55,8 @@ class AbstractContext(object): } _FLOAT_PARAMS = { 'seek', + 'start', + 'end' } _LIST_PARAMS = { 'channel_ids', diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index 01b73f171..0799f54cf 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -127,7 +127,11 @@ def video_playback_item(context, video_item): return list_item if not context.get_param('resume'): - if 'ResumeTime' in props: + if context.get_param('start'): + prop_value = video_item.get_start_time() + if prop_value: + props['ResumeTime'] = prop_value + elif 'ResumeTime' in props: del props['ResumeTime'] prop_value = video_item.get_duration() diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index de21377ea..8b77302ca 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -15,7 +15,7 @@ class PlaybackMonitorThread(threading.Thread): - def __init__(self, provider, context, playback_json): + def __init__(self, player, provider, context, playback_json): super(PlaybackMonitorThread, self).__init__() self._stopped = threading.Event() @@ -25,7 +25,7 @@ def __init__(self, provider, context, playback_json): self.provider = provider self.ui = self.context.get_ui() - self.player = xbmc.Player() + self.player = player self.playback_json = playback_json self.video_id = self.playback_json.get('video_id') @@ -56,10 +56,6 @@ def run(self): use_local_history = self.playback_json.get('use_local_history', False) playback_stats = self.playback_json.get('playback_stats') refresh_only = self.playback_json.get('refresh_only', False) - try: - seek_time = float(self.playback_json.get('seek_time')) - except (ValueError, TypeError): - seek_time = None player = self.player @@ -160,14 +156,20 @@ def run(self): self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) break - if seek_time: - player.seekTime(seek_time) - try: - self.current_time = float(player.getTime()) - except RuntimeError: - pass - if self.current_time >= seek_time: - seek_time = None + if player._start_time or player._seek_time: + _seek_time = player._start_time or player._seek_time + if self.current_time < _seek_time: + player.seekTime(_seek_time) + try: + self.current_time = float(player.getTime()) + except RuntimeError: + pass + + if player._end_time and self.current_time >= player._end_time: + if player._start_time: + player.seekTime(player._start_time) + else: + player.stop() if self.abort_now(): self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) @@ -359,6 +361,9 @@ def __init__(self, *args, **kwargs): self.provider = kwargs.get('provider') self.ui = self.context.get_ui() self.threads = [] + self._seek_time = None + self._start_time = None + self._end_time = None def stop_threads(self): for thread in self.threads: @@ -400,11 +405,25 @@ def cleanup_threads(self, only_ended=True): def onPlayBackStarted(self): playback_json = self.ui.get_property('playback_json') - if playback_json: - playback_json = json.loads(playback_json) - self.ui.clear_property('playback_json') - self.cleanup_threads() - self.threads.append(PlaybackMonitorThread(self.provider, self.context, playback_json)) + if not playback_json: + return + + playback_json = json.loads(playback_json) + try: + self._seek_time = float(playback_json.get('seek_time')) + self._start_time = float(playback_json.get('start_time')) + self._end_time = float(playback_json.get('end_time')) + except (ValueError, TypeError): + self._seek_time = None + self._start_time = None + self._end_time = None + + self.ui.clear_property('playback_json') + self.cleanup_threads() + self.threads.append(PlaybackMonitorThread(self, + self.provider, + self.context, + playback_json)) def onPlayBackEnded(self): self.stop_threads() @@ -415,3 +434,11 @@ def onPlayBackStopped(self): def onPlayBackError(self): self.onPlayBackEnded() + + def onPlayBackSeek(self, time, seekOffset): + time_s = time // 1000 + self._seek_time = None + if ((self._end_time and time_s > self._end_time) + or (self._start_time and time_s < self._start_time)): + self._start_time = None + self._end_time = None diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 1df2af008..ea7daf631 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -93,30 +93,31 @@ def play_video(provider, context): utils.update_play_info(provider, context, video_id, video_item, video_stream, use_play_data=use_play_data) - seek_time = 0.0 - play_count = 0 - playback_stats = video_stream.get('playback_stats') + seek_time = 0.0 if params.get('resume') else params.get('seek', 0.0) + start_time = params.get('start', 0.0) + end_time = params.get('end', 0.0) - if not params.get('resume'): - try: - seek_time = params.get('seek', 0.0) - except (ValueError, TypeError): - pass + if start_time: + video_item.set_start_time(start_time) + if end_time: + video_item.set_duration_from_seconds(end_time) - if use_play_data: - play_count = video_item.get_play_count() or 0 + play_count = use_play_data and video_item.get_play_count() or 0 + playback_stats = video_stream.get('playback_stats') playback_json = { - "video_id": video_id, - "channel_id": metadata.get('channel', {}).get('id', ''), - "video_status": metadata.get('video', {}).get('status', {}), - "playing_file": video_item.get_uri(), - "play_count": play_count, - "use_remote_history": use_remote_history, - "use_local_history": use_play_data, - "playback_stats": playback_stats, - "seek_time": seek_time, - "refresh_only": screensaver + 'video_id': video_id, + 'channel_id': metadata.get('channel', {}).get('id', ''), + 'video_status': metadata.get('video', {}).get('status', {}), + 'playing_file': video_item.get_uri(), + 'play_count': play_count, + 'use_remote_history': use_remote_history, + 'use_local_history': use_play_data, + 'playback_stats': playback_stats, + 'seek_time': seek_time, + 'start_time': start_time, + 'end_time': end_time, + 'refresh_only': screensaver } ui.set_property('playback_json', json.dumps(playback_json)) From b3bbd5c6564e9c2f55439a7754c598d714b92ace Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:47:22 +1100 Subject: [PATCH 056/141] Update live status based on current video details - Partially fix #540 - Also properly select protocol type for InputStream Helper --- .../youtube_plugin/youtube/helper/utils.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index fd29f4b3d..35a58ff17 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -657,6 +657,7 @@ def update_play_info(provider, context, video_id, video_item, video_stream, meta_data = video_stream.get('meta', None) if meta_data: + video_item.live = meta_data.get('status', {}).get('live', False) video_item.set_subtitles(meta_data.get('subtitles', None)) image = get_thumbnail(settings.use_thumbnail_size(), meta_data.get('images', {})) @@ -674,17 +675,19 @@ def update_play_info(provider, context, video_id, video_item, video_stream, elif video_item.use_hls_video() or video_item.use_mpd_video(): video_item.set_isa_video(settings.use_isa()) - license_info = video_stream.get('license_info', {}) - license_proxy = license_info.get('proxy', '') - license_url = license_info.get('url', '') - license_token = license_info.get('token', '') + if video_item.use_isa_video(): + license_info = video_stream.get('license_info', {}) + license_proxy = license_info.get('proxy', '') + license_url = license_info.get('url', '') + license_token = license_info.get('token', '') - if ISHelper and license_proxy and license_url and license_token: - ISHelper('mpd', drm='com.widevine.alpha').check_inputstream() + if ISHelper and license_proxy and license_url and license_token: + ISHelper('mpd' if video_item.use_mpd_video() else 'hls', + drm='com.widevine.alpha').check_inputstream() - video_item.set_license_key(license_proxy) - ui.set_property('license_url', license_url) - ui.set_property('license_token', license_token) + video_item.set_license_key(license_proxy) + ui.set_property('license_url', license_url) + ui.set_property('license_token', license_token) def update_fanarts(provider, context, channel_items_dict, data=None): From 72480b5b5c606dd3f4540d94312fa9e996a31e9d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 6 Dec 2023 11:49:21 +1100 Subject: [PATCH 057/141] Support Youtube Clips - Closes #450 - Support following plugin url formats: - plugin://plugin.video.youtube/uri2addon/?uri=https://www.youtube.com/clip/ - plugin://plugin.video.youtube/play/?video_id=&start=&end=&clip=true - optional clip parameter will loop the clip between start_time and end_time, otherwise video will stop after end_time is reached --- .../kodion/context/abstract_context.py | 1 + .../youtube_plugin/kodion/utils/methods.py | 10 ++- .../lib/youtube_plugin/kodion/utils/player.py | 11 ++- .../youtube/helper/url_resolver.py | 53 ++++++++++- .../youtube/helper/url_to_item_converter.py | 90 +++++++++---------- .../youtube_plugin/youtube/helper/yt_play.py | 7 +- 6 files changed, 112 insertions(+), 60 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index d005f9c97..497e949c9 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -32,6 +32,7 @@ class AbstractContext(object): 'ask_for_quality', 'audio_only', 'confirmed', + 'clip', 'enable', 'hide_folders', 'hide_live', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index e17d50b42..a88e2475b 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -272,8 +272,9 @@ def friendly_number(number, precision=3, scale=('', 'K', 'M', 'B')): ).rstrip('0').rstrip('.') + scale[magnitude], _input -_RE_PERIODS = re.compile(r'(\d+)(d|h|m|s)') +_RE_PERIODS = re.compile(r'([\d.]+)(d|h|m|s|$)') _SECONDS_IN_PERIODS = { + '': 1, # 1 second for unitless period 's': 1, # 1 second 'm': 60, # 1 minute 'h': 3600, # 1 hour @@ -285,11 +286,12 @@ def duration_to_seconds(duration): if ':' in duration: seconds = 0 for part in duration.split(':'): - seconds = seconds * 60 + int(part, 10) + seconds = seconds * 60 + (float(part) if '.' in part else int(part)) return seconds return sum( - int(number) * _SECONDS_IN_PERIODS[period] - for number, period in re.findall(_RE_PERIODS, duration) + (float(number) if '.' in number else int(number)) + * _SECONDS_IN_PERIODS.get(period, 1) + for number, period in re.findall(_RE_PERIODS, duration.lower()) ) diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index 8b77302ca..1481d932f 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -56,6 +56,7 @@ def run(self): use_local_history = self.playback_json.get('use_local_history', False) playback_stats = self.playback_json.get('playback_stats') refresh_only = self.playback_json.get('refresh_only', False) + clip = self.playback_json.get('clip', False) player = self.player @@ -166,7 +167,7 @@ def run(self): pass if player._end_time and self.current_time >= player._end_time: - if player._start_time: + if clip and player._start_time: player.seekTime(player._start_time) else: player.stop() @@ -403,7 +404,10 @@ def cleanup_threads(self, only_ended=True): ', '.join([thread.video_id for thread in active_threads])) self.threads = active_threads - def onPlayBackStarted(self): + def onAVStarted(self): + if not self.ui.busy_dialog_active(): + self.ui.clear_property('busy') + playback_json = self.ui.get_property('playback_json') if not playback_json: return @@ -426,6 +430,9 @@ def onPlayBackStarted(self): playback_json)) def onPlayBackEnded(self): + if not self.ui.busy_dialog_active(): + self.ui.clear_property('busy') + self.stop_threads() self.cleanup_threads() diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index bed917bd6..3033bf834 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -9,6 +9,7 @@ """ import re +from html import unescape from urllib.parse import parse_qsl, urlencode, urlparse from ...kodion.network import BaseRequestsClass @@ -51,6 +52,15 @@ def resolve(self, url, url_components): class YouTubeResolver(AbstractResolver): + _RE_CHANNEL_URL = re.compile(r'') + _RE_CLIP_DETAILS = re.compile(r'()' + r'|("startTimeMs":"(?P\d+)")' + r'|("endTimeMs":"(?P\d+)")') + def __init__(self, *args, **kwargs): super(YouTubeResolver, self).__init__(*args, **kwargs) @@ -67,6 +77,7 @@ def supports_url(self, url, url_components): '/@', '/c/', '/channel/', + '/clip', '/user/', )): return 'GET' @@ -118,13 +129,47 @@ def resolve(self, url, url_components, method='HEAD'): if response.status_code != 200: return url + if path.startswith('/clip'): + all_matches = self._RE_CLIP_DETAILS.finditer(response.text) + num_matched = 0 + url_components = params = start_time = end_time = None + for matches in all_matches: + matches = matches.groupdict() + + if not num_matched & 1: + url = matches['video_url'] + if url: + num_matched += 1 + url_components = urlparse(unescape(url)) + params = dict(parse_qsl(url_components.query)) + + if not num_matched & 2: + start_time = matches['start_time'] + if start_time: + start_time = int(start_time) / 1000 + num_matched += 2 + + if not num_matched & 4: + end_time = matches['end_time'] + if end_time: + end_time = int(end_time) / 1000 + num_matched += 4 + + if num_matched != 7: + continue + + params.update({ + 'clip': True, + 'start': start_time, + 'end': end_time, + }) + return url_components._replace(query=urlencode(params)).geturl() + # we try to extract the channel id from the html content # With the channel id we can construct a URL we already work with # https://www.youtube.com/channel/ - if method == 'GET': - match = re.search( - r'', - response.text) + elif method == 'GET': + match = self._RE_CHANNEL_URL.search(response.text) if match: return match.group('channel_url') diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index 536e0e0d1..6b564163d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -13,11 +13,11 @@ from . import utils from ...kodion.items import DirectoryItem, UriItem, VideoItem +from ...kodion.utils import duration_to_seconds class UrlToItemConverter(object): RE_PATH_ID = re.compile(r'/\w+/(?P[^/?#]+)', re.I) - RE_SEEK_TIME = re.compile(r'\d+') VALID_HOSTNAMES = { 'youtube.com', 'www.youtube.com', @@ -47,73 +47,67 @@ def add_url(self, url, provider, context): return url_params = dict(parse_qsl(parsed_url.query)) - path = parsed_url.path.lower() - - channel_id = live = playlist_id = seek_time = video_id = None - if path == '/watch': - video_id = url_params.get('v') - playlist_id = url_params.get('list') - seek_time = url_params.get('t') - elif path == '/playlist': - playlist_id = url_params.get('list') + new_params = { + new: process(url_params[old]) if process else url_params[old] + for old, new, process in ( + ('end', 'end', duration_to_seconds), + ('start', 'start', duration_to_seconds), + ('t', 'seek', duration_to_seconds), + ('list', 'playlist_id', False), + ('v', 'video_id', False), + ('live', 'live', False), + ('clip', 'clip', False), + ) + if old in url_params + } + + path = parsed_url.path.rstrip('/').lower() + channel_id = video_id = None + if path.startswith(('/playlist', '/watch')): + pass elif path.startswith('/channel/'): re_match = self.RE_PATH_ID.match(parsed_url.path) channel_id = re_match.group('id') - if '/live' in path: - live = 1 - elif path.startswith(('/live/', '/shorts/')): + if path.endswith(('/live', '/streams')): + new_params['live'] = 1 + elif path.startswith(('/clip/', '/embed/', '/live/', '/shorts/')): re_match = self.RE_PATH_ID.match(parsed_url.path) - video_id = re_match.group('id') - seek_time = url_params.get('t') + new_params['video_id'] = re_match.group('id') else: context.log_debug('Unknown path "{0}" in url "{1}"'.format( parsed_url.path, url )) return - if video_id: - plugin_params = { - 'video_id': video_id, - } - if seek_time: - seek_time = sum( - int(number) * seconds_per_unit - for number, seconds_per_unit in zip( - reversed(re.findall(self.RE_SEEK_TIME, seek_time)), - (1, 60, 3600, 86400) - ) - if number - ) - plugin_params['seek'] = seek_time - if playlist_id: - plugin_params['playlist_id'] = playlist_id + if 'video_id' in new_params: + video_id = new_params['video_id'] video_item = VideoItem( - '', context.create_uri(['play'], plugin_params) + '', context.create_uri(['play'], new_params) ) self._video_id_dict[video_id] = video_item - elif playlist_id: + elif 'playlist_id' in new_params: + playlist_id = new_params['playlist_id'] if self._flatten: self._playlist_ids.append(playlist_id) - else: - playlist_item = DirectoryItem( - '', context.create_uri(['playlist', playlist_id]) - ) - playlist_item.set_fanart(provider.get_fanart(context)) - self._playlist_id_dict[playlist_id] = playlist_item + return + + playlist_item = DirectoryItem( + '', context.create_uri(['playlist', playlist_id]), new_params + ) + playlist_item.set_fanart(provider.get_fanart(context)) + self._playlist_id_dict[playlist_id] = playlist_item elif channel_id: if self._flatten: - if live: - context.set_param('live', live) self._channel_ids.append(channel_id) - else: - channel_item = DirectoryItem( - '', context.create_uri(['channel', channel_id], - {'live': live} if live else None) - ) - channel_item.set_fanart(provider.get_fanart(context)) - self._channel_id_dict[channel_id] = channel_item + return + + channel_item = DirectoryItem( + '', context.create_uri(['channel', channel_id], new_params) + ) + channel_item.set_fanart(provider.get_fanart(context)) + self._channel_id_dict[channel_id] = channel_item else: context.log_debug('No items found in url "{0}"'.format(url)) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index ea7daf631..130e7d1e0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -99,8 +99,10 @@ def play_video(provider, context): if start_time: video_item.set_start_time(start_time) - if end_time: - video_item.set_duration_from_seconds(end_time) + # Setting the duration based on end_time can cause issues with + # listing/sorting and other addons that monitor playback + # if end_time: + # video_item.set_duration_from_seconds(end_time) play_count = use_play_data and video_item.get_play_count() or 0 playback_stats = video_stream.get('playback_stats') @@ -117,6 +119,7 @@ def play_video(provider, context): 'seek_time': seek_time, 'start_time': start_time, 'end_time': end_time, + 'clip': params.get('clip'), 'refresh_only': screensaver } From a32d39393bdc5ca1fb47f7bb017f3f740863ce9a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:42:05 +1100 Subject: [PATCH 058/141] Workaround Kodi crash due to multiple busy dialogs - Workaround crashes in #540, #113 - Won't fix all busy dialog crashes due to how Kodi resolves plugin urls, but should fix most common crashes --- .../kodion/context/xbmc/xbmc_context.py | 4 +- .../kodion/player/xbmc/xbmc_playlist.py | 52 ++++++++++++++----- .../kodion/plugin/xbmc/xbmc_runner.py | 31 +++++++++-- .../kodion/ui/xbmc/xbmc_context_ui.py | 7 +++ .../youtube/helper/yt_playlist.py | 4 +- .../youtube_plugin/youtube/helper/yt_video.py | 2 +- .../lib/youtube_plugin/youtube/provider.py | 6 +-- 7 files changed, 81 insertions(+), 25 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 9ea2bbe4c..a95f6a431 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -298,8 +298,8 @@ def get_region(self): def addon(self): return self._addon - def is_plugin_path(self, uri, uri_path): - return uri.startswith('plugin://%s/%s/' % (self.get_id(), uri_path)) + def is_plugin_path(self, uri, uri_path=''): + return uri.startswith('plugin://%s/%s' % (self.get_id(), uri_path)) @staticmethod def format_date_short(date_obj, short_isoformat=False): diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py index 69c603e04..bc171be58 100644 --- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py @@ -12,6 +12,7 @@ import xbmc from ..abstract_playlist import AbstractPlaylist +from ...items import VideoItem from ...ui.xbmc import xbmc_items @@ -43,24 +44,25 @@ def unshuffle(self): def size(self): return self._playlist.size() - def get_items(self): - rpc_request = json.dumps( - { - "jsonrpc": "2.0", - "method": "Playlist.GetItems", - "params": { - "properties": ["title", "file"], - "playlistid": self._playlist.getPlayListId() - }, - "id": 1 - }) + def get_items(self, properties=None, dumps=False): + rpc_request = json.dumps({ + 'jsonrpc': '2.0', + 'method': 'Playlist.GetItems', + 'params': { + 'properties': properties if properties else ['title', 'file'], + 'playlistid': self._playlist.getPlayListId() + }, + 'id': 1 + }) response = json.loads(xbmc.executeJSONRPC(rpc_request)) if 'result' in response: if 'items' in response['result']: - return response['result']['items'] - return [] + result = response['result']['items'] + else: + result = [] + return json.dumps(result) if dumps else result if 'error' in response: message = response['error']['message'] @@ -69,4 +71,26 @@ def get_items(self): else: error = 'Requested |%s| received error |%s|' % (rpc_request, str(response)) self._context.log_debug(error) - return [] + return '[]' if dumps else [] + + def add_items(self, items, loads=False): + if loads: + items = json.loads(items) + + # Playlist.GetItems allows retrieving full playlist item details, but + # Playlist.Add only allows for file/path/id etc. + # Have to add items individually rather than using JSON-RPC + + for item in items: + self.add(VideoItem(item.get('title', ''), item['file'])) + + # rpc_request = json.dumps({ + # 'jsonrpc': '2.0', + # 'method': 'Playlist.Add', + # 'params': { + # 'playlistid': self._playlist.getPlayListId(), + # 'item': items, + # }, + # 'id': 1 + # }) + # response = json.loads(xbmc.executeJSONRPC(rpc_request)) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index 37fe89365..c4b178b6c 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -14,6 +14,7 @@ from ..abstract_provider_runner import AbstractProviderRunner from ...exceptions import KodionException from ...items import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem +from ...player import Playlist from ...ui.xbmc import info_labels, xbmc_items @@ -24,8 +25,24 @@ def __init__(self): self.settings = None def run(self, provider, context): - self.handle = context.get_handle() + ui = context.get_ui() + + if ui.get_property('busy').lower() == 'true': + ui.clear_property('busy') + if ui.busy_dialog_active(): + playlist = Playlist('video', context) + playlist.clear() + + xbmcplugin.endOfDirectory(self.handle, succeeded=False) + + items = ui.get_property('playlist') + if items: + ui.clear_property('playlist') + context.log_error('Multiple busy dialogs active - playlist' + ' reloading to prevent Kodi crashing') + playlist.add_items(items, loads=True) + return False try: results = provider.navigate(context) @@ -80,17 +97,25 @@ def run(self, provider, context): return succeeded def _set_resolved_url(self, context, base_item): + uri = base_item.get_uri() + if base_item.playable: + ui = context.get_ui() + if not context.is_plugin_path(uri) and ui.busy_dialog_active(): + ui.set_property('busy', 'true') + playlist = Playlist('video', context) + ui.set_property('playlist', playlist.get_items(dumps=True)) + item = xbmc_items.to_playback_item(context, base_item) xbmcplugin.setResolvedUrl(self.handle, succeeded=True, listitem=item) return True - uri = base_item.get_uri() - if uri.startswith('plugin://'): + if context.is_plugin_path(uri): context.log_debug('Redirecting to |{0}|'.format(uri)) context.execute('RunPlugin({0})'.format(uri)) + xbmcplugin.endOfDirectory(self.handle, succeeded=False, updateListing=False, diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index fb47f7684..032380c87 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -232,3 +232,10 @@ def set_focus_next_item(self): self._context.execute('SetFocus(%s,%s)' % (cid, str(current_position))) except ValueError: pass + + @staticmethod + def busy_dialog_active(): + dialog_id = xbmcgui.getCurrentWindowDialogId() + if dialog_id == 10160 or dialog_id == 10138: + return dialog_id + return False diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 2ecec12be..4d639a6d4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -27,7 +27,7 @@ def _process_add_video(provider, context, keymap_action=False): video_id = context.get_param('video_id', '') if not video_id: - if context.is_plugin_path(listitem_path, 'play'): + if context.is_plugin_path(listitem_path, 'play/'): video_id = kodion.utils.find_video_id(listitem_path) keymap_action = True if not video_id: @@ -141,7 +141,7 @@ def _process_select_playlist(provider, context): video_id = context.get_param('video_id', '') if not video_id: - if context.is_plugin_path(listitem_path, 'play'): + if context.is_plugin_path(listitem_path, 'play/'): video_id = kodion.utils.find_video_id(listitem_path) if video_id: context.set_param('video_id', video_id) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index b1362406f..ecc4a32d5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -25,7 +25,7 @@ def _process_rate_video(provider, context, re_match): try: video_id = re_match.group('video_id') except IndexError: - if context.is_plugin_path(listitem_path, 'play'): + if context.is_plugin_path(listitem_path, 'play/'): video_id = kodion.utils.find_video_id(listitem_path) if not video_id: diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index a7c75ebfc..fa87b85bb 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -566,14 +566,14 @@ def _on_my_location(self, context, re_match): @RegisterProviderPath('^/play/$') def on_play(self, context, re_match): ui = context.get_ui() - path = ui.get_info_label('Container.ListItem(0).FileNameAndPath') redirect = False params = context.get_params() if ({'channel_id', 'live', 'playlist_id', 'playlist_ids', 'video_id'} .isdisjoint(params.keys())): - if context.is_plugin_path(path, 'play'): + path = ui.get_info_label('Container.ListItem(0).FileNameAndPath') + if context.is_plugin_path(path, 'play/'): video_id = find_video_id(path) if video_id: context.set_param('video_id', video_id) @@ -626,7 +626,7 @@ def on_play(self, context, re_match): if builtin: context.execute(builtin.format( - context.create_uri(['play'], {'video_id': video_id}) + context.create_uri(['play'], params) )) return False return yt_play.play_video(self, context) From f91cfae9ed68ce8fefb1779a2dd5d55d13f72106 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:28:04 +1100 Subject: [PATCH 059/141] Tidy up and reformat - store context as _context instance variable - change YoutubePlayer time variables to have non-protected names - seek_time, start_time, end_time - handle watch history logging in update_watch_history --- .../lib/youtube_plugin/kodion/utils/player.py | 290 +++++++++++------- .../youtube_plugin/youtube/client/youtube.py | 20 +- .../youtube/helper/subtitles.py | 36 +-- 3 files changed, 214 insertions(+), 132 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index 1481d932f..1e0e40ab9 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -21,9 +21,9 @@ def __init__(self, player, provider, context, playback_json): self._stopped = threading.Event() self._ended = threading.Event() - self.context = context + self._context = context self.provider = provider - self.ui = self.context.get_ui() + self.ui = self._context.get_ui() self.player = player @@ -40,14 +40,20 @@ def __init__(self, player, provider, context, playback_json): self.daemon = True self.start() - def update_times(self, total_time, current_time, segment_start, percent_complete): + def update_times(self, + total_time, + current_time, + segment_start, + percent_complete): self.total_time = total_time self.current_time = current_time self.segment_start = segment_start self.percent_complete = percent_complete def abort_now(self): - return not self.player.isPlaying() or self.context.abort_requested() or self.stopped() + return (not self.player.isPlaying() + or self._context.abort_requested() + or self.stopped()) def run(self): playing_file = self.playback_json.get('playing_file') @@ -60,10 +66,11 @@ def run(self): player = self.player - self.context.log_debug('PlaybackMonitorThread[%s]: Starting...' % self.video_id) - access_manager = self.context.get_access_manager() + self._context.log_debug('PlaybackMonitorThread[{0}]: Starting' + .format(self.video_id)) + access_manager = self._context.get_access_manager() - settings = self.context.get_settings() + settings = self._context.get_settings() if playback_stats is None: playback_stats = {} @@ -83,8 +90,8 @@ def run(self): report_url = playback_stats.get('playback_url', '') - while not player.isPlaying() and not self.context.abort_requested(): - self.context.log_debug('Waiting for playback to start') + while not player.isPlaying() and not self._context.abort_requested(): + self._context.log_debug('Waiting for playback to start') xbmc.sleep(int(np_wait_time * 1000)) if np_waited >= 5: @@ -93,12 +100,18 @@ def run(self): np_waited += np_wait_time - client = self.provider.get_client(self.context) + client = self.provider.get_client(self._context) is_logged_in = self.provider.is_logged_in() if is_logged_in and report_url and use_remote_history: - client.update_watch_history(self.video_id, report_url) - self.context.log_debug('Playback start reported: |%s|' % self.video_id) + client.update_watch_history( + self._context, + self.video_id, + report_url, + st=0, + et='N/A', + state=state + ) report_url = playback_stats.get('watchtime_url', '') @@ -107,10 +120,10 @@ def run(self): notification_sent = False - while player.isPlaying() and not self.context.abort_requested() and not self.stopped(): + while not self.abort_now(): if not notification_sent: notification_sent = True - self.context.send_notification('PlaybackStarted', { + self._context.send_notification('PlaybackStarted', { 'video_id': self.video_id, 'channel_id': self.channel_id, 'status': self.video_status, @@ -132,7 +145,10 @@ def run(self): pass if self.abort_now(): - self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) + self.update_times(last_total_time, + last_current_time, + last_segment_start, + last_percent_complete) break try: @@ -145,20 +161,27 @@ def run(self): self.current_time = 0.0 if self.abort_now(): - self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) + self.update_times(last_total_time, + last_current_time, + last_segment_start, + last_percent_complete) break try: - self.percent_complete = int(float(self.current_time) / float(self.total_time) * 100) + self.percent_complete = int(100 * self.current_time + / self.total_time) except ZeroDivisionError: self.percent_complete = 0 if self.abort_now(): - self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) + self.update_times(last_total_time, + last_current_time, + last_segment_start, + last_percent_complete) break - if player._start_time or player._seek_time: - _seek_time = player._start_time or player._seek_time + if player.start_time or player.seek_time: + _seek_time = player.start_time or player.seek_time if self.current_time < _seek_time: player.seekTime(_seek_time) try: @@ -166,20 +189,24 @@ def run(self): except RuntimeError: pass - if player._end_time and self.current_time >= player._end_time: - if clip and player._start_time: - player.seekTime(player._start_time) + if player.end_time and self.current_time >= player.end_time: + if clip and player.start_time: + player.seekTime(player.start_time) else: player.stop() if self.abort_now(): - self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) + self.update_times(last_total_time, + last_current_time, + last_segment_start, + last_percent_complete) break if p_waited >= report_interval: + # refresh client, tokens may need refreshing if is_logged_in: - self.provider.reset_client() # refresh client, tokens may need refreshing - client = self.provider.get_client(self.context) + self.provider.reset_client() + client = self.provider.get_client(self._context) is_logged_in = self.provider.is_logged_in() if self.current_time == played_time: @@ -192,11 +219,14 @@ def run(self): played_time = self.current_time if self.abort_now(): - self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete) + self.update_times(last_total_time, + last_current_time, + last_segment_start, + last_percent_complete) break - if (is_logged_in and report_url and use_remote_history and ( - first_report or p_waited >= report_interval)): + if (is_logged_in and report_url and use_remote_history + and (first_report or p_waited >= report_interval)): if first_report: first_report = False self.segment_start = 0.0 @@ -219,17 +249,16 @@ def run(self): if self.segment_start > segment_end: segment_end = self.segment_start + 10.0 - if state == 'playing' or last_state == 'playing': # only report state='paused' once - client.update_watch_history(self.video_id, report_url - .format(st=format(self.segment_start, '.3f'), - et=format(segment_end, '.3f'), - state=state)) - self.context.log_debug( - 'Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' % - (self.video_id, - format(self.segment_start, '.3f'), - format(segment_end, '.3f'), - self.percent_complete, state)) + # only report state='paused' once + if state == 'playing' or last_state == 'playing': + client.update_watch_history( + self._context, + self.video_id, + report_url, + st=format(self.segment_start, '.3f'), + et=format(segment_end, '.3f'), + state=state + ) self.segment_start = segment_end @@ -241,99 +270,127 @@ def run(self): p_waited += p_wait_time if is_logged_in and report_url and use_remote_history: - client.update_watch_history(self.video_id, report_url - .format(st=format(self.segment_start, '.3f'), - et=format(self.current_time, '.3f'), - state=state)) - self.context.log_debug('Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' % - (self.video_id, - format(self.segment_start, '.3f'), - format(self.current_time, '.3f'), - self.percent_complete, state)) - - self.context.send_notification('PlaybackStopped', { + client.update_watch_history( + self._context, + self.video_id, + report_url, + st=format(self.segment_start, '.3f'), + et=format(self.current_time, '.3f'), + state=state + ) + + self._context.send_notification('PlaybackStopped', { 'video_id': self.video_id, 'channel_id': self.channel_id, 'status': self.video_status, }) - self.context.log_debug('Playback stopped [%s]: %s secs of %s @ %s%%' % - (self.video_id, format(self.current_time, '.3f'), - format(self.total_time, '.3f'), self.percent_complete)) + self._context.log_debug('Playback stopped [{video_id}]:' + ' {current:.3f} secs of {total:.3f}' + ' @ {percent}%'.format( + video_id=self.video_id, + current=self.current_time, + total=self.total_time, + percent=self.percent_complete, + )) state = 'stopped' + # refresh client, tokens may need refreshing if is_logged_in: - self.provider.reset_client() # refresh client, tokens may need refreshing - client = self.provider.get_client(self.context) + self.provider.reset_client() + client = self.provider.get_client(self._context) is_logged_in = self.provider.is_logged_in() if self.percent_complete >= settings.get_play_count_min_percent(): play_count += 1 self.current_time = 0.0 if is_logged_in and report_url and use_remote_history: - client.update_watch_history(self.video_id, report_url - .format(st=format(self.total_time, '.3f'), - et=format(self.total_time, '.3f'), - state=state)) - self.context.log_debug('Playback reported [%s] @ 100%% state=%s' % (self.video_id, state)) + client.update_watch_history( + self._context, + self.video_id, + report_url, + st=format(self.total_time, '.3f'), + et=format(self.total_time, '.3f'), + state=state + ) else: if is_logged_in and report_url and use_remote_history: - client.update_watch_history(self.video_id, report_url - .format(st=format(self.current_time, '.3f'), - et=format(self.current_time, '.3f'), - state=state)) - self.context.log_debug('Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' % - (self.video_id, format(self.current_time, '.3f'), - format(self.current_time, '.3f'), - self.percent_complete, state)) + client.update_watch_history( + self._context, + self.video_id, + report_url, + st=format(self.current_time, '.3f'), + et=format(self.current_time, '.3f'), + state=state + ) refresh_only = True if use_local_history: - self.context.get_playback_history().update(self.video_id, play_count, self.total_time, - self.current_time, self.percent_complete) + self._context.get_playback_history().update(self.video_id, + play_count, + self.total_time, + self.current_time, + self.percent_complete) if not refresh_only and is_logged_in: - if settings.get_bool('youtube.playlist.watchlater.autoremove', True): + if settings.get_bool('youtube.playlist.watchlater.autoremove', + True): watch_later_id = access_manager.get_watch_later_id() if watch_later_id: - playlist_item_id = \ - client.get_playlist_item_id_of_video_id(playlist_id=watch_later_id, video_id=self.video_id) + playlist_item_id = client.get_playlist_item_id_of_video_id( + playlist_id=watch_later_id, video_id=self.video_id + ) if playlist_item_id: - json_data = client.remove_video_from_playlist(watch_later_id, playlist_item_id) - _ = self.provider.v3_handle_error(self.provider, self.context, json_data) + json_data = client.remove_video_from_playlist( + watch_later_id, playlist_item_id + ) + _ = self.provider.v3_handle_error(self.provider, + self._context, + json_data) history_playlist_id = access_manager.get_watch_history_id() if history_playlist_id and history_playlist_id != 'HL': - json_data = client.add_video_to_playlist(history_playlist_id, self.video_id) - _ = self.provider.v3_handle_error(self.provider, self.context, json_data) + json_data = client.add_video_to_playlist(history_playlist_id, + self.video_id) + _ = self.provider.v3_handle_error(self.provider, + self._context, + json_data) # rate video if settings.get_bool('youtube.post.play.rate', False): do_rating = True - if not settings.get_bool('youtube.post.play.rate.playlists', False): + if not settings.get_bool('youtube.post.play.rate.playlists', + False): playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) do_rating = int(playlist.size()) < 2 if do_rating: json_data = client.get_video_rating(self.video_id) - success = self.provider.v3_handle_error(self.provider, self.context, json_data) + success = self.provider.v3_handle_error(self.provider, + self._context, + json_data) if success: items = json_data.get('items', [{'rating': 'none'}]) rating = items[0].get('rating', 'none') if rating == 'none': rating_match = \ - re.search('/(?P[^/]+)/(?P[^/]+)', '/%s/%s/' % - (self.video_id, rating)) - self.provider.yt_video.process('rate', self.provider, self.context, rating_match) + re.search(r'/(?P[^/]+)' + r'/(?P[^/]+)', + '/{0}/{1}/'.format(self.video_id, + rating)) + self.provider.yt_video.process('rate', + self.provider, + self._context, + rating_match) playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - do_refresh = (int(playlist.size()) < 2) or (playlist.getposition() == -1) - - if do_refresh and settings.get_bool('youtube.post.play.refresh', False) and \ - not xbmc.getInfoLabel('Container.FolderPath') \ - .startswith(self.context.create_uri(['kodion', 'search', 'input'])): + do_refresh = playlist.size() < 2 or playlist.getposition() == -1 + if (do_refresh and settings.get_bool('youtube.post.play.refresh', False) + and not xbmc.getInfoLabel('Container.FolderPath').startswith( + self._context.create_uri(['kodion', 'search', 'input']) + )): # don't refresh search input it causes request for new input, # (Container.Update in abstract_provider /kodion/search/input/ # would resolve this but doesn't work with Remotes(Yatse)) @@ -342,14 +399,16 @@ def run(self): self.end() def stop(self): - self.context.log_debug('PlaybackMonitorThread[%s]: Stop event set...' % self.video_id) + self._context.log_debug('PlaybackMonitorThread[{0}]: Stop event set' + .format(self.video_id)) self._stopped.set() def stopped(self): return self._stopped.is_set() def end(self): - self.context.log_debug('PlaybackMonitorThread[%s]: End event set...' % self.video_id) + self._context.log_debug('PlaybackMonitorThread[{0}]: End event set' + .format(self.video_id)) self._ended.set() def ended(self): @@ -357,14 +416,15 @@ def ended(self): class YouTubePlayer(xbmc.Player): - def __init__(self, *args, **kwargs): - self.context = kwargs.get('context') + def __init__(self, *_args, **kwargs): + super(YouTubePlayer, self).__init__() + self._context = kwargs.get('context') self.provider = kwargs.get('provider') - self.ui = self.context.get_ui() + self.ui = self._context.get_ui() self.threads = [] - self._seek_time = None - self._start_time = None - self._end_time = None + self.seek_time = None + self.start_time = None + self.end_time = None def stop_threads(self): for thread in self.threads: @@ -372,7 +432,8 @@ def stop_threads(self): continue if not thread.stopped(): - self.context.log_debug('PlaybackMonitorThread[%s]: stopping...' % thread.video_id) + self._context.log_debug('PlaybackMonitorThread[{0}]: stopping' + .format(thread.video_id)) thread.stop() for thread in self.threads: @@ -390,9 +451,11 @@ def cleanup_threads(self, only_ended=True): continue if thread.ended(): - self.context.log_debug('PlaybackMonitorThread[%s]: clean up...' % thread.video_id) + self._context.log_debug('PlaybackMonitorThread[{0}]: clean up' + .format(thread.video_id)) else: - self.context.log_debug('PlaybackMonitorThread[%s]: stopping...' % thread.video_id) + self._context.log_debug('PlaybackMonitorThread[{0}]: stopping' + .format(thread.video_id)) if not thread.stopped(): thread.stop() try: @@ -400,8 +463,9 @@ def cleanup_threads(self, only_ended=True): except RuntimeError: pass - self.context.log_debug('PlaybackMonitor active threads: |%s|' % - ', '.join([thread.video_id for thread in active_threads])) + self._context.log_debug('PlaybackMonitor active threads: |{0}|'.format( + ', '.join([thread.video_id for thread in active_threads]) + )) self.threads = active_threads def onAVStarted(self): @@ -414,19 +478,19 @@ def onAVStarted(self): playback_json = json.loads(playback_json) try: - self._seek_time = float(playback_json.get('seek_time')) - self._start_time = float(playback_json.get('start_time')) - self._end_time = float(playback_json.get('end_time')) + self.seek_time = float(playback_json.get('seek_time')) + self.start_time = float(playback_json.get('start_time')) + self.end_time = float(playback_json.get('end_time')) except (ValueError, TypeError): - self._seek_time = None - self._start_time = None - self._end_time = None + self.seek_time = None + self.start_time = None + self.end_time = None self.ui.clear_property('playback_json') self.cleanup_threads() self.threads.append(PlaybackMonitorThread(self, self.provider, - self.context, + self._context, playback_json)) def onPlayBackEnded(self): @@ -444,8 +508,8 @@ def onPlayBackError(self): def onPlayBackSeek(self, time, seekOffset): time_s = time // 1000 - self._seek_time = None - if ((self._end_time and time_s > self._end_time) - or (self._start_time and time_s < self._start_time)): - self._start_time = None - self._end_time = None + self.seek_time = None + if ((self.end_time and time_s > self.end_time) + or (self.start_time and time_s < self.start_time)): + self.start_time = None + self.end_time = None diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index c81a4fedd..cb86ad423 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -65,7 +65,25 @@ def calculate_next_page_token(page, max_result): return 'C%s%s%sAA' % (high[high_iteration], low[low_iteration], overflow_token) - def update_watch_history(self, video_id, url): + def update_watch_history(self, + context, + video_id, + url, + st=None, + et=None, + state=None): + if None not in (st, et, state): + url.format(st=st, et=et, state=state) + else: + st = et = state = 'N/A' + + context.log_debug('Playback reported [{video_id}]:' + ' {st} segment start,' + ' {et} segment end,' + ' state={state}'.format( + video_id=video_id, st=st, et=et, state=state + )) + headers = {'Host': 'www.youtube.com', 'Connection': 'keep-alive', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.36 Safari/537.36', diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index bcf7dd4d4..48019112c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -27,7 +27,7 @@ class Subtitles(object): def __init__(self, context, video_id, captions, headers=None): self.video_id = video_id - self.context = context + self._context = context settings = context.get_settings() self.language = settings.get_language() @@ -41,7 +41,7 @@ def __init__(self, context, video_id, captions, headers=None): headers.pop('Content-Type', None) self.headers = headers - ui = self.context.get_ui() + ui = self._context.get_ui() self.prompt_override = (ui.get_property('prompt_for_subtitles') == video_id) ui.clear_property('prompt_for_subtitles') @@ -93,15 +93,15 @@ def srt_filename(self, sub_language): def _write_file(self, filepath, contents): if not make_dirs(self.BASE_PATH): - self.context.log_debug('Failed to create directories: %s' % self.BASE_PATH) + self._context.log_debug('Failed to create directories: %s' % self.BASE_PATH) return False - self.context.log_debug('Writing subtitle file: %s' % filepath) + self._context.log_debug('Writing subtitle file: %s' % filepath) try: with xbmcvfs.File(filepath, 'w') as srt_file: success = srt_file.write(contents) except (IOError, OSError): - self.context.log_debug('File write failed for: %s' % filepath) + self._context.log_debug('File write failed for: %s' % filepath) return False return success @@ -109,7 +109,7 @@ def _unescape(self, text): try: text = unescape(text) except: - self.context.log_debug('Subtitle unescape: failed to unescape text') + self._context.log_debug('Subtitle unescape: failed to unescape text') return text def get_default_lang(self): @@ -123,7 +123,7 @@ def get_subtitles(self): languages = self.LANG_PROMPT else: languages = self.subtitle_languages - self.context.log_debug('Subtitle get_subtitles: for setting |%s|' % str(languages)) + self._context.log_debug('Subtitle get_subtitles: for setting |%s|' % str(languages)) if languages == self.LANG_NONE: return [] if languages == self.LANG_CURR: @@ -146,7 +146,7 @@ def get_subtitles(self): list_of_subs.extend(self._get('en-US')) list_of_subs.extend(self._get('en-GB')) return list(set(list_of_subs)) - self.context.log_debug('Unknown language_enum: %s for subtitles' % str(languages)) + self._context.log_debug('Unknown language_enum: %s for subtitles' % str(languages)) return [] def _get_all(self, download=False): @@ -173,13 +173,13 @@ def _prompt(self): num_total = num_captions + num_translations if num_total: - choice = self.context.get_ui().on_select( - self.context.localize('subtitles.language'), + choice = self._context.get_ui().on_select( + self._context.localize('subtitles.language'), [name for _, name in captions] + [name + ' *' for _, name in translations] ) if choice == -1: - self.context.log_debug('Subtitle selection cancelled') + self._context.log_debug('Subtitle selection cancelled') return [] subtitle = None @@ -192,13 +192,13 @@ def _prompt(self): if subtitle: return subtitle - self.context.log_debug('No subtitles found for prompt') + self._context.log_debug('No subtitles found for prompt') return [] def _get(self, lang_code='en', language=None, no_asr=False, download=None): filename = self.srt_filename(lang_code) if xbmcvfs.exists(filename): - self.context.log_debug('Subtitle exists for: %s, filename: %s' % (lang_code, filename)) + self._context.log_debug('Subtitle exists for: %s, filename: %s' % (lang_code, filename)) return [filename] if download is None: @@ -230,7 +230,7 @@ def _get(self, lang_code='en', language=None, no_asr=False, download=None): if (lang_code != self.defaults['lang_code'] and not has_translation and caption_track is None): - self.context.log_debug('No subtitles found for: %s' % lang_code) + self._context.log_debug('No subtitles found for: %s' % lang_code) return [] subtitle_url = None @@ -253,24 +253,24 @@ def _get(self, lang_code='en', language=None, no_asr=False, download=None): ) if subtitle_url: - self.context.log_debug('Subtitle url: %s' % subtitle_url) + self._context.log_debug('Subtitle url: %s' % subtitle_url) if not download: return [subtitle_url] response = BaseRequestsClass().request(subtitle_url, headers=self.headers) if response.text: - self.context.log_debug('Subtitle found for: %s' % lang_code) + self._context.log_debug('Subtitle found for: %s' % lang_code) self._write_file(filename, bytearray(self._unescape(response.text), encoding='utf8', errors='ignore')) return [filename] - self.context.log_debug('Failed to retrieve subtitles for: %s' % lang_code) + self._context.log_debug('Failed to retrieve subtitles for: %s' % lang_code) return [] - self.context.log_debug('No subtitles found for: %s' % lang_code) + self._context.log_debug('No subtitles found for: %s' % lang_code) return [] @staticmethod From a9e331961b2d4af57153a5f5833e628346aadf8f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 7 Dec 2023 01:50:05 +1100 Subject: [PATCH 060/141] Add ForceResolvePlugin property to listitem - Attempt to reduce edge cases that can Kodi to crash due to multiple busy dialogs --- .../youtube_plugin/kodion/items/uri_item.py | 2 +- .../kodion/ui/xbmc/info_labels.py | 6 ++--- .../kodion/ui/xbmc/xbmc_items.py | 24 +++++++++++++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/uri_item.py b/resources/lib/youtube_plugin/kodion/items/uri_item.py index 2c3e2141d..e30df6e7e 100644 --- a/resources/lib/youtube_plugin/kodion/items/uri_item.py +++ b/resources/lib/youtube_plugin/kodion/items/uri_item.py @@ -13,6 +13,6 @@ class UriItem(BaseItem): def __init__(self, uri, playable=None): - super(UriItem, self).__init__(name='', uri=uri) + super(UriItem, self).__init__(name=uri, uri=uri) if playable is not None: self._playable = playable diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index 767f34d5a..9012179e2 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -101,12 +101,12 @@ def create_from_item(base_item): _process_string_value(info_labels, 'plot', base_item.get_plot()) # Image - if isinstance(base_item, ImageItem): + elif isinstance(base_item, ImageItem): # 'title' = 'Blow Your Head Off' (string) _process_string_value(info_labels, 'title', base_item.get_title()) # Audio - if isinstance(base_item, AudioItem): + elif isinstance(base_item, AudioItem): # 'duration' = 79 (int) _process_int_value(info_labels, 'duration', base_item.get_duration()) @@ -120,7 +120,7 @@ def create_from_item(base_item): _process_audio_rating(info_labels, base_item.get_rating()) # Video - if isinstance(base_item, VideoItem): + elif isinstance(base_item, VideoItem): # mediatype _process_mediatype(info_labels, 'mediatype', base_item.get_mediatype()) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index 0799f54cf..a511ca982 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -74,6 +74,7 @@ def video_playback_item(context, video_item): } props = { 'isPlayable': str(video_item.playable).lower(), + 'ForceResolvePlugin': 'true', } if (alternative_player @@ -170,6 +171,7 @@ def audio_listitem(context, audio_item): } props = { 'isPlayable': str(audio_item.playable).lower(), + 'ForceResolvePlugin': 'true', } list_item = ListItem(**kwargs) @@ -194,12 +196,23 @@ def audio_listitem(context, audio_item): return list_item -def uri_listitem(context, base_item): - uri = base_item.get_uri() +def uri_listitem(context, uri_item): + uri = uri_item.get_uri() context.log_debug('Converting UriItem |%s|' % uri) - item = ListItem(path=uri, offscreen=True) - item.setProperty('IsPlayable', str(base_item.playable).lower()) - return item + + kwargs = { + 'label': uri_item.get_name(), + 'path': uri, + 'offscreen': True, + } + props = { + 'isPlayable': str(uri_item.playable).lower(), + 'ForceResolvePlugin': 'true', + } + + list_item = ListItem(**kwargs) + list_item.setProperties(props) + return list_item def video_listitem(context, video_item): @@ -220,6 +233,7 @@ def video_listitem(context, video_item): } props = { 'isPlayable': str(video_item.playable).lower(), + 'ForceResolvePlugin': 'true', } list_item = ListItem(**kwargs) From b0329bf7a82f53eba1055be8d8d41f431d6e3ad6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:31:22 +1100 Subject: [PATCH 061/141] Update monitor and http_server - Follow up to 6d1301ea - Fix for server settings not applying until addon restarts - Use BaseRequestsClass - Update formatting - Use Settings rather than getting addon settings directly - this is in preparation for using new settings class via xbmcaddon.Addon('id').getSettings() - aim to update settings on onSettingsChanged rather than reading and parsing settings xml all the time - Add basic validation of port setting --- .../kodion/constants/const_settings.py | 2 +- .../kodion/network/http_server.py | 662 ++++++++++-------- .../kodion/settings/abstract_settings.py | 2 +- .../youtube_plugin/kodion/utils/monitor.py | 96 ++- .../youtube/client/__config__.py | 1 - resources/settings.xml | 6 +- 6 files changed, 432 insertions(+), 337 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 47dfeb3a0..da0308bf4 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -47,7 +47,7 @@ CONNECT_TIMEOUT = 'requests.timeout.connect' # (int) READ_TIMEOUT = 'requests.timeout.read' # (int) -HTTPD_PORT = 'kodion.mpd.proxy.port' # (number) +HTTPD_PORT = 'kodion.http.port' # (number) HTTPD_LISTEN = 'kodion.http.listen' # (string) HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (string) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 910fb9a89..e5fd5422e 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -10,35 +10,45 @@ import json import os import re -import requests -import socket +from socket import error as socket_error from http import server as BaseHTTPServer -from urllib.parse import parse_qs -from urllib.parse import urlparse +from textwrap import dedent +from urllib.parse import parse_qs, urlparse -import xbmc -import xbmcgui -import xbmcvfs +from xbmc import getCondVisibility, executebuiltin +from xbmcgui import Dialog, Window +from xbmcvfs import translatePath from xbmcaddon import Addon +from .requests import BaseRequestsClass from ..logger import log_debug from ..settings import Settings _addon_id = 'plugin.video.youtube' -_settings = Settings(Addon(id=_addon_id)) +_addon = Addon(_addon_id) +_settings = Settings(_addon) +_i18n = _addon.getLocalizedString +_server_requests = BaseRequestsClass() class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + base_path = translatePath('special://temp/{0}'.format(_addon_id)) + chunk_size = 1024 * 64 + local_ranges = ( + '10.', + '172.16.', + '192.168.', + '127.0.0.1', + 'localhost', + '::1', + ) + def __init__(self, request, client_address, server): - addon = Addon(_addon_id) - whitelist_ips = addon.getSetting('kodion.http.ip.whitelist') - whitelist_ips = ''.join(whitelist_ips.split()) - self.whitelist_ips = whitelist_ips.split(',') - self.local_ranges = ('10.', '172.16.', '192.168.', '127.0.0.1', 'localhost', '::1') - self.chunk_size = 1024 * 64 - self.base_path = xbmcvfs.translatePath('special://temp/%s' % _addon_id) - super(YouTubeProxyRequestHandler, self).__init__(request, client_address, server) + self.whitelist_ips = _settings.httpd_whitelist() + super(YouTubeProxyRequestHandler, self).__init__(request, + client_address, + server) def connection_allowed(self): client_ip = self.client_address[0] @@ -50,146 +60,169 @@ def connection_allowed(self): log_lines.append('Whitelisted: |%s|' % str(conn_allowed)) if not conn_allowed: - log_debug('HTTPServer: Connection from |%s| not allowed' % client_ip) + log_debug('HTTPServer: Connection from |{client_ip| not allowed'. + format(client_ip=client_ip)) elif self.path != '/ping': log_debug(' '.join(log_lines)) return conn_allowed # noinspection PyPep8Naming def do_GET(self): - addon = Addon('plugin.video.youtube') - mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true' - api_config_enabled = addon.getSetting('youtube.api.config.page') == 'true' + api_config_enabled = _settings.api_config_page() # Strip trailing slash if present - stripped_path = self.path.rstrip('/') + stripped_path = self.path.rstrip('/').lower() + if stripped_path != '/ping': + log_debug('HTTPServer: GET Request uri path |{stripped_path}|' + .format(stripped_path=stripped_path)) - if stripped_path == '/client_ip': - client_json = json.dumps({"ip": "{ip}".format(ip=self.client_address[0])}) + if not self.connection_allowed(): + self.send_error(403) + + elif stripped_path == '/client_ip': + client_json = json.dumps({"ip": "{ip}" + .format(ip=self.client_address[0])}) self.send_response(200) self.send_header('Content-Type', 'application/json; charset=utf-8') - self.send_header('Content-Length', len(client_json)) + self.send_header('Content-Length', str(len(client_json))) self.end_headers() self.wfile.write(client_json.encode('utf-8')) - if stripped_path != '/ping': - log_debug('HTTPServer: GET Request uri path |{proxy_path}|'.format(proxy_path=self.path)) - - if not self.connection_allowed(): - self.send_error(403) - elif mpd_proxy_enabled and self.path.endswith('.mpd'): - file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) + elif _settings.use_mpd_videos() and stripped_path.endswith('.mpd'): + file_path = os.path.join(self.base_path, + self.path.strip('/').strip('\\')) file_chunk = True - log_debug('HTTPServer: Request file path |{file_path}|'.format(file_path=file_path.encode('utf-8'))) + log_debug('HTTPServer: Request file path |{file_path}|' + .format(file_path=file_path)) try: with open(file_path, 'rb') as f: self.send_response(200) self.send_header('Content-Type', 'application/xml+dash') - self.send_header('Content-Length', os.path.getsize(file_path)) + self.send_header('Content-Length', + str(os.path.getsize(file_path))) self.end_headers() while file_chunk: file_chunk = f.read(self.chunk_size) if file_chunk: self.wfile.write(file_chunk) except IOError: - response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8')) + response = ('File Not Found: |{proxy_path}| -> |{file_path}|' + .format(proxy_path=self.path, file_path=file_path)) self.send_error(404, response) - elif api_config_enabled and stripped_path.lower() == '/api': + + elif api_config_enabled and stripped_path == '/api': html = self.api_config_page() html = html.encode('utf-8') + self.send_response(200) self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Content-Length', len(html)) + self.send_header('Content-Length', str(len(html))) self.end_headers() + for chunk in self.get_chunks(html): self.wfile.write(chunk) + elif api_config_enabled and stripped_path.startswith('/api_submit'): - addon = Addon('plugin.video.youtube') - i18n = addon.getLocalizedString - xbmc.executebuiltin('Dialog.Close(addonsettings,true)') - old_api_key = addon.getSetting('youtube.api.key') - old_api_id = addon.getSetting('youtube.api.id') - old_api_secret = addon.getSetting('youtube.api.secret') + executebuiltin('Dialog.Close(addonsettings, true)') + query = urlparse(self.path).query params = parse_qs(query) + updated = [] + api_key = params.get('api_key', [None])[0] api_id = params.get('api_id', [None])[0] api_secret = params.get('api_secret', [None])[0] - footer = i18n(30638) if api_key and api_id and api_secret else '' + # Bookmark this page + footer = _i18n(30638) if api_key and api_id and api_secret else '' + if re.search(r'api_key=(?:&|$)', query): api_key = '' if re.search(r'api_id=(?:&|$)', query): api_id = '' if re.search(r'api_secret=(?:&|$)', query): api_secret = '' - updated = [] - if api_key is not None and api_key != old_api_key: - addon.setSetting('youtube.api.key', api_key) - updated.append(i18n(30201)) - if api_id is not None and api_id != old_api_id: - addon.setSetting('youtube.api.id', api_id) - updated.append(i18n(30202)) - if api_secret is not None and api_secret != old_api_secret: - updated.append(i18n(30203)) - addon.setSetting('youtube.api.secret', api_secret) - if addon.getSetting('youtube.api.key') and addon.getSetting('youtube.api.id') and \ - addon.getSetting('youtube.api.secret'): - enabled = i18n(30636) + + if api_key is not None and api_key != _settings.api_key(): + _settings.api_key(new_key=api_key) + updated.append(_i18n(30201)) # API Key + + if api_id is not None and api_id != _settings.api_id(): + _settings.api_id(new_id=api_id) + updated.append(_i18n(30202)) # API ID + + if api_secret is not None and api_secret != _settings.api_secret(): + _settings.api_secret(new_secret=api_secret) + updated.append(_i18n(30203)) # API Secret + + if api_key and api_id and api_secret: + enabled = _i18n(30636) # Personal keys enabled else: - enabled = i18n(30637) - if not updated: - updated = i18n(30635) + enabled = _i18n(30637) # Personal keys disabled + + if updated: + # Successfully updated + updated = _i18n(30631) % ', '.join(updated) else: - updated = i18n(30631) % ', '.join(updated) + # No changes, not updated + updated = _i18n(30635) + html = self.api_submit_page(updated, enabled, footer) html = html.encode('utf-8') + self.send_response(200) self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Content-Length', len(html)) + self.send_header('Content-Length', str(len(html))) self.end_headers() + for chunk in self.get_chunks(html): self.wfile.write(chunk) + elif stripped_path == '/ping': self.send_error(204) + else: self.send_error(501) # noinspection PyPep8Naming def do_HEAD(self): - log_debug('HTTPServer: HEAD Request uri path |{proxy_path}|'.format(proxy_path=self.path)) + log_debug('HTTPServer: HEAD Request uri path |{proxy_path}|' + .format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) - else: - addon = Addon('plugin.video.youtube') - mpd_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true' - if mpd_proxy_enabled and self.path.endswith('.mpd'): - file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) - if not os.path.isfile(file_path): - response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8')) - self.send_error(404, response) - else: - self.send_response(200) - self.send_header('Content-Type', 'application/xml+dash') - self.send_header('Content-Length', os.path.getsize(file_path)) - self.end_headers() + elif _settings.use_mpd_videos() and self.path.endswith('.mpd'): + file_path = os.path.join(self.base_path, + self.path.strip('/').strip('\\')) + if not os.path.isfile(file_path): + response = ('File Not Found: |{proxy_path}| -> |{file_path}|' + .format(proxy_path=self.path, file_path=file_path)) + self.send_error(404, response) else: - self.send_error(501) + self.send_response(200) + self.send_header('Content-Type', 'application/xml+dash') + self.send_header('Content-Length', + str(os.path.getsize(file_path))) + self.end_headers() + else: + self.send_error(501) # noinspection PyPep8Naming def do_POST(self): - log_debug('HTTPServer: Request uri path |{proxy_path}|'.format(proxy_path=self.path)) + log_debug('HTTPServer: Request uri path |{proxy_path}|' + .format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) elif self.path.startswith('/widevine'): - license_url = xbmcgui.Window(10000).getProperty('plugin.video.youtube-license_url') - license_token = xbmcgui.Window(10000).getProperty('plugin.video.youtube-license_token') + home = Window(10000) - if not license_url: + lic_url = home.getProperty('plugin.video.youtube-license_url') + if not lic_url: self.send_error(404) return - if not license_token: + + lic_token = home.getProperty('plugin.video.youtube-license_token') + if not lic_token: self.send_error(403) return @@ -200,29 +233,40 @@ def do_POST(self): li_headers = { 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'Bearer %s' % license_token + 'Authorization': 'Bearer %s' % lic_token } - result = requests.post(url=license_url, headers=li_headers, data=post_data, stream=True) + response = _server_requests.request(lic_url, + method='POST', + headers=li_headers, + data=post_data, + stream=True) - response_length = int(result.headers.get('content-length')) - content = result.raw.read(response_length) + response_length = int(response.headers.get('content-length')) + content = response.raw.read(response_length) content_split = content.split('\r\n\r\n'.encode('utf-8')) response_header = content_split[0].decode('utf-8', 'ignore') response_body = content_split[1] - response_length = len(response_body) - match = re.search(r'^Authorized-Format-Types:\s*(?P.+?)\r*$', response_header, re.MULTILINE) + match = re.search(r'^Authorized-Format-Types:\s*' + r'(?P.+?)\r*$', + response_header, + re.MULTILINE) if match: authorized_types = match.group('authorized_types').split(',') - log_debug('HTTPServer: Found authorized formats |{authorized_fmts}|'.format(authorized_fmts=authorized_types)) - - fmt_to_px = {'SD': (1280 * 528) - 1, 'HD720': 1280 * 720, 'HD': 7680 * 4320} + log_debug('HTTPServer: Found authorized formats |{auth_fmts}|' + .format(auth_fmts=authorized_types)) + + fmt_to_px = { + 'SD': (1280 * 528) - 1, + 'HD720': 1280 * 720, + 'HD': 7680 * 4320 + } if 'HD' in authorized_types: size_limit = fmt_to_px['HD'] elif 'HD720' in authorized_types: - if xbmc.getCondVisibility('system.platform.android') == 1: + if getCondVisibility('system.platform.android') == 1: size_limit = fmt_to_px['HD720'] else: size_limit = fmt_to_px['SD'] @@ -232,10 +276,11 @@ def do_POST(self): self.send_response(200) if size_limit: - self.send_header('X-Limit-Video', 'max={size_limit}px'.format(size_limit=str(size_limit))) - for header, value in result.headers.items(): + self.send_header('X-Limit-Video', + 'max={0}px'.format(size_limit)) + for header, value in response.headers.items(): if re.match('^[Cc]ontent-[Ll]ength$', header): - self.send_header(header, response_length) + self.send_header(header, str(len(response_body))) else: self.send_header(header, value) self.end_headers() @@ -255,242 +300,265 @@ def get_chunks(self, data): @staticmethod def api_config_page(): - addon = Addon('plugin.video.youtube') - i18n = addon.getLocalizedString - api_key = addon.getSetting('youtube.api.key') - api_id = addon.getSetting('youtube.api.id') - api_secret = addon.getSetting('youtube.api.secret') - html = Pages().api_configuration.get('html') - css = Pages().api_configuration.get('css') - html = html.format(css=css, title=i18n(30634), api_key_head=i18n(30201), api_id_head=i18n(30202), - api_secret_head=i18n(30203), api_id_value=api_id, api_key_value=api_key, - api_secret_value=api_secret, submit=i18n(30630), header=i18n(30634)) + api_key = _settings.api_key() + api_id = _settings.api_id() + api_secret = _settings.api_secret() + html = Pages.api_configuration.get('html') + css = Pages.api_configuration.get('css') + html = html.format( + css=css, + title=_i18n(30634), # YouTube Add-on API Configuration + api_key_head=_i18n(30201), # API Key + api_id_head=_i18n(30202), # API ID + api_secret_head=_i18n(30203), # API Secret + api_id_value=api_id, + api_key_value=api_key, + api_secret_value=api_secret, + submit=_i18n(30630), # Save + header=_i18n(30634), # YouTube Add-on API Configuration + ) return html @staticmethod def api_submit_page(updated_keys, enabled, footer): - addon = Addon('plugin.video.youtube') - i18n = addon.getLocalizedString - html = Pages().api_submit.get('html') - css = Pages().api_submit.get('css') - html = html.format(css=css, title=i18n(30634), updated=updated_keys, enabled=enabled, footer=footer, header=i18n(30634)) + html = Pages.api_submit.get('html') + css = Pages.api_submit.get('css') + html = html.format( + css=css, + title=_i18n(30634), # YouTube Add-on API Configuration + updated=updated_keys, + enabled=enabled, + footer=footer, + header=_i18n(30634), # YouTube Add-on API Configuration + ) return html class Pages(object): api_configuration = { - 'html': - '\n\n' - '\n\t\n' - '\t{title}\n' - '\t\n' - '\n\n' - '\t
\n' - '\t
{header}
\n' - '\t
\n' - '\t\t\n' - '\t\t\n' - '\t\t\n' - '\t\t\n' - '\t
\n' - '\t
\n' - '\n', - - 'css': - 'body {\n' - ' background: #141718;\n' - '}\n' - '.center {\n' - ' margin: auto;\n' - ' width: 600px;\n' - ' padding: 10px;\n' - '}\n' - '.config_form {\n' - ' width: 575px;\n' - ' height: 145px;\n' - ' font-size: 16px;\n' - ' background: #1a2123;\n' - ' padding: 30px 30px 15px 30px;\n' - ' border: 5px solid #1a2123;\n' - '}\n' - 'h5 {\n' - ' font-family: Arial, Helvetica, sans-serif;\n' - ' font-size: 16px;\n' - ' color: #fff;\n' - ' font-weight: 600;\n' - ' width: 575px;\n' - ' height: 20px;\n' - ' background: #0f84a5;\n' - ' padding: 5px 30px 5px 30px;\n' - ' border: 5px solid #0f84a5;\n' - ' margin: 0px;\n' - '}\n' - '.config_form input[type=submit],\n' - '.config_form input[type=button],\n' - '.config_form input[type=text],\n' - '.config_form textarea,\n' - '.config_form label {\n' - ' font-family: Arial, Helvetica, sans-serif;\n' - ' font-size: 16px;\n' - ' color: #fff;\n' - '}\n' - '.config_form label {\n' - ' display:block;\n' - ' margin-bottom: 10px;\n' - '}\n' - '.config_form label > span {\n' - ' display: inline-block;\n' - ' float: left;\n' - ' width: 150px;\n' - '}\n' - '.config_form input[type=text] {\n' - ' background: transparent;\n' - ' border: none;\n' - ' border-bottom: 1px solid #147a96;\n' - ' width: 400px;\n' - ' outline: none;\n' - ' padding: 0px 0px 0px 0px;\n' - '}\n' - '.config_form input[type=text]:focus {\n' - ' border-bottom: 1px dashed #0f84a5;\n' - '}\n' - '.config_form input[type=submit],\n' - '.config_form input[type=button] {\n' - ' width: 150px;\n' - ' background: #141718;\n' - ' border: none;\n' - ' padding: 8px 0px 8px 10px;\n' - ' border-radius: 5px;\n' - ' color: #fff;\n' - ' margin-top: 10px\n' - '}\n' - '.config_form input[type=submit]:hover,\n' - '.config_form input[type=button]:hover {\n' - ' background: #0f84a5;\n' - '}\n' + 'html': dedent('''\ + + + + + + {title} + + + +
+
{header}
+
+ + + + +
+
+ + + '''), + 'css': ''.join('\t\t\t'.expandtabs(2) + line for line in dedent(''' + body { + background: #141718; + } + .center { + margin: auto; + width: 600px; + padding: 10px; + } + .config_form { + width: 575px; + height: 145px; + font-size: 16px; + background: #1a2123; + padding: 30px 30px 15px 30px; + border: 5px solid #1a2123; + } + h5 { + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + color: #fff; + font-weight: 600; + width: 575px; + height: 20px; + background: #0f84a5; + padding: 5px 30px 5px 30px; + border: 5px solid #0f84a5; + margin: 0px; + } + .config_form input[type=submit], + .config_form input[type=button], + .config_form input[type=text], + .config_form textarea, + .config_form label { + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + color: #fff; + } + .config_form label { + display:block; + margin-bottom: 10px; + } + .config_form label > span { + display: inline-block; + float: left; + width: 150px; + } + .config_form input[type=text] { + background: transparent; + border: none; + border-bottom: 1px solid #147a96; + width: 400px; + outline: none; + padding: 0px 0px 0px 0px; + } + .config_form input[type=text]:focus { + border-bottom: 1px dashed #0f84a5; + } + .config_form input[type=submit], + .config_form input[type=button] { + width: 150px; + background: #141718; + border: 1px solid #147a96; + padding: 8px 0px 8px 10px; + border-radius: 5px; + color: #fff; + margin-top: 10px + } + .config_form input[type=submit]:hover, + .config_form input[type=button]:hover { + background: #0f84a5; + } + ''').splitlines(True)) + '\t\t'.expandtabs(2) } api_submit = { - 'html': - '\n\n' - '\n\t\n' - '\t{title}\n' - '\t\n' - '\n\n' - '\t
\n' - '\t
{header}
\n' - '\t
\n' - '\t\t{updated}\n' - '\t\t{enabled}\n' - '\t\t \n' - '\t\t \n' - '\t\t \n' - '\t\t \n' - '\t\t
\n' - '\t\t\t{footer}\n' - '\t\t
\n' - '\t
\n' - '\t
\n' - '\n', - - 'css': - 'body {\n' - ' background: #141718;\n' - '}\n' - '.center {\n' - ' margin: auto;\n' - ' width: 600px;\n' - ' padding: 10px;\n' - '}\n' - '.textcenter {\n' - ' margin: auto;\n' - ' width: 600px;\n' - ' padding: 10px;\n' - ' text-align: center;\n' - '}\n' - '.content {\n' - ' width: 575px;\n' - ' height: 145px;\n' - ' background: #1a2123;\n' - ' padding: 30px 30px 15px 30px;\n' - ' border: 5px solid #1a2123;\n' - '}\n' - 'h5 {\n' - ' font-family: Arial, Helvetica, sans-serif;\n' - ' font-size: 16px;\n' - ' color: #fff;\n' - ' font-weight: 600;\n' - ' width: 575px;\n' - ' height: 20px;\n' - ' background: #0f84a5;\n' - ' padding: 5px 30px 5px 30px;\n' - ' border: 5px solid #0f84a5;\n' - ' margin: 0px;\n' - '}\n' - 'span {\n' - ' font-family: Arial, Helvetica, sans-serif;\n' - ' font-size: 16px;\n' - ' color: #fff;\n' - ' display: block;\n' - ' float: left;\n' - ' width: 575px;\n' - '}\n' - 'small {\n' - ' font-family: Arial, Helvetica, sans-serif;\n' - ' font-size: 12px;\n' - ' color: #fff;\n' - '}\n' + 'html': dedent('''\ + + + + + + {title} + + + +
+
{header}
+
+

{updated}

+

{enabled}

+

+ {footer} +

+
+
+ + + '''), + 'css': ''.join('\t\t\t'.expandtabs(2) + line for line in dedent(''' + body { + background: #141718; + } + .center { + margin: auto; + width: 600px; + padding: 10px; + } + .text_center { + margin: 2em auto auto; + width: 600px; + padding: 10px; + text-align: center; + } + .content { + width: 575px; + height: 145px; + background: #1a2123; + padding: 30px 30px 15px 30px; + border: 5px solid #1a2123; + } + h5 { + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + color: #fff; + font-weight: 600; + width: 575px; + height: 20px; + background: #0f84a5; + padding: 5px 30px 5px 30px; + border: 5px solid #0f84a5; + margin: 0px; + } + p { + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + color: #fff; + float: left; + width: 575px; + margin: 0.5em auto; + } + small { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + color: #fff; + } + ''').splitlines(True)) + '\t\t'.expandtabs(2) } def get_http_server(address=None, port=None): - addon = Addon(_addon_id) - address = address if address else addon.getSetting('kodion.http.listen') - address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '0.0.0.0' - port = int(port) if port else 50152 + address = _settings.httpd_listen(for_request=False, ip_address=address) + port = _settings.httpd_port(port) try: - server = BaseHTTPServer.HTTPServer((address, port), YouTubeProxyRequestHandler) + server = BaseHTTPServer.HTTPServer((address, port), + YouTubeProxyRequestHandler) return server - except socket.error as e: - log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|'.format(address=address, port=port, response=str(e))) - xbmcgui.Dialog().notification(addon.getAddonInfo('name'), str(e), - addon.getAddonInfo('icon'), - 5000, False) + except socket_error as e: + log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|' + .format(address=address, port=port, response=str(e))) + Dialog().notification(_addon.getAddonInfo('name'), + str(e), + _addon.getAddonInfo('icon'), + time=5000, + sound=False) return None def is_httpd_live(address=None, port=None): - addon = Addon(_addon_id) - address = address if address else addon.getSetting('kodion.http.listen') - address = '127.0.0.1' if address == '0.0.0.0' else address - address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '127.0.0.1' - port = int(port) if port else 50152 + address = _settings.httpd_listen(for_request=True, ip_address=address) + port = _settings.httpd_port(port=port) url = 'http://{address}:{port}/ping'.format(address=address, port=port) try: - response = requests.get(url) + response = _server_requests.request(url) result = response.status_code == 204 if not result: - log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response=response.status_code)) + log_debug('HTTPServer: Ping |{address}:{port}| |{response}|' + .format(address=address, + port=port, + response=response.status_code)) return result except: - log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response='failed')) + log_debug('HTTPServer: Ping |{address}:{port}| |{response}|' + .format(address=address, port=port, response='failed')) return False def get_client_ip_address(address=None, port=None): - addon = Addon(_addon_id) - address = address if address else addon.getSetting('kodion.http.listen') - address = '127.0.0.1' if address == '0.0.0.0' else address - address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '127.0.0.1' - port = int(port) if port else 50152 + address = _settings.httpd_listen(for_request=True, ip_address=address) + port = _settings.httpd_port(port=port) url = 'http://{address}:{port}/client_ip'.format(address=address, port=port) - response = requests.get(url) + response = _server_requests.request(url) ip_address = None if response.status_code == 200: response_json = response.json() diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 79a32cd6b..e5cabea63 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -173,7 +173,7 @@ def use_mpd_live_streams(self): def httpd_port(self, port=None): default_port = 50152 - if not port: + if port is None: port = self.get_int(SETTINGS.HTTPD_PORT, default_port) try: diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index 96cb77bea..1cd981580 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -14,37 +14,41 @@ from urllib.parse import unquote import xbmc -import xbmcaddon import xbmcvfs +from xbmcaddon import Addon +from ..logger import log_debug from ..network import get_http_server, is_httpd_live -from .. import logger +from ..settings import Settings class YouTubeMonitor(xbmc.Monitor): + _addon_id = 'plugin.video.youtube' + _addon = Addon(_addon_id) + _settings = Settings(_addon) # noinspection PyUnusedLocal,PyMissingConstructor def __init__(self, *args, **kwargs): - self.addon_id = 'plugin.video.youtube' - addon = xbmcaddon.Addon(self.addon_id) - self._whitelist = addon.getSetting('kodion.http.ip.whitelist') - self._httpd_port = int(addon.getSetting('kodion.mpd.proxy.port')) - self._old_httpd_port = self._httpd_port - self._use_httpd = (addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.isa') == 'true') or \ - (addon.getSetting('youtube.api.config.page') == 'true') - self._httpd_address = addon.getSetting('kodion.http.listen') - self._old_httpd_address = self._httpd_address + settings = self._settings + self._whitelist = settings.httpd_whitelist() + self._old_httpd_port = self._httpd_port = int(settings.httpd_port()) + self._use_httpd = (settings.use_mpd_videos() + or settings.api_config_page()) + self._old_httpd_address = self._httpd_address = settings.httpd_listen() self.httpd = None self.httpd_thread = None if self.use_httpd(): self.start_httpd() - del addon + super(YouTubeMonitor, self).__init__() def onNotification(self, sender, method, data): - if sender == 'plugin.video.youtube' and method.endswith('.check_settings'): - data = json.loads(data) - data = json.loads(unquote(data[0])) - logger.log_debug('onNotification: |check_settings| -> |%s|' % json.dumps(data)) + if (sender == 'plugin.video.youtube' + and method.endswith('.check_settings')): + if not isinstance(data, dict): + data = json.loads(data) + data = json.loads(unquote(data[0])) + log_debug('onNotification: |check_settings| -> |{data}|' + .format(data=data)) _use_httpd = data.get('use_httpd') _httpd_port = data.get('httpd_port') @@ -55,32 +59,48 @@ def onNotification(self, sender, method, data): port_changed = self._httpd_port != _httpd_port address_changed = self._httpd_address != _httpd_address - if _whitelist != self._whitelist: + if whitelist_changed: self._whitelist = _whitelist if self._use_httpd != _use_httpd: self._use_httpd = _use_httpd - if self._httpd_port != _httpd_port: + if port_changed: self._old_httpd_port = self._httpd_port self._httpd_port = _httpd_port - if self._httpd_address != _httpd_address: + if address_changed: self._old_httpd_address = self._httpd_address self._httpd_address = _httpd_address - if self.use_httpd() and not self.httpd: + if not _use_httpd: + if self.httpd: + self.shutdown_httpd() + elif not self.httpd: self.start_httpd() - elif self.use_httpd() and (port_changed or whitelist_changed or address_changed): + elif port_changed or whitelist_changed or address_changed: if self.httpd: self.restart_httpd() else: self.start_httpd() - elif not self.use_httpd() and self.httpd: - self.shutdown_httpd() elif sender == 'plugin.video.youtube': - logger.log_debug('onNotification: |unknown method|') + log_debug('onNotification: |unhandled method| -> |{method}|' + .format(method=method)) + + def onSettingsChanged(self): + YouTubeMonitor._addon = Addon(self._addon_id) + YouTubeMonitor._settings = Settings(self._addon) + data = { + 'use_httpd': (self._settings.use_mpd_videos() + or self._settings.api_config_page()), + 'httpd_port': self._settings.httpd_port(), + 'whitelist': self._settings.httpd_whitelist(), + 'httpd_address': self._settings.httpd_listen() + } + self.onNotification('plugin.video.youtube', + 'Other.check_settings', + data) def use_httpd(self): return self._use_httpd @@ -104,12 +124,11 @@ def start_httpd(self): if self.httpd: return - logger.log_debug('HTTPServer: Starting |{ip}:{port}|'.format( - ip=self.httpd_address(), - port=str(self.httpd_port()) - )) + log_debug('HTTPServer: Starting |{ip}:{port}|' + .format(ip=self.httpd_address(), port=str(self.httpd_port()))) self.httpd_port_sync() - self.httpd = get_http_server(address=self.httpd_address(), port=self.httpd_port()) + self.httpd = get_http_server(address=self.httpd_address(), + port=self.httpd_port()) if not self.httpd: return @@ -117,15 +136,16 @@ def start_httpd(self): self.httpd_thread.daemon = True self.httpd_thread.start() sock_name = self.httpd.socket.getsockname() - logger.log_debug('HTTPServer: Serving on |{ip}:{port}|'.format( + log_debug('HTTPServer: Serving on |{ip}:{port}|'.format( ip=str(sock_name[0]), port=str(sock_name[1]) )) def shutdown_httpd(self): if self.httpd: - logger.log_debug('HTTPServer: Shutting down |{ip}:{port}|'.format(ip=self.old_httpd_address(), - port=str(self.old_httpd_port()))) + log_debug('HTTPServer: Shutting down |{ip}:{port}|' + .format(ip=self.old_httpd_address(), + port=self.old_httpd_port())) self.httpd_port_sync() self.httpd.shutdown() self.httpd.socket.close() @@ -134,9 +154,11 @@ def shutdown_httpd(self): self.httpd = None def restart_httpd(self): - logger.log_debug('HTTPServer: Restarting... |{old_ip}:{old_port}| -> |{ip}:{port}|' - .format(old_ip=self.old_httpd_address(), old_port=str(self.old_httpd_port()), - ip=self.httpd_address(), port=str(self.httpd_port()))) + log_debug('HTTPServer: Restarting |{old_ip}:{old_port}| > |{ip}:{port}|' + .format(old_ip=self.old_httpd_address(), + old_port=self.old_httpd_port(), + ip=self.httpd_address(), + port=self.httpd_port())) self.shutdown_httpd() self.start_httpd() @@ -158,6 +180,8 @@ def remove_temp_dir(self): pass if os.path.isdir(path): - logger.log_debug('Failed to remove directory: {dir}'.format(dir=path.encode('utf-8'))) + log_debug('Failed to remove directory: {path}'.format( + path=path + )) return False return True diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index ed780b264..c52bc2757 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -206,7 +206,6 @@ def _strip_api_keys(self, api_key, client_id, client_secret): notification_data = {'use_httpd': (__settings.use_mpd_videos() - or __settings.use_mpd_live_streams() or __settings.api_config_page()), 'httpd_port': __settings.httpd_port(), 'whitelist': __settings.httpd_whitelist(), diff --git a/resources/settings.xml b/resources/settings.xml index 4c06c207f..f13b761e1 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -790,9 +790,13 @@ true
- + 0 50152 + + 0 + 65535 + true From 33371f97fc907c1b0a6541e63df74170e8764ca4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:16:19 +1100 Subject: [PATCH 062/141] Fix regressions in live channels following 72480b5 --- .../youtube/helper/url_resolver.py | 12 +- .../youtube/helper/url_to_item_converter.py | 115 ++++++++++-------- .../lib/youtube_plugin/youtube/provider.py | 2 +- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index 3033bf834..0a28d1f93 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -97,7 +97,7 @@ def supports_url(self, url, url_components): return 'GET' if len(path) == 1 and path[0] else False def resolve(self, url, url_components, method='HEAD'): - path = url_components.path.lower() + path = url_components.path.rstrip('/').lower() if path == '/redirect': params = dict(parse_qsl(url_components.query)) url = params['q'] @@ -171,7 +171,15 @@ def resolve(self, url, url_components, method='HEAD'): elif method == 'GET': match = self._RE_CHANNEL_URL.search(response.text) if match: - return match.group('channel_url') + url = match.group('channel_url') + if path.endswith(('/live', '/streams')): + url_components = urlparse(unescape(url)) + params = dict(parse_qsl(url_components.query)) + params['live'] = 1 + return url_components._replace( + query=urlencode(params) + ).geturl() + return url return response.url diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index 6b564163d..dbe3f50be 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -17,7 +17,7 @@ class UrlToItemConverter(object): - RE_PATH_ID = re.compile(r'/\w+/(?P[^/?#]+)', re.I) + RE_PATH_ID = re.compile(r'/[^/]+/(?P[^/?#]+)', re.I) VALID_HOSTNAMES = { 'youtube.com', 'www.youtube.com', @@ -62,13 +62,13 @@ def add_url(self, url, provider, context): } path = parsed_url.path.rstrip('/').lower() - channel_id = video_id = None if path.startswith(('/playlist', '/watch')): pass elif path.startswith('/channel/'): re_match = self.RE_PATH_ID.match(parsed_url.path) - channel_id = re_match.group('id') - if path.endswith(('/live', '/streams')): + new_params['channel_id'] = re_match.group('id') + if ('live' not in new_params + and path.endswith(('/live', '/streams'))): new_params['live'] = 1 elif path.startswith(('/clip/', '/embed/', '/live/', '/shorts/')): re_match = self.RE_PATH_ID.match(parsed_url.path) @@ -81,6 +81,7 @@ def add_url(self, url, provider, context): if 'video_id' in new_params: video_id = new_params['video_id'] + video_item = VideoItem( '', context.create_uri(['play'], new_params) ) @@ -88,6 +89,7 @@ def add_url(self, url, provider, context): elif 'playlist_id' in new_params: playlist_id = new_params['playlist_id'] + if self._flatten: self._playlist_ids.append(playlist_id) return @@ -98,12 +100,17 @@ def add_url(self, url, provider, context): playlist_item.set_fanart(provider.get_fanart(context)) self._playlist_id_dict[playlist_id] = playlist_item - elif channel_id: - if self._flatten: + elif 'channel_id' in new_params: + channel_id = new_params['channel_id'] + live = new_params.get('live') + + if not live and self._flatten: self._channel_ids.append(channel_id) return - channel_item = DirectoryItem( + channel_item = VideoItem( + '', context.create_uri(['play'], new_params) + ) if live else DirectoryItem( '', context.create_uri(['channel', channel_id], new_params) ) channel_item.set_fanart(provider.get_fanart(context)) @@ -116,10 +123,10 @@ def add_urls(self, urls, provider, context): for url in urls: self.add_url(url, provider, context) - def get_items(self, provider, context, title_required=True): + def get_items(self, provider, context, skip_title=False): result = [] - if self._flatten and self._channel_ids: + if self._channel_ids: # remove duplicates self._channel_ids = list(set(self._channel_ids)) @@ -133,7 +140,7 @@ def get_items(self, provider, context, title_required=True): channels_item.set_fanart(provider.get_fanart(context)) result.append(channels_item) - if self._flatten and self._playlist_ids: + if self._playlist_ids: # remove duplicates self._playlist_ids = list(set(self._playlist_ids)) @@ -153,61 +160,63 @@ def get_items(self, provider, context, title_required=True): playlists_item.set_fanart(provider.get_fanart(context)) result.append(playlists_item) - if not self._flatten: - result.extend(self.get_channel_items(provider, context)) + if self._channel_id_dict: + result += self.get_channel_items(provider, context, skip_title) - if not self._flatten: - result.extend(self.get_playlist_items(provider, context)) + if self._playlist_id_dict: + result += self.get_playlist_items(provider, context, skip_title) - # add videos - result.extend(self.get_video_items(provider, context, title_required)) + if self._video_id_dict: + result += self.get_video_items(provider, context, skip_title) return result - def get_video_items(self, provider, context, title_required=True): - incognito = context.get_param('incognito', False) - use_play_data = not incognito + def get_video_items(self, provider, context, skip_title=False): + if self._video_items: + return self._video_items - if not self._video_items: - channel_id_dict = {} - utils.update_video_infos(provider, context, self._video_id_dict, - channel_items_dict=channel_id_dict, - use_play_data=use_play_data) - utils.update_fanarts(provider, context, channel_id_dict) + use_play_data = not context.get_param('incognito', False) - self._video_items = [ - video_item - for video_item in self._video_id_dict.values() - if not title_required or video_item.get_title() - ] + channel_id_dict = {} + utils.update_video_infos(provider, context, self._video_id_dict, + channel_items_dict=channel_id_dict, + use_play_data=use_play_data) + utils.update_fanarts(provider, context, channel_id_dict) + self._video_items = [ + video_item + for video_item in self._video_id_dict.values() + if skip_title or video_item.get_title() + ] return self._video_items - def get_playlist_items(self, provider, context): - if not self._playlist_items: - channel_id_dict = {} - utils.update_playlist_infos(provider, context, - self._playlist_id_dict, - channel_items_dict=channel_id_dict) - utils.update_fanarts(provider, context, channel_id_dict) - - self._playlist_items = [ - playlist_item - for playlist_item in self._playlist_id_dict.values() - if playlist_item.get_name() - ] - + def get_playlist_items(self, provider, context, skip_title=False): + if self._playlist_items: + return self._playlist_items + + channel_id_dict = {} + utils.update_playlist_infos(provider, context, + self._playlist_id_dict, + channel_items_dict=channel_id_dict) + utils.update_fanarts(provider, context, channel_id_dict) + + self._playlist_items = [ + playlist_item + for playlist_item in self._playlist_id_dict.values() + if skip_title or playlist_item.get_title() + ] return self._playlist_items - def get_channel_items(self, provider, context): - if not self._channel_items: - channel_id_dict = {} - utils.update_fanarts(provider, context, channel_id_dict) + def get_channel_items(self, provider, context, skip_title=False): + if self._channel_items: + return self._channel_items - self._channel_items = [ - channel_item - for channel_item in self._channel_id_dict.values() - if channel_item.get_name() - ] + channel_id_dict = {} + utils.update_fanarts(provider, context, channel_id_dict) + self._channel_items = [ + channel_item + for channel_item in self._channel_id_dict.values() + if skip_title or channel_item.get_title() + ] return self._channel_items diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index fa87b85bb..1baa3caf4 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -272,7 +272,7 @@ def on_uri2addon(self, context, re_match): res_url = resolver.resolve(uri) url_converter = UrlToItemConverter(flatten=True) url_converter.add_url(res_url, self, context) - items = url_converter.get_items(self, context, title_required=False) + items = url_converter.get_items(self, context, skip_title=True) if items: return items[0] From 6115f9073a5a608b4618e7cfe493d4f13b6a1506 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:21:28 +1100 Subject: [PATCH 063/141] Remove duplicate code in VideoInfo._get_error_details --- .../youtube/helper/video_info.py | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 580f722b3..12c9d50bb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -986,8 +986,7 @@ def _process_url_params(self, url): urlencode(query, doseq=True), parts.fragment)) - @staticmethod - def _get_error_details(playability_status, details=None): + def _get_error_details(self, playability_status, details=None): if not playability_status: return None if not details: @@ -997,26 +996,9 @@ def _get_error_details(playability_status, details=None): ('reason', 'title') ) - result = playability_status - for keys in details: - is_dict = isinstance(result, dict) - if not is_dict and not isinstance(result, list): - return None - - if not isinstance(keys, (list, tuple)): - keys = [keys] - for key in keys: - if is_dict: - if key not in result: - continue - elif not isinstance(key, int) or len(result) <= key: - continue - result = result[key] - break - else: - return None + result = self.json_traverse(playability_status, details) - if 'runs' not in result: + if not result or 'runs' not in result: return result detail_texts = [ From 000b415d3d81dc6a95e1572ae21f24dbbd496d3c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:48:53 +1100 Subject: [PATCH 064/141] Update LoginClient and YouTubeRequestClient - Update formatting - Use LoginException as default exception type for LoginClient - Fix for possible undefined instance variables - TODO: use build_client for LoginClient --- .../youtube/client/login_client.py | 204 +++++++++++------- .../youtube/client/request_client.py | 17 +- 2 files changed, 132 insertions(+), 89 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index f713e16ea..70edd8319 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -31,6 +31,21 @@ class LoginClient(YouTubeRequestClient): api_keys_changed = keys_changed + ANDROID_CLIENT_AUTH_URL = 'https://android.clients.google.com/auth' + DEVICE_CODE_URL = 'https://accounts.google.com/o/oauth2/device/code' + REVOKE_URL = 'https://accounts.google.com/o/oauth2/revoke' + SERVICE_URLS = 'oauth2:' + 'https://www.googleapis.com/auth/'.join(( + 'youtube ' + 'youtube.force-ssl ' + 'plus.me ' + 'emeraldsea.mobileapps.doritos.cookie ' + 'plus.stream.read ' + 'plus.stream.write ' + 'plus.pages.manage ' + 'identity.plus.page.impersonation', + )) + TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' + CONFIGS = { 'youtube-tv': { 'system': 'YouTube TV', @@ -63,24 +78,23 @@ def __init__(self, config=None, language='en-US', region='', self._log_error_callback = None - super(LoginClient, self).__init__() + super(LoginClient, self).__init__(exc_type=LoginException) @staticmethod def _login_json_hook(response): - json_data = None try: json_data = response.json() if 'error' in json_data: raise YouTubeException('"error" in response JSON data', json_data=json_data, - response=response,) + response=response) except ValueError as error: raise InvalidJSONError(error, response=response) response.raise_for_status() return json_data @staticmethod - def _login_error_hook(error, response): + def _login_error_hook(error, _response): json_data = getattr(error, 'json_data', None) if not json_data: return None, None, None, None, LoginException @@ -112,29 +126,36 @@ def set_access_token_tv(self, access_token_tv=''): def revoke(self, refresh_token): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'accounts.google.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} post_data = {'token': refresh_token} - self.request('https://accounts.google.com/o/oauth2/revoke', - method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, - error_title='Logout Failed', - error_info='Revoke failed: {exc}', - raise_exc=LoginException - ) + self.request(self.REVOKE_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Logout Failed', + error_info='Revoke failed: {exc}', + raise_exc=True) def refresh_token_tv(self, refresh_token): client_id = str(self.CONFIGS['youtube-tv']['id']) client_secret = str(self.CONFIGS['youtube-tv']['secret']) - return self.refresh_token(refresh_token, client_id=client_id, client_secret=client_secret) + return self.refresh_token(refresh_token, + client_id=client_id, + client_secret=client_secret) def refresh_token(self, refresh_token, client_id='', client_secret=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'www.googleapis.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} client_id = client_id or self._config['id'] @@ -145,21 +166,24 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): 'grant_type': 'refresh_token'} config_type = self._get_config_type(client_id, client_secret) - client_summary = ''.join([ + client = ''.join([ '(config_type: |', config_type, '|', ' client_id: |', client_id[:5], '...|', ' client_secret: |', client_secret[:5], '...|)' ]) - log_debug('Refresh token for ' + client_summary) - - json_data = self.request('https://www.googleapis.com/oauth2/v4/token', - method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, - error_title='Login Failed', - error_info='Refresh failed for ' + client_summary + ': {exc}', - raise_exc=LoginException - ) + log_debug('Refresh token for {0}'.format(client)) + + json_data = self.request(self.TOKEN_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Login Failed', + error_info=('Refresh token failed' + ' {client}: {{exc}}' + .format(client=client)), + raise_exc=True) if json_data: access_token = json_data['access_token'] @@ -170,12 +194,16 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): def request_access_token_tv(self, code, client_id='', client_secret=''): client_id = client_id or self.CONFIGS['youtube-tv']['id'] client_secret = client_secret or self.CONFIGS['youtube-tv']['secret'] - return self.request_access_token(code, client_id=client_id, client_secret=client_secret) + return self.request_access_token(code, + client_id=client_id, + client_secret=client_secret) def request_access_token(self, code, client_id='', client_secret=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'www.googleapis.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} client_id = client_id or self._config['id'] @@ -186,21 +214,24 @@ def request_access_token(self, code, client_id='', client_secret=''): 'grant_type': 'http://oauth.net/grant_type/device/1.0'} config_type = self._get_config_type(client_id, client_secret) - client_summary = ''.join([ + client = ''.join([ '(config_type: |', config_type, '|', ' client_id: |', client_id[:5], '...|', ' client_secret: |', client_secret[:5], '...|)' ]) - log_debug('Requesting access token for ' + client_summary) - - json_data = self.request('https://www.googleapis.com/oauth2/v4/token', - method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, - error_title='Login Failed', - error_info='Access token request failed for ' + client_summary + ': {exc}', - raise_exc=LoginException('Login Failed: Unknown response') - ) + log_debug('Requesting access token for {0}'.format(client)) + + json_data = self.request(self.TOKEN_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Login Failed: Unknown response', + error_info=('Access token request failed' + ' {client}: {{exc}}' + .format(client=client)), + raise_exc=True) return json_data def request_device_and_user_code_tv(self): @@ -210,7 +241,9 @@ def request_device_and_user_code_tv(self): def request_device_and_user_code(self, client_id=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'accounts.google.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} client_id = client_id or self._config['id'] @@ -218,20 +251,23 @@ def request_device_and_user_code(self, client_id=''): 'scope': 'https://www.googleapis.com/auth/youtube'} config_type = self._get_config_type(client_id) - client_summary = ''.join([ + client = ''.join([ '(config_type: |', config_type, '|', ' client_id: |', client_id[:5], '...|)', ]) - log_debug('Requesting device and user code for ' + client_summary) - - json_data = self.request('https://accounts.google.com/o/oauth2/device/code', - method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, - error_title='Login Failed', - error_info='Requesting device and user code failed for ' + client_summary + ': {exc}', - raise_exc=LoginException('Login Failed: Unknown response') - ) + log_debug('Requesting device and user code for {0}'.format(client)) + + json_data = self.request(self.DEVICE_CODE_URL, + method='POST', + data=post_data, + headers=headers, + response_hook=self._login_json_hook, + error_hook=self._login_error_hook, + error_title='Login Failed: Unknown response', + error_info=('Device/user code request failed' + ' {client}: {{exc}}' + .format(client=client)), + raise_exc=True) return json_data def get_access_token(self): @@ -246,34 +282,30 @@ def authenticate(self, username, password): 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} - post_data = {'device_country': self._region.lower(), - 'operatorCountry': self._region.lower(), - 'lang': self._language.replace('-', '_'), - 'sdk_version': '19', - # 'google_play_services_version': '6188034', - 'accountType': 'HOSTED_OR_GOOGLE', - 'Email': username.encode('utf-8'), - 'service': 'oauth2:https://www.googleapis.com/auth/youtube ' - 'https://www.googleapis.com/auth/youtube.force-ssl ' - 'https://www.googleapis.com/auth/plus.me ' - 'https://www.googleapis.com/auth/emeraldsea.mobileapps.doritos.cookie ' - 'https://www.googleapis.com/auth/plus.stream.read ' - 'https://www.googleapis.com/auth/plus.stream.write ' - 'https://www.googleapis.com/auth/plus.pages.manage ' - 'https://www.googleapis.com/auth/identity.plus.page.impersonation', - 'source': 'android', - 'androidId': '38c6ee9a82b8b10a', - 'app': 'com.google.android.youtube', - # 'client_sig': '24bb24c05e47e0aefa68a58a766179d9b613a600', - 'callerPkg': 'com.google.android.youtube', - # 'callerSig': '24bb24c05e47e0aefa68a58a766179d9b613a600', - 'Passwd': password.encode('utf-8')} - - result = self.request('https://android.clients.google.com/auth', - method='POST', data=post_data, headers=headers, - error_title='Login Failed', - raise_exc=LoginException - ) + post_data = { + 'device_country': self._region.lower(), + 'operatorCountry': self._region.lower(), + 'lang': self._language.replace('-', '_'), + 'sdk_version': '19', + # 'google_play_services_version': '6188034', + 'accountType': 'HOSTED_OR_GOOGLE', + 'Email': username.encode('utf-8'), + 'service': self.SERVICE_URLS, + 'source': 'android', + 'androidId': '38c6ee9a82b8b10a', + 'app': 'com.google.android.youtube', + # 'client_sig': '24bb24c05e47e0aefa68a58a766179d9b613a600', + 'callerPkg': 'com.google.android.youtube', + # 'callerSig': '24bb24c05e47e0aefa68a58a766179d9b613a600', + 'Passwd': password.encode('utf-8') + } + + result = self.request(self.ANDROID_CLIENT_AUTH_URL, + method='POST', + data=post_data, + headers=headers, + error_title='Login Failed', + raise_exc=True) lines = result.text.replace('\n', '&') params = dict(parse_qsl(lines)) @@ -287,11 +319,17 @@ def authenticate(self, username, password): def _get_config_type(self, client_id, client_secret=None): """used for logging""" if client_secret is None: - using_conf_tv = (client_id == self.CONFIGS['youtube-tv'].get('id')) - using_conf_main = (client_id == self.CONFIGS['main'].get('id')) + using_conf_tv = client_id == self.CONFIGS['youtube-tv'].get('id') + using_conf_main = client_id == self.CONFIGS['main'].get('id') else: - using_conf_tv = ((client_id == self.CONFIGS['youtube-tv'].get('id')) and (client_secret == self.CONFIGS['youtube-tv'].get('secret'))) - using_conf_main = ((client_id == self.CONFIGS['main'].get('id')) and (client_secret == self.CONFIGS['main'].get('secret'))) + using_conf_tv = ( + client_secret == self.CONFIGS['youtube-tv'].get('secret') + and client_id == self.CONFIGS['youtube-tv'].get('id') + ) + using_conf_main = ( + client_secret == self.CONFIGS['main'].get('secret') + and client_id == self.CONFIGS['main'].get('id') + ) if not using_conf_main and not using_conf_tv: return 'None' if using_conf_tv: diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 0bfb2771d..0c31c0291 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -255,8 +255,10 @@ class YouTubeRequestClient(BaseRequestsClass): }, } - def __init__(self): - super(YouTubeRequestClient, self).__init__(exc_type=YouTubeException) + def __init__(self, exc_type=YouTubeException): + super(YouTubeRequestClient, self).__init__(exc_type=exc_type) + self._access_token = None + self.video_id = None @staticmethod def json_traverse(json_data, path): @@ -286,7 +288,7 @@ def json_traverse(json_data, path): return None return result - def build_client(self, client_name, auth_header=False): + def build_client(self, client_name, auth_header=False, data=None): def _merge_dicts(item1, item2, _=Ellipsis): if not isinstance(item1, dict) or not isinstance(item2, dict): return item1 if item2 is _ else item2 @@ -301,11 +303,14 @@ def _merge_dicts(item1, item2, _=Ellipsis): _format['{0}.{1}'.format(id(new), key)] = (new, key, value) new[key] = value return new or _ + _format = {} client = (self.CLIENTS.get(client_name) or self.CLIENTS['web']).copy() client = _merge_dicts(self.CLIENTS['_common'], client) + if data: + client.update(data) client['json']['videoId'] = self.video_id if auth_header and self._access_token: client['_access_token'] = self._access_token @@ -313,8 +318,8 @@ def _merge_dicts(item1, item2, _=Ellipsis): elif 'Authorization' in client['headers']: del client['headers']['Authorization'] - for values, key, value in _format.values(): - if key in values: - values[key] = value.format(**client) + for values, value_key, template_value in _format.values(): + if value_key in values: + values[value_key] = template_value.format(**client) return client From ca18f6a89a096ce6ff2c8e14c8510ea55af1c350 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:10:11 +1100 Subject: [PATCH 065/141] Updates to listing display - Remove unused date/time formatting code - Use locale appropriate date/time display rather than ISO format - Standardise stats item separator to match label mask - Don't bother including all data in label2 as it just duplicates details in skins that use it - Fix non-upcoming videos showing date/time in italics - Fix showing zero value stats - Fix stats with a value of 1 referred to as plural/multiple - Fix incorrect date sorting after e16c7a0 (Kodi datetime need 'T' as seperator) --- .../kodion/context/abstract_context.py | 4 +- .../kodion/context/xbmc/xbmc_context.py | 37 ++++++------------- .../youtube_plugin/kodion/items/base_item.py | 14 +++---- .../youtube_plugin/kodion/items/video_item.py | 4 +- .../kodion/ui/xbmc/info_labels.py | 2 +- .../kodion/ui/xbmc/xbmc_items.py | 19 ++-------- .../kodion/utils/datetime_parser.py | 31 ++++++---------- .../youtube_plugin/youtube/helper/utils.py | 33 +++++++++++------ .../lib/youtube_plugin/youtube/provider.py | 10 ++--- 9 files changed, 67 insertions(+), 87 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 497e949c9..0dd7e2769 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -117,10 +117,10 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): self.parse_params() self._uri = self.create_uri(self._path, self._params) - def format_date_short(self, date_obj): + def format_date_short(self, date_obj, str_format=None): raise NotImplementedError() - def format_time(self, time_obj): + def format_time(self, time_obj, str_format=None): raise NotImplementedError() def get_language(self): diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index a95f6a431..ecfbb28e9 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -8,7 +8,6 @@ See LICENSES/GPL-2.0-only for more information. """ -import datetime import json import os import sys @@ -21,11 +20,11 @@ import xbmcvfs from ..abstract_context import AbstractContext -from ...player.xbmc.xbmc_playlist import XbmcPlaylist +from ... import utils from ...player.xbmc.xbmc_player import XbmcPlayer +from ...player.xbmc.xbmc_playlist import XbmcPlaylist from ...settings.xbmc.xbmc_plugin_settings import XbmcPluginSettings from ...ui.xbmc.xbmc_context_ui import XbmcContextUI -from ... import utils class XbmcContext(AbstractContext): @@ -302,30 +301,18 @@ def is_plugin_path(self, uri, uri_path=''): return uri.startswith('plugin://%s/%s' % (self.get_id(), uri_path)) @staticmethod - def format_date_short(date_obj, short_isoformat=False): - if short_isoformat: - if isinstance(date_obj, datetime.datetime): - date_obj = date_obj.date() - return date_obj.isoformat() - - date_format = xbmc.getRegion('dateshort') - _date_obj = date_obj - if isinstance(_date_obj, datetime.date): - _date_obj = datetime.datetime(_date_obj.year, _date_obj.month, _date_obj.day) - - return _date_obj.strftime(date_format) + def format_date_short(date_obj, str_format=None): + if str_format is None: + str_format = xbmc.getRegion('dateshort') + return date_obj.strftime(str_format) @staticmethod - def format_time(time_obj, short_isoformat=False): - if short_isoformat: - return '{:02d}:{:02d}'.format(time_obj.hour, time_obj.minute) - - time_format = xbmc.getRegion('time') - _time_obj = time_obj - if isinstance(_time_obj, datetime.time): - _time_obj = datetime.time(_time_obj.hour, _time_obj.minute, _time_obj.second) - - return _time_obj.strftime(time_format.replace("%H%H", "%H")) + def format_time(time_obj, str_format=None): + if str_format is None: + str_format = (xbmc.getRegion('time') + .replace("%H%H", "%H") + .replace(':%S', '')) + return time_obj.strftime(str_format) def get_language(self): """ diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 49da304e7..5054f3a29 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -56,10 +56,10 @@ def get_id(self): Returns a unique id of the item. :return: unique id of the item. """ - m = hashlib.md5() - m.update(self._name.encode('utf-8')) - m.update(self._uri.encode('utf-8')) - return m.hexdigest() + md5_hash = hashlib.md5() + md5_hash.update(self._name.encode('utf-8')) + md5_hash.update(self._uri.encode('utf-8')) + return md5_hash.hexdigest() def get_name(self): """ @@ -113,9 +113,9 @@ def get_date(self, as_text=True, short=False): if not self._date: return '' if short: - return self._date.date().isoformat() + return self._date.date().strftime('%x') if as_text: - return self._date.isoformat(sep=' ') + return self._date.strftime('%x %X') return self._date def set_dateadded(self, year, month, day, hour=0, minute=0, second=0): @@ -133,7 +133,7 @@ def get_dateadded(self, as_text=True): if not self._dateadded: return '' if as_text: - return self._dateadded.isoformat(sep=' ') + return self._dateadded.strftime('%x %X') return self._dateadded def set_added_utc(self, date_time): diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 35521f098..234b5c160 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -116,7 +116,7 @@ def get_premiered(self, as_text=True): if not self._premiered: return '' if as_text: - return self._premiered.isoformat() + return self._premiered.strftime('%x') return self._premiered def set_plot(self, plot): @@ -200,7 +200,7 @@ def get_aired(self, as_text=True): if not self._aired: return '' if as_text: - return self._aired.isoformat() + return self._aired.strftime('%x') return self._aired def set_scheduled_start_utc(self, date_time): diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index 9012179e2..35d74db9c 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -19,7 +19,7 @@ def _process_date_value(info_labels, name, param): def _process_datetime_value(info_labels, name, param): if param: - info_labels[name] = param.isoformat(' ') + info_labels[name] = param.isoformat('T') def _process_int_value(info_labels, name, param): diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index a511ca982..e16416ee6 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -11,9 +11,10 @@ from xbmcgui import ListItem from . import info_labels -from ...items import VideoItem, AudioItem, UriItem +from ...items import AudioItem, UriItem, VideoItem from ...utils import datetime_parser + try: from infotagger.listitem import set_info_tag except ImportError: @@ -62,13 +63,7 @@ def video_playback_item(context, video_item): else: kwargs = { 'label': video_item.get_title() or video_item.get_name(), - 'label2': ' | '.join((part - for part in ( - video_item.get_code(), - video_item.get_date(short=True), - video_item.get_duration(as_text=True), - ) - if part)), + 'label2': video_item.get_short_details(), 'path': uri, 'offscreen': True, } @@ -221,13 +216,7 @@ def video_listitem(context, video_item): kwargs = { 'label': video_item.get_title() or video_item.get_name(), - 'label2': ' | '.join((part - for part in ( - video_item.get_code(), - video_item.get_date(short=True), - video_item.get_duration(as_text=True), - ) - if part)), + 'label2': video_item.get_short_details(), 'path': uri, 'offscreen': True, } diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 1f6c4ea33..438cfe511 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -15,21 +15,17 @@ from ..exceptions import KodionException -__RE_MATCH_TIME_ONLY__ = re.compile(r'^(?P[0-9]{2})([:]?(?P[0-9]{2})([:]?(?P[0-9]{2}))?)?$') -__RE_MATCH_DATE_ONLY__ = re.compile(r'^(?P[0-9]{4})[-]?(?P[0-9]{2})[-]?(?P[0-9]{2})$') -__RE_MATCH_DATETIME__ = re.compile(r'^(?P[0-9]{4})[-]?(?P[0-9]{2})[-]?(?P[0-9]{2})["T ](?P[0-9]{2})[:]?(?P[0-9]{2})[:]?(?P[0-9]{2})') +__RE_MATCH_TIME_ONLY__ = re.compile(r'^(?P[0-9]{2})(:?(?P[0-9]{2})(:?(?P[0-9]{2}))?)?$') +__RE_MATCH_DATE_ONLY__ = re.compile(r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})$') +__RE_MATCH_DATETIME__ = re.compile(r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})["T ](?P[0-9]{2}):?(?P[0-9]{2}):?(?P[0-9]{2})') __RE_MATCH_PERIOD__ = re.compile(r'P((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?') __RE_MATCH_ABBREVIATED__ = re.compile(r'(\w+), (?P\d+) (?P\w+) (?P\d+) (?P\d+):(?P\d+):(?P\d+)') -now = time.time() -__LOCAL_OFFSET__ = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) +__LOCAL_OFFSET__ = datetime.now() - datetime.utcnow() __EPOCH_DT__ = datetime.fromtimestamp(0) -now = datetime.now - - def parse(datetime_string, as_utc=True): offset = 0 if as_utc else None @@ -74,7 +70,8 @@ def _to_int(value): offset=offset ) - # period - at the moment we support only hours, minutes and seconds (e.g. videos and audio) + # period - at the moment we support only hours, minutes and seconds + # e.g. videos and audio period_match = __RE_MATCH_PERIOD__.match(datetime_string) if period_match: return timedelta(hours=_to_int(period_match.group('hours')), @@ -97,24 +94,19 @@ def _to_int(value): offset=offset ) - raise KodionException("Could not parse iso 8601 timestamp '%s'" % datetime_string) + raise KodionException('Could not parse |{datetime}| as ISO 8601' + .format(datetime=datetime_string)) def get_scheduled_start(context, datetime_object, local=True): now = datetime.now() if local else datetime.utcnow() if datetime_object.date() == now.date(): return '@ {start_time}'.format( - start_time=context.format_time( - datetime_object.timetz(), short_isoformat=True - ) + start_time=context.format_time(datetime_object.time()) ) return '@ {start_date}, {start_time}'.format( - start_time=context.format_time( - datetime_object.timetz(), short_isoformat=True - ), - start_date=context.format_date_short( - datetime_object.date(), short_isoformat=True - ) + start_time=context.format_time(datetime_object.time()), + start_date=context.format_date_short(datetime_object.date()) ) @@ -189,6 +181,7 @@ def strptime(s, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): fmt = '%Y-%m-%dT%H:%M:%S.%fZ' import _strptime + try: time.strptime('01 01 2012', '%d %m %Y') # dummy call except: diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 35a58ff17..c7a3d3acf 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -422,22 +422,32 @@ def update_video_infos(provider, context, video_id_dict, if 'statistics' in yt_item: for stat, value in yt_item['statistics'].items(): label = context.LOCAL_MAP.get('stats.' + stat) - if label: - color = __COLOR_MAP.get(stat, 'white') - str_value, value = utils.friendly_number(value) - label_stats.append(ui.color(color, str_value)) - stats.append(ui.color(color, ui.bold(' '.join(( - str_value, context.localize(label) - ))))) - else: + if not label: + continue + + str_value, value = utils.friendly_number(value) + if not value: continue + + color = __COLOR_MAP.get(stat, 'white') + label = context.localize(label) + if value == 1: + label = label.rstrip('s') + + label_stats.append(ui.color(color, str_value)) + stats.append(ui.color(color, ui.bold(' '.join(( + str_value, label + ))))) + if stat == 'likeCount': rating[0] = value elif stat == 'viewCount': rating[1] = value video_item.set_count(value) - label_stats = '|'.join(label_stats) - stats = '|'.join(stats) + + label_stats = ' | '.join(label_stats) + stats = ' | '.join(stats) + if 0 < rating[0] <= rating[1]: if rating[0] == rating[1]: rating = 10 @@ -482,7 +492,8 @@ def update_video_infos(provider, context, video_id_dict, description = ''.join(( ui.bold(channel_name, cr_after=2) if channel_name else '', ui.new_line(stats, cr_after=1) if stats else '', - ui.italic(start_at, cr_after=1) if start_at else '', + (ui.italic(start_at, cr_after=1) if video_item.upcoming + else ui.new_line(start_at, cr_after=1)) if start_at else '', ui.new_line() if stats or start_at else '', description, )) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 1baa3caf4..4cfe07f5f 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1446,18 +1446,18 @@ def on_root(self, context, re_match): def set_content_type(context, content_type): context.set_content_type(content_type) context.add_sort_method( - (constants.sort_method.UNSORTED, '%T', '%P | %J | %D'), - (constants.sort_method.LABEL_IGNORE_THE, '%T', '%P | %J | %D'), + (constants.sort_method.UNSORTED, '%T \u2022 %P', '%D | %J'), + (constants.sort_method.LABEL_IGNORE_THE, '%T \u2022 %P', '%D | %J'), ) if content_type != constants.content_type.VIDEOS: return context.add_sort_method( - (constants.sort_method.PROGRAM_COUNT, '%T \u2022 %P | %J | %D', '%C'), - (constants.sort_method.VIDEO_RATING, '%T \u2022 %P | %J | %D', '%R'), + (constants.sort_method.PROGRAM_COUNT, '%T \u2022 %P | %D | %J', '%C'), + (constants.sort_method.VIDEO_RATING, '%T \u2022 %P | %D | %J', '%R'), (constants.sort_method.DATE, '%T \u2022 %P | %D', '%J'), (constants.sort_method.DATEADDED, '%T \u2022 %P | %D', '%a'), (constants.sort_method.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'), - (constants.sort_method.TRACKNUM, '[%N. ]%T', '%P | %J | %D'), + (constants.sort_method.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'), ) def handle_exception(self, context, exception_to_handle): From cceb4c90d3b9dce744869d07401a6569fcdb75ec Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:51:25 +1100 Subject: [PATCH 066/141] Disable Opus audio by default - Workaround for #537 --- resources/settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/settings.xml b/resources/settings.xml index f13b761e1..bf0da9d9b 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -263,7 +263,7 @@ 0 - avc1,vp9,av01,hdr,hfr,vorbis,opus,mp4a,ssa,ac-3,ec-3,dts,filter + avc1,vp9,av01,hdr,hfr,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter From e26e85927bb436d803e2007fd70d4fe94a94ec89 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 8 Dec 2023 09:58:59 +1100 Subject: [PATCH 067/141] Remove debugging code --- resources/lib/default.py | 8 +------- resources/lib/youtube_plugin/kodion/logger.py | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/resources/lib/default.py b/resources/lib/default.py index bbc54e2c9..10385f513 100644 --- a/resources/lib/default.py +++ b/resources/lib/default.py @@ -8,14 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ -from xbmc import log - -from youtube_plugin import youtube from youtube_plugin.kodion import runner -from youtube_plugin.kodion.debug import Profiler - +from youtube_plugin import youtube -profiler = Profiler(enabled=True, lazy=False) __provider__ = youtube.Provider() runner.run(__provider__) -log(profiler.get_stats(), 1) diff --git a/resources/lib/youtube_plugin/kodion/logger.py b/resources/lib/youtube_plugin/kodion/logger.py index c97587d21..df709b3bb 100644 --- a/resources/lib/youtube_plugin/kodion/logger.py +++ b/resources/lib/youtube_plugin/kodion/logger.py @@ -11,7 +11,7 @@ import xbmc import xbmcaddon -DEBUG = xbmc.LOGINFO +DEBUG = xbmc.LOGDEBUG INFO = xbmc.LOGINFO NOTICE = INFO WARNING = xbmc.LOGWARNING From 0111bf6404d1ac97dad106bbd1f71fb08a6c775c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 9 Dec 2023 18:26:03 +1100 Subject: [PATCH 068/141] Fix incorrect method signatures --- .../lib/youtube_plugin/kodion/context/abstract_context.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 0dd7e2769..72fe8361f 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -117,10 +117,12 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): self.parse_params() self._uri = self.create_uri(self._path, self._params) - def format_date_short(self, date_obj, str_format=None): + @staticmethod + def format_date_short(date_obj, str_format=None): raise NotImplementedError() - def format_time(self, time_obj, str_format=None): + @staticmethod + def format_time(time_obj, str_format=None): raise NotImplementedError() def get_language(self): From 928745d30828a0e67b25165fa698da674de00b4c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 9 Dec 2023 18:40:43 +1100 Subject: [PATCH 069/141] Fix datetime_parser.now being removed - Still used in YouTube._get_recommendations_for_home - TODO: remove once _get_recommendations_for_home is fixed --- .../kodion/utils/datetime_parser.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 438cfe511..16d4cefbb 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -15,13 +15,15 @@ from ..exceptions import KodionException +now = datetime.now + __RE_MATCH_TIME_ONLY__ = re.compile(r'^(?P[0-9]{2})(:?(?P[0-9]{2})(:?(?P[0-9]{2}))?)?$') __RE_MATCH_DATE_ONLY__ = re.compile(r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})$') __RE_MATCH_DATETIME__ = re.compile(r'^(?P[0-9]{4})[-/.]?(?P[0-9]{2})[-/.]?(?P[0-9]{2})["T ](?P[0-9]{2}):?(?P[0-9]{2}):?(?P[0-9]{2})') __RE_MATCH_PERIOD__ = re.compile(r'P((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?') __RE_MATCH_ABBREVIATED__ = re.compile(r'(\w+), (?P\d+) (?P\w+) (?P\d+) (?P\d+):(?P\d+):(?P\d+)') -__LOCAL_OFFSET__ = datetime.now() - datetime.utcnow() +__LOCAL_OFFSET__ = now() - datetime.utcnow() __EPOCH_DT__ = datetime.fromtimestamp(0) @@ -99,8 +101,8 @@ def _to_int(value): def get_scheduled_start(context, datetime_object, local=True): - now = datetime.now() if local else datetime.utcnow() - if datetime_object.date() == now.date(): + _now = now() if local else datetime.utcnow() + if datetime_object.date() == _now: return '@ {start_time}'.format( start_time=context.format_time(datetime_object.time()) ) @@ -116,12 +118,12 @@ def utc_to_local(dt, offset=None): def datetime_to_since(context, dt): - now = datetime.now() - diff = now - dt - yesterday = now - timedelta(days=1) - yyesterday = now - timedelta(days=2) - use_yesterday = (now - yesterday).total_seconds() > 10800 - today = now.date() + _now = now() + diff = _now - dt + yesterday = _now - timedelta(days=1) + yyesterday = _now - timedelta(days=2) + use_yesterday = (_now - yesterday).total_seconds() > 10800 + today = _now.date() tomorrow = today + timedelta(days=1) seconds = diff.total_seconds() From 446b85aaa185fcd6a01ea5f5bd6221471aa6ef5f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 01:09:07 +1100 Subject: [PATCH 070/141] Add settings methods for WL and HL --- .../kodion/constants/const_settings.py | 3 +++ .../kodion/settings/abstract_settings.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index da0308bf4..0b299f170 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -57,3 +57,6 @@ API_SECRET = 'youtube.api.secret' # (string) CLIENT_SELECTION = 'youtube.client.selection' # (int) + +WATCH_LATER_PLAYLIST = 'youtube.folder.watch_later.playlist' # (str) +HISTORY_PLAYLIST = 'youtube.folder.history.playlist' # (str) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index e5cabea63..6e1c65e97 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -319,3 +319,15 @@ def show_detailed_description(self): def get_language(self): return self.get_string(SETTINGS.LANGUAGE, 'en_US').replace('_', '-') + + def get_watch_later_playlist(self): + return self.get_string(SETTINGS.WATCH_LATER_PLAYLIST, '').strip() + + def set_watch_later_playlist(self, value): + return self.set_string(SETTINGS.WATCH_LATER_PLAYLIST, value) + + def get_history_playlist(self): + return self.get_string(SETTINGS.HISTORY_PLAYLIST, '').strip() + + def set_history_playlist(self, value): + return self.set_string(SETTINGS.HISTORY_PLAYLIST, value) From 737d31dfc4f28ce82aaaa12cdb48caaf46c41094 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 01:14:14 +1100 Subject: [PATCH 071/141] Better handle failed playlist request - Fix #545 - Also consolidate progress dialog methods --- .../kodion/ui/abstract_progress_dialog.py | 27 +++++++++++-- .../kodion/ui/xbmc/xbmc_context_ui.py | 3 +- .../kodion/ui/xbmc/xbmc_progress_dialog.py | 32 ++++++--------- .../kodion/ui/xbmc/xbmc_progress_dialog_bg.py | 40 ------------------- .../youtube_plugin/youtube/helper/yt_play.py | 9 +++-- 5 files changed, 43 insertions(+), 68 deletions(-) delete mode 100644 resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog_bg.py diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py index 743711ba6..0af140254 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py @@ -10,9 +10,14 @@ class AbstractProgressDialog(object): - def __init__(self, total=100): + def __init__(self, dialog, heading, text, total=100): + self._dialog = dialog() + self._dialog.create(heading, text) + + # simple reset because KODI won't do it :( self._total = int(total) - self._position = 0 + self._position = 1 + self.update(steps=-1) def get_total(self): return self._total @@ -21,13 +26,27 @@ def get_position(self): return self._position def close(self): - raise NotImplementedError() + if self._dialog: + self._dialog.close() + self._dialog = None def set_total(self, total): self._total = int(total) def update(self, steps=1, text=None): - raise NotImplementedError() + self._position += steps + + if not self._total: + position = 0 + elif self._position >= self._total: + position = 100 + else: + position = int(100 * self._position / self._total) + + if isinstance(text, str): + self._dialog.update(percent=position, message=text) + else: + self._dialog.update(percent=position) def is_aborted(self): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 032380c87..ce6eb53cc 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -11,8 +11,7 @@ import xbmc import xbmcgui -from .xbmc_progress_dialog import XbmcProgressDialog -from .xbmc_progress_dialog_bg import XbmcProgressDialogBG +from .xbmc_progress_dialog import XbmcProgressDialog, XbmcProgressDialogBG from ..abstract_context_ui import AbstractContextUI from ... import utils diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py index a1f061b99..fda3cde27 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py @@ -14,27 +14,21 @@ class XbmcProgressDialog(AbstractProgressDialog): def __init__(self, heading, text): - super(XbmcProgressDialog, self).__init__(100) - self._dialog = xbmcgui.DialogProgress() - self._dialog.create(heading, text) + super(XbmcProgressDialog, self).__init__(xbmcgui.DialogProgress, + heading, + text, + 100) - # simple reset because KODI won't do it :( - self._position = 1 - self.update(steps=-1) - - def close(self): - if self._dialog: - self._dialog.close() - self._dialog = None + def is_aborted(self): + return self._dialog.iscanceled() - def update(self, steps=1, text=None): - self._position += steps - position = int(float((100.0 // self._total)) * self._position) - if isinstance(text, str): - self._dialog.update(position, text) - else: - self._dialog.update(position) +class XbmcProgressDialogBG(AbstractProgressDialog): + def __init__(self, heading, text): + super(XbmcProgressDialogBG, self).__init__(xbmcgui.DialogProgressBG, + heading, + text, + 100) def is_aborted(self): - return self._dialog.iscanceled() + return False diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog_bg.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog_bg.py deleted file mode 100644 index 222cde7f7..000000000 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog_bg.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -import xbmcgui -from ..abstract_progress_dialog import AbstractProgressDialog - - -class XbmcProgressDialogBG(AbstractProgressDialog): - def __init__(self, heading, text): - super(XbmcProgressDialogBG, self).__init__(100) - self._dialog = xbmcgui.DialogProgressBG() - self._dialog.create(heading, text) - - # simple reset because KODI won't do it :( - self._position = 1 - self.update(steps=-1) - - def close(self): - if self._dialog: - self._dialog.close() - self._dialog = None - - def update(self, steps=1, text=None): - self._position += steps - position = int((100.0 / float(self._total)) * float(self._position)) - - if isinstance(text, str): - self._dialog.update(percent=position, message=text) - else: - self._dialog.update(percent=position) - - def is_aborted(self): - return False diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 130e7d1e0..07d22c2c4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -159,11 +159,14 @@ def play_playlist(provider, context): while page_token is not None: json_data = client.get_playlist_items(playlist_id, page_token) if not v3.handle_error(context, json_data): - return None + break if page_token == 0: - total += int(json_data.get('pageInfo', {}) - .get('totalResults', 0)) + playlist_total = int(json_data.get('pageInfo', {}) + .get('totalResults', 0)) + if not playlist_total: + break + total += playlist_total progress_dialog.set_total(total) result = v3.response_to_items(provider, From a3dc87986cc84c19848a386ee9b2043b47e554db Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 01:28:16 +1100 Subject: [PATCH 072/141] Better handle seeking out of a Clip - This may need to be tweaked after more testing --- resources/lib/youtube_plugin/kodion/utils/player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index 1e0e40ab9..b2dcce605 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -507,9 +507,9 @@ def onPlayBackError(self): self.onPlayBackEnded() def onPlayBackSeek(self, time, seekOffset): - time_s = time // 1000 + time_s = time / 1000 self.seek_time = None - if ((self.end_time and time_s > self.end_time) - or (self.start_time and time_s < self.start_time)): + if ((self.end_time and time_s > self.end_time + 1) + or (self.start_time and time_s < self.start_time - 1)): self.start_time = None self.end_time = None From ea63dfdad61cc7120d7a9bbee645f002461a3dc6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 01:30:14 +1100 Subject: [PATCH 073/141] Make merge_dicts a common utils method --- .../youtube_plugin/kodion/utils/__init__.py | 2 ++ .../youtube_plugin/kodion/utils/methods.py | 18 +++++++++++++ .../youtube/client/request_client.py | 26 +++++-------------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 0cd6e0e08..bffb440ce 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -18,6 +18,7 @@ friendly_number, loose_version, make_dirs, + merge_dicts, seconds_to_duration, select_stream, strip_html_from_text, @@ -47,6 +48,7 @@ 'friendly_number', 'loose_version', 'make_dirs', + 'merge_dicts', 'seconds_to_duration', 'select_stream', 'strip_html_from_text', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index a88e2475b..a648a81eb 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -27,6 +27,7 @@ 'friendly_number', 'loose_version', 'make_dirs', + 'merge_dicts', 'print_items', 'seconds_to_duration', 'select_stream', @@ -297,3 +298,20 @@ def duration_to_seconds(duration): def seconds_to_duration(seconds): return str(timedelta(seconds=seconds)) + + +def merge_dicts(item1, item2, templates=None, _=Ellipsis): + if not isinstance(item1, dict) or not isinstance(item2, dict): + return item1 if item2 is _ else item2 + new = {} + keys = set(item1) + keys.update(item2) + for key in keys: + value = merge_dicts(item1.get(key, _), item2.get(key, _), templates) + if value is _: + continue + if (templates is not None + and isinstance(value, str) and '{' in value): + templates['{0}.{1}'.format(id(new), key)] = (new, key, value) + new[key] = value + return new or _ diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 0c31c0291..603bc3233 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -7,6 +7,7 @@ See LICENSES/GPL-2.0-only for more information. """ +from ...kodion.utils import merge_dicts from ...kodion.network import BaseRequestsClass from ...youtube.youtube_exceptions import YouTubeException @@ -289,25 +290,10 @@ def json_traverse(json_data, path): return result def build_client(self, client_name, auth_header=False, data=None): - def _merge_dicts(item1, item2, _=Ellipsis): - if not isinstance(item1, dict) or not isinstance(item2, dict): - return item1 if item2 is _ else item2 - new = {} - keys = set(item1) - keys.update(item2) - for key in keys: - value = _merge_dicts(item1.get(key, _), item2.get(key, _)) - if value is _: - continue - if isinstance(value, str) and '{' in value: - _format['{0}.{1}'.format(id(new), key)] = (new, key, value) - new[key] = value - return new or _ - - _format = {} + templates = {} client = (self.CLIENTS.get(client_name) or self.CLIENTS['web']).copy() - client = _merge_dicts(self.CLIENTS['_common'], client) + client = merge_dicts(self.CLIENTS['_common'], client, templates) if data: client.update(data) @@ -318,8 +304,8 @@ def _merge_dicts(item1, item2, _=Ellipsis): elif 'Authorization' in client['headers']: del client['headers']['Authorization'] - for values, value_key, template_value in _format.values(): - if value_key in values: - values[value_key] = template_value.format(**client) + for values, template_id, template in templates.values(): + if template_id in values: + values[template_id] = template.format(**client) return client From f5742c8077e94262ba4e3d2e6ac32ee1a5ee2014 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 01:54:03 +1100 Subject: [PATCH 074/141] Make AccessManager inherit from JSONStore directly - Remove LoginTokenStore and login_tokens.py - Prevents issues with possible circular imports - Move access_manager out of kodion.utils and into kodion.json_store - Remove JSONStore from kodion.json_store.__all__ - JSONStore.save can now partially update data - JSONStore.load/save/get_data can now process data after JSON decoding - TODO: Fix usage of AccessManager in youtube.client.__config__.APICheck to avoid keeping two copies of the data - TODO: Fix usage of APIKeyStore to avoid keeping two copies of the data --- .../kodion/context/abstract_context.py | 2 +- .../kodion/json_store/__init__.py | 5 +- .../kodion/json_store/access_manager.py | 599 ++++++++++++++++++ .../kodion/json_store/api_keys.py | 2 +- .../kodion/json_store/json_store.py | 34 +- .../kodion/json_store/login_tokens.py | 99 --- .../youtube_plugin/kodion/utils/__init__.py | 2 - .../kodion/utils/access_manager.py | 418 ------------ .../youtube/client/__config__.py | 14 +- .../lib/youtube_plugin/youtube/provider.py | 6 +- 10 files changed, 633 insertions(+), 548 deletions(-) create mode 100644 resources/lib/youtube_plugin/kodion/json_store/access_manager.py delete mode 100644 resources/lib/youtube_plugin/kodion/json_store/login_tokens.py delete mode 100644 resources/lib/youtube_plugin/kodion/utils/access_manager.py diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 72fe8361f..99af7c3fb 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -13,10 +13,10 @@ from .. import constants from .. import logger +from ..json_store import AccessManager from ..utils import ( create_path, create_uri_path, - AccessManager, DataCache, FavoriteList, FunctionCache, diff --git a/resources/lib/youtube_plugin/kodion/json_store/__init__.py b/resources/lib/youtube_plugin/kodion/json_store/__init__.py index 1b8044317..96383f1c0 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/__init__.py +++ b/resources/lib/youtube_plugin/kodion/json_store/__init__.py @@ -7,9 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ -from .json_store import JSONStore +from .access_manager import AccessManager from .api_keys import APIKeyStore -from .login_tokens import LoginTokenStore -__all__ = ('APIKeyStore', 'JSONStore', 'LoginTokenStore',) +__all__ = ('AccessManager', 'APIKeyStore',) diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py new file mode 100644 index 000000000..c5264ca67 --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -0,0 +1,599 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2018 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +import time +import uuid +from hashlib import md5 + +from .json_store import JSONStore + + +__author__ = 'bromix' + + +class AccessManager(JSONStore): + DEFAULT_NEW_USER = { + 'access_token': '', + 'refresh_token': '', + 'token_expires': -1, + 'last_key_hash': '', + 'name': 'Default', + 'watch_later': ' WL', + 'watch_history': 'HL' + } + + def __init__(self, context): + super(AccessManager, self).__init__('access_manager.json') + self._settings = context.get_settings() + access_manager_data = self._data['access_manager'] + self._user = access_manager_data.get('current_user', 0) + self._last_origin = access_manager_data.get('last_origin', + 'plugin.video.youtube') + + def set_defaults(self, reset=False): + data = {} if reset else self.get_data() + if 'access_manager' not in data: + data = { + 'access_manager': { + 'users': { + 0: self.DEFAULT_NEW_USER.copy() + } + } + } + if 'users' not in data['access_manager']: + data['access_manager']['users'] = { + 0: self.DEFAULT_NEW_USER.copy() + } + if 0 not in data['access_manager']['users']: + data['access_manager']['users'][0] = self.DEFAULT_NEW_USER.copy() + if 'current_user' not in data['access_manager']: + data['access_manager']['current_user'] = 0 + if 'last_origin' not in data['access_manager']: + data['access_manager']['last_origin'] = 'plugin.video.youtube' + if 'developers' not in data['access_manager']: + data['access_manager']['developers'] = {} + + # clean up + if data['access_manager']['current_user'] == 'default': + data['access_manager']['current_user'] = 0 + if 'access_token' in data['access_manager']: + del data['access_manager']['access_token'] + if 'refresh_token' in data['access_manager']: + del data['access_manager']['refresh_token'] + if 'token_expires' in data['access_manager']: + del data['access_manager']['token_expires'] + if 'default' in data['access_manager']: + if ((data['access_manager']['default'].get('access_token') + or data['access_manager']['default'].get('refresh_token')) + and not data['access_manager']['users'][0].get( + 'access_token') + and not data['access_manager']['users'][0].get( + 'refresh_token')): + if 'name' not in data['access_manager']['default']: + data['access_manager']['default']['name'] = 'Default' + data['access_manager']['users'][0] = data['access_manager'][ + 'default'] + del data['access_manager']['default'] + # end clean up + + current_user = data['access_manager']['current_user'] + if 'watch_later' not in data['access_manager']['users'][current_user]: + data['access_manager']['users'][current_user]['watch_later'] = ' WL' + if 'watch_history' not in data['access_manager']['users'][current_user]: + data['access_manager']['users'][current_user][ + 'watch_history'] = 'HL' + + # ensure all users have uuid + uuids = set() + for user in data['access_manager']['users'].values(): + c_uuid = user.get('id') + while not c_uuid or c_uuid in uuids: + c_uuid = uuid.uuid4().hex + uuids.add(c_uuid) + user['id'] = c_uuid + # end uuid check + + self.save(data) + + @staticmethod + def _process_data(data): + # process users, change str keys (old format) to int (current format) + users = data['access_manager']['users'] + if '0' in users: + data['access_manager']['users'] = { + int(key): value + for key, value in users.items() + } + return data + + def get_data(self, process=_process_data.__func__): + return super(AccessManager, self).get_data(process) + + def load(self, process=_process_data.__func__): + return super(AccessManager, self).load(process) + + def save(self, data, update=False, process=_process_data.__func__): + return super(AccessManager, self).save(data, update, process) + + def get_current_user_details(self): + """ + :return: current user + """ + return self.get_users()[self._user] + + def get_current_user_id(self): + """ + :return: uuid of the current user + """ + return self.get_users()[self._user]['id'] + + def get_new_user(self, username=''): + """ + :param username: string, users name + :return: a new user dict + """ + uuids = [ + user.get('id') + for user in self.get_users().values() + ] + new_uuid = None + while not new_uuid or new_uuid in uuids: + new_uuid = uuid.uuid4().hex + return { + 'access_token': '', + 'refresh_token': '', + 'token_expires': -1, + 'last_key_hash': '', + 'name': username, + 'id': new_uuid, + 'watch_later': ' WL', + 'watch_history': 'HL' + } + + def get_users(self): + """ + Returns users + :return: users + """ + return self._data['access_manager'].get('users', {}) + + def add_user(self, username='', user=None): + """ + Add single new user to users collection + :param username: str, chosen name of new user + :param user: int, optional index for new user + :return: tuple, (index, details) of newly added user + """ + users = self.get_users() + new_user_details = self.get_new_user(username) + new_user = max(users) + 1 if users and user is None else user or 0 + users[new_user] = new_user_details + data = { + 'access_manager': { + 'users': users + } + } + self.save(data, update=True) + return new_user, new_user_details + + def remove_user(self, user): + """ + Remove user from collection of current users + :param user: int, user index + :return: + """ + users = self.get_users() + if user in users: + del users[user] + data = { + 'access_manager': { + 'users': users + } + } + self.save(data, update=True) + + def set_users(self, users): + """ + Updates all users + :param users: dict, users + :return: + """ + data = self.get_data() + data['access_manager']['users'] = users + self.save(data) + + def set_user(self, user, switch_to=False): + """ + Updates the user + :param user: string, username + :param switch_to: boolean, change current user + :return: + """ + self._user = user + if switch_to: + data = { + 'access_manager': { + 'current_user': user + } + } + self.save(data, update=True) + + def get_current_user(self): + """ + Returns the current user + :return: user + """ + return self._user + + def get_username(self, user=None): + """ + Returns the username of the current or nominated user + :return: username + """ + if user is None: + user = self._user + users = self.get_users() + if user in users: + return users[user].get('name') + return '' + + def set_username(self, user, username): + """ + Sets the username of the nominated user + :return: True if username was set, false otherwise + """ + users = self.get_users() + if user in users: + users[user]['name'] = username + data = { + 'access_manager': { + 'users': users + } + } + self.save(data, update=True) + return True + return False + + def get_watch_later_id(self): + """ + Returns the current users watch later playlist id + :return: the current users watch later playlist id + """ + updated = False + watch_later_ids = ('wl', ' wl') + + current_user = self.get_current_user_details() + current_playlist_id = current_user.get('watch_later', '') + settings_playlist_id = self._settings.get_watch_later_playlist() + + if settings_playlist_id.lower().startswith(watch_later_ids): + self._settings.set_watch_later_playlist('') + settings_playlist_id = '' + + if current_playlist_id.lower().startswith(watch_later_ids): + updated = True + current_user['watch_later'] = settings_playlist_id + self._settings.set_watch_later_playlist('') + settings_playlist_id = '' + + if settings_playlist_id and current_playlist_id != settings_playlist_id: + updated = True + current_user['watch_later'] = settings_playlist_id + self._settings.set_watch_later_playlist('') + + if updated: + data = { + 'access_manager': { + 'users': { + self._user: current_user + } + } + } + self.save(data, update=True) + + return current_user.get('watch_later', '') + + def set_watch_later_id(self, playlist_id): + """ + Sets the current users watch later playlist id + :param playlist_id: string, watch later playlist id + :return: + """ + if playlist_id.lower() == 'wl' or playlist_id.lower() == ' wl': + playlist_id = '' + + current_user = self.get_current_user_details() + current_user['watch_later'] = playlist_id + self._settings.set_watch_later_playlist('') + data = { + 'access_manager': { + 'users': { + self._user: current_user + } + } + } + self.save(data, update=True) + + def get_watch_history_id(self): + """ + Returns the current users watch history playlist id + :return: the current users watch history playlist id + """ + current_user = self.get_current_user_details() + current_playlist_id = current_user.get('watch_history', 'HL') + settings_playlist_id = self._settings.get_history_playlist() + + if settings_playlist_id and current_playlist_id != settings_playlist_id: + current_user['watch_history'] = settings_playlist_id + self._settings.set_history_playlist('') + data = { + 'access_manager': { + 'users': { + self._user: current_user + } + } + } + self.save(data, update=True) + + return current_user.get('watch_history', 'HL') + + def set_watch_history_id(self, playlist_id): + """ + Sets the current users watch history playlist id + :param playlist_id: string, watch history playlist id + :return: + """ + current_user = self.get_current_user_details() + current_user['watch_history'] = playlist_id + self._settings.set_history_playlist('') + data = { + 'access_manager': { + 'users': { + self._user: current_user + } + } + } + self.save(data, update=True) + + def set_last_origin(self, origin): + """ + Updates the origin + :param origin: string, origin + :return: + """ + self._last_origin = origin + data = { + 'access_manager': { + 'last_origin': origin + } + } + self.save(data, update=True) + + def get_last_origin(self): + """ + Returns the last origin + :return: + """ + return self._last_origin + + def get_access_token(self): + """ + Returns the access token for some API + :return: access_token + """ + return self.get_current_user_details().get('access_token', '') + + def get_refresh_token(self): + """ + Returns the refresh token + :return: refresh token + """ + return self.get_current_user_details().get('refresh_token', '') + + def has_refresh_token(self): + return self.get_refresh_token() != '' + + def is_access_token_expired(self): + """ + Returns True if the access_token is expired otherwise False. + If no expiration date was provided and an access_token exists + this method will always return True + :return: + """ + current_user = self.get_current_user_details() + access_token = current_user.get('access_token', '') + expires = int(current_user.get('token_expires', -1)) + + # with no access_token it must be expired + if not access_token: + return True + + # in this case no expiration date was set + if expires == -1: + return False + + now = int(time.time()) + return expires <= now + + def update_access_token(self, + access_token, + unix_timestamp=None, + refresh_token=None): + """ + Updates the old access token with the new one. + :param access_token: + :param unix_timestamp: + :param refresh_token: + :return: + """ + current_user = self.get_current_user_details() + current_user['access_token'] = access_token + + if unix_timestamp is not None: + current_user['token_expires'] = int(unix_timestamp) + + if refresh_token is not None: + current_user['refresh_token'] = refresh_token + + data = { + 'access_manager': { + 'users': { + self._user: current_user + } + } + } + self.save(data, update=True) + + @staticmethod + def get_new_developer(): + """ + :return: a new developer dict + """ + return { + 'access_token': '', + 'refresh_token': '', + 'token_expires': -1, + 'last_key_hash': '' + } + + def get_developers(self): + """ + Returns developers + :return: dict, developers + """ + return self._data['access_manager'].get('developers', {}) + + def get_developer(self, addon_id): + return self.get_developers().get(addon_id, {}) + + def set_developers(self, developers): + """ + Updates the users + :param developers: dict, developers + :return: + """ + data = self.get_data() + data['access_manager']['developers'] = developers + self.save(data) + + def get_dev_access_token(self, addon_id): + """ + Returns the access token for some API + :param addon_id: addon id + :return: access_token + """ + return self.get_developer(addon_id).get('access_token', '') + + def get_dev_refresh_token(self, addon_id): + """ + Returns the refresh token + :return: refresh token + """ + return self.get_developer(addon_id).get('refresh_token', '') + + def developer_has_refresh_token(self, addon_id): + return self.get_dev_refresh_token(addon_id) != '' + + def is_dev_access_token_expired(self, addon_id): + """ + Returns True if the access_token is expired otherwise False. + If no expiration date was provided and an access_token exists + this method will always return True + :return: + """ + developer = self.get_developer(addon_id) + access_token = developer.get('access_token', '') + expires = int(developer.get('token_expires', -1)) + + # with no access_token it must be expired + if not access_token: + return True + + # in this case no expiration date was set + if expires == -1: + return False + + now = int(time.time()) + return expires <= now + + def update_dev_access_token(self, + addon_id, + access_token, + unix_timestamp=None, + refresh_token=None): + """ + Updates the old access token with the new one. + :param addon_id: + :param access_token: + :param unix_timestamp: + :param refresh_token: + :return: + """ + developer = self.get_developer(addon_id) + developer['access_token'] = access_token + + if unix_timestamp is not None: + developer['token_expires'] = int(unix_timestamp) + + if refresh_token is not None: + developer['refresh_token'] = refresh_token + + data = { + 'access_manager': { + 'developers': { + addon_id: developer + } + } + } + self.save(data, update=True) + + def get_dev_last_key_hash(self, addon_id): + return self.get_developer(addon_id).get('last_key_hash', '') + + def set_dev_last_key_hash(self, addon_id, key_hash): + developer = self.get_developer(addon_id) + developer['last_key_hash'] = key_hash + data = { + 'access_manager': { + 'developers': { + addon_id: developer + } + } + } + self.save(data, update=True) + + def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): + last_hash = self.get_dev_last_key_hash(addon_id) + current_hash = self.__calc_key_hash(api_key, client_id, client_secret) + + if not last_hash and current_hash: + self.set_dev_last_key_hash(addon_id, current_hash) + return False + + if last_hash != current_hash: + self.set_dev_last_key_hash(addon_id, current_hash) + return True + + return False + + @staticmethod + def __calc_key_hash(api_key, client_id, client_secret): + + md5_hash = md5() + try: + md5_hash.update(api_key.encode('utf-8')) + md5_hash.update(client_id.encode('utf-8')) + md5_hash.update(client_secret.encode('utf-8')) + except: + md5_hash.update(api_key) + md5_hash.update(client_id) + md5_hash.update(client_secret) + + return md5_hash.hexdigest() diff --git a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py index aeaf8d0cd..3c20a22c4 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py +++ b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py @@ -7,7 +7,7 @@ See LICENSES/GPL-2.0-only for more information. """ -from . import JSONStore +from .json_store import JSONStore class APIKeyStore(JSONStore): diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 577f431d1..09d5e5f9b 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -10,19 +10,20 @@ import json import os -import xbmcaddon +from xbmcaddon import Addon import xbmcvfs from ..logger import log_debug, log_error -from ..utils import make_dirs +from ..utils import make_dirs, merge_dicts + + +_addon_id = 'plugin.video.youtube' +_addon = Addon(_addon_id) class JSONStore(object): def __init__(self, filename): - addon_id = 'plugin.video.youtube' - addon = xbmcaddon.Addon(addon_id) - - self.base_path = xbmcvfs.translatePath(addon.getAddonInfo('profile')) + self.base_path = xbmcvfs.translatePath(_addon.getAddonInfo('profile')) if not xbmcvfs.exists(self.base_path) and not make_dirs(self.base_path): log_error('JSONStore.__init__ |{path}| invalid path'.format( @@ -38,8 +39,10 @@ def __init__(self, filename): def set_defaults(self, reset=False): raise NotImplementedError - def save(self, data): - if data == self._data: + def save(self, data, update=False, process=None): + if update: + data = merge_dicts(self._data, data) + elif data == self._data: log_debug('JSONStore.save |{filename}| data unchanged'.format( filename=self.filename )) @@ -53,7 +56,7 @@ def save(self, data): _data = json.loads(json.dumps(data)) with open(self.filename, mode='w', encoding='utf-8') as jsonfile: json.dump(_data, jsonfile, indent=4, sort_keys=True) - self._data = _data + self._data = process(_data) if process is not None else _data except (IOError, OSError): log_error('JSONStore.save |{filename}| no access to file'.format( filename=self.filename @@ -65,7 +68,7 @@ def save(self, data): )) self.set_defaults(reset=True) - def load(self): + def load(self, process=None): log_debug('JSONStore.load |{filename}|'.format( filename=self.filename )) @@ -74,7 +77,8 @@ def load(self): data = jsonfile.read() if not data: raise ValueError - self._data = json.loads(data) + _data = json.loads(data) + self._data = process(_data) if process is not None else _data except (IOError, OSError): log_error('JSONStore.load |{filename}| no access to file'.format( filename=self.filename @@ -84,14 +88,16 @@ def load(self): data=data )) - def get_data(self): + def get_data(self, process=None): try: if not self._data: raise ValueError - return json.loads(json.dumps(self._data)) + _data = json.loads(json.dumps(self._data)) + return process(_data) if process is not None else _data except (TypeError, ValueError): log_error('JSONStore.get_data |{data}| invalid data'.format( data=self._data )) self.set_defaults(reset=True) - return json.loads(json.dumps(self._data)) + _data = json.loads(json.dumps(self._data)) + return process(_data) if process is not None else _data diff --git a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py b/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py deleted file mode 100644 index dedd70a94..000000000 --- a/resources/lib/youtube_plugin/kodion/json_store/login_tokens.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2018-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -import uuid -from . import JSONStore - - -# noinspection PyTypeChecker -class LoginTokenStore(JSONStore): - DEFAULT_NEW_USER = { - 'access_token': '', - 'refresh_token': '', - 'token_expires': -1, - 'last_key_hash': '', - 'name': 'Default', - 'watch_later': ' WL', - 'watch_history': 'HL' - } - - def __init__(self): - super(LoginTokenStore, self).__init__('access_manager.json') - - def set_defaults(self, reset=False): - data = {} if reset else self.get_data() - if 'access_manager' not in data: - data = { - 'access_manager': { - 'users': { - 0: self.DEFAULT_NEW_USER.copy() - } - } - } - if 'users' not in data['access_manager']: - data['access_manager']['users'] = { - 0: self.DEFAULT_NEW_USER.copy() - } - if 0 not in data['access_manager']['users']: - data['access_manager']['users'][0] = self.DEFAULT_NEW_USER.copy() - if 'current_user' not in data['access_manager']: - data['access_manager']['current_user'] = 0 - if 'last_origin' not in data['access_manager']: - data['access_manager']['last_origin'] = 'plugin.video.youtube' - if 'developers' not in data['access_manager']: - data['access_manager']['developers'] = {} - - # clean up - if data['access_manager']['current_user'] == 'default': - data['access_manager']['current_user'] = 0 - if 'access_token' in data['access_manager']: - del data['access_manager']['access_token'] - if 'refresh_token' in data['access_manager']: - del data['access_manager']['refresh_token'] - if 'token_expires' in data['access_manager']: - del data['access_manager']['token_expires'] - if 'default' in data['access_manager']: - if ((data['access_manager']['default'].get('access_token') - or data['access_manager']['default'].get('refresh_token')) - and not data['access_manager']['users'][0].get('access_token') - and not data['access_manager']['users'][0].get('refresh_token')): - if 'name' not in data['access_manager']['default']: - data['access_manager']['default']['name'] = 'Default' - data['access_manager']['users'][0] = data['access_manager']['default'] - del data['access_manager']['default'] - # end clean up - - current_user = data['access_manager']['current_user'] - if 'watch_later' not in data['access_manager']['users'][current_user]: - data['access_manager']['users'][current_user]['watch_later'] = ' WL' - if 'watch_history' not in data['access_manager']['users'][current_user]: - data['access_manager']['users'][current_user]['watch_history'] = 'HL' - - # ensure all users have uuid - uuids = set() - for user in data['access_manager']['users'].values(): - c_uuid = user.get('id') - while not c_uuid or c_uuid in uuids: - c_uuid = uuid.uuid4().hex - uuids.add(c_uuid) - user['id'] = c_uuid - # end uuid check - - self.save(data) - - def get_data(self): - data = super(LoginTokenStore, self).get_data() - # process users, change str keys to int - users = data['access_manager']['users'] - if '0' in users: - data['access_manager']['users'] = { - int(key): value - for key, value in users.items() - } - return data diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index bffb440ce..9a5afdfac 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -30,7 +30,6 @@ from .favorite_list import FavoriteList from .watch_later_list import WatchLaterList from .function_cache import FunctionCache -from .access_manager import AccessManager from .monitor import YouTubeMonitor from .player import YouTubePlayer from .playback_history import PlaybackHistory @@ -55,7 +54,6 @@ 'to_str', 'to_unicode', 'to_utf8', - 'AccessManager', 'DataCache', 'FavoriteList', 'FunctionCache', diff --git a/resources/lib/youtube_plugin/kodion/utils/access_manager.py b/resources/lib/youtube_plugin/kodion/utils/access_manager.py deleted file mode 100644 index c2f2d4d34..000000000 --- a/resources/lib/youtube_plugin/kodion/utils/access_manager.py +++ /dev/null @@ -1,418 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -import uuid -import time - -from hashlib import md5 - -from ..json_store import LoginTokenStore - -__author__ = 'bromix' - - -class AccessManager(object): - def __init__(self, context): - self._settings = context.get_settings() - self._jstore = LoginTokenStore() - self._json = self._jstore.get_data() - self._user = self._json['access_manager'].get('current_user', 0) - self._last_origin = self._json['access_manager'].get('last_origin', 'plugin.video.youtube') - - def get_current_user_id(self): - """ - - :return: uuid of the current user - """ - self._json = self._jstore.get_data() - return self._json['access_manager']['users'][self.get_user()]['id'] - - def get_new_user(self, username=''): - """ - :param username: string, users name - :return: a new user dict - """ - uuids = [ - user.get('id') - for user in self._json['access_manager']['users'].values() - ] - new_uuid = None - while not new_uuid or new_uuid in uuids: - new_uuid = uuid.uuid4().hex - return { - 'access_token': '', - 'refresh_token': '', - 'token_expires': -1, - 'last_key_hash': '', - 'name': username, - 'id': new_uuid, - 'watch_later': ' WL', - 'watch_history': 'HL' - } - - def get_users(self): - """ - Returns users - :return: users - """ - return self._json['access_manager'].get('users', {}) - - def add_user(self, username='', user=None): - """ - Add single new user to users collection - :param username: str, chosen name of new user - :param user: int, optional index for new user - :return: tuple, (index, details) of newly added user - """ - users = self._json['access_manager'].get('users', {}) - new_user_details = self.get_new_user(username) - new_user = max(users) + 1 if users and user is None else user or 0 - users[new_user] = new_user_details - self._json['access_manager']['users'] = users - self._jstore.save(self._json) - return new_user, new_user_details - - def remove_user(self, user): - """ - Remove user from collection of current users - :param user: int, user index - :return: - """ - users = self._json['access_manager'].get('users', {}) - if user in users: - del users[user] - self._json['access_manager']['users'] = users - self._jstore.save(self._json) - - def set_users(self, users): - """ - Updates all users - :param users: dict, users - :return: - """ - self._json = self._jstore.get_data() - self._json['access_manager']['users'] = users - self._jstore.save(self._json) - - def set_user(self, user, switch_to=False): - """ - Updates the user - :param user: string, username - :param switch_to: boolean, change current user - :return: - """ - self._user = user - if switch_to: - self._json = self._jstore.get_data() - self._json['access_manager']['current_user'] = user - self._jstore.save(self._json) - - def get_user(self): - """ - Returns the current user - :return: user - """ - return self._user - - def get_username(self, user=None): - """ - Returns the username of the current or nominated user - :return: username - """ - if user is None: - user = self._user - users = self._json['access_manager'].get('users', {}) - if user in users: - return users[user].get('name') - return '' - - def set_username(self, user, username): - """ - Sets the username of the nominated user - :return: True if username was set, false otherwise - """ - users = self._json['access_manager'].get('users', {}) - if user in users: - users[user]['name'] = username - self._json['access_manager']['users'] = users - self._jstore.save(self._json) - return True - return False - - def get_watch_later_id(self): - """ - Returns the current users watch later playlist id - :return: the current users watch later playlist id - """ - - self._json = self._jstore.get_data() - current_playlist_id = self._json['access_manager']['users'].get(self._user, {}).get('watch_later', '') - settings_playlist_id = self._settings.get_string('youtube.folder.watch_later.playlist', '').strip() - - if settings_playlist_id.lower().startswith(('wl', ' wl')): - self._settings.set_string('youtube.folder.watch_later.playlist', '') - settings_playlist_id = '' - - if current_playlist_id.lower().startswith(('wl', ' wl')): - self._json['access_manager']['users'][self._user]['watch_later'] = settings_playlist_id - self._jstore.save(self._json) - - self._settings.set_string('youtube.folder.watch_later.playlist', '') - settings_playlist_id = '' - - if settings_playlist_id and current_playlist_id != settings_playlist_id: - self._json['access_manager']['users'][self._user]['watch_later'] = settings_playlist_id - self._jstore.save(self._json) - - self._settings.set_string('youtube.folder.watch_later.playlist', '') - - return self._json['access_manager']['users'].get(self._user, {}).get('watch_later', '') - - def set_watch_later_id(self, playlist_id): - """ - Sets the current users watch later playlist id - :param playlist_id: string, watch later playlist id - :return: - """ - if playlist_id.lower() == 'wl' or playlist_id.lower() == ' wl': - playlist_id = '' - - self._json = self._jstore.get_data() - self._json['access_manager']['users'][self._user]['watch_later'] = playlist_id - self._settings.set_string('youtube.folder.watch_later.playlist', '') - self._jstore.save(self._json) - - def get_watch_history_id(self): - """ - Returns the current users watch history playlist id - :return: the current users watch history playlist id - """ - - self._json = self._jstore.get_data() - current_playlist_id = self._json['access_manager']['users'].get(self._user, {}).get('watch_history', 'HL') - settings_playlist_id = self._settings.get_string('youtube.folder.history.playlist', '').strip() - if settings_playlist_id and (current_playlist_id != settings_playlist_id): - self._json['access_manager']['users'][self._user]['watch_history'] = settings_playlist_id - self._jstore.save(self._json) - self._settings.set_string('youtube.folder.history.playlist', '') - return self._json['access_manager']['users'].get(self._user, {}).get('watch_history', 'HL') - - def set_watch_history_id(self, playlist_id): - """ - Sets the current users watch history playlist id - :param playlist_id: string, watch history playlist id - :return: - """ - - self._json = self._jstore.get_data() - self._json['access_manager']['users'][self._user]['watch_history'] = playlist_id - self._settings.set_string('youtube.folder.history.playlist', '') - self._jstore.save(self._json) - - def set_last_origin(self, origin): - """ - Updates the origin - :param origin: string, origin - :return: - """ - self._last_origin = origin - self._json = self._jstore.get_data() - self._json['access_manager']['last_origin'] = origin - self._jstore.save(self._json) - - def get_last_origin(self): - """ - Returns the last origin - :return: - """ - return self._last_origin - - def get_access_token(self): - """ - Returns the access token for some API - :return: access_token - """ - self._json = self._jstore.get_data() - return self._json['access_manager']['users'].get(self._user, {}).get('access_token', '') - - def get_refresh_token(self): - """ - Returns the refresh token - :return: refresh token - """ - self._json = self._jstore.get_data() - return self._json['access_manager']['users'].get(self._user, {}).get('refresh_token', '') - - def has_refresh_token(self): - return self.get_refresh_token() != '' - - def is_access_token_expired(self): - """ - Returns True if the access_token is expired otherwise False. - If no expiration date was provided and an access_token exists - this method will always return True - :return: - """ - self._json = self._jstore.get_data() - access_token = self._json['access_manager']['users'].get(self._user, {}).get('access_token', '') - expires = int(self._json['access_manager']['users'].get(self._user, {}).get('token_expires', -1)) - - # with no access_token it must be expired - if not access_token: - return True - - # in this case no expiration date was set - if expires == -1: - return False - - now = int(time.time()) - return expires <= now - - def update_access_token(self, access_token, unix_timestamp=None, refresh_token=None): - """ - Updates the old access token with the new one. - :param access_token: - :param unix_timestamp: - :param refresh_token: - :return: - """ - self._json = self._jstore.get_data() - self._json['access_manager']['users'][self._user]['access_token'] = access_token - - if unix_timestamp is not None: - self._json['access_manager']['users'][self._user]['token_expires'] = int(unix_timestamp) - - if refresh_token is not None: - self._json['access_manager']['users'][self._user]['refresh_token'] = refresh_token - - self._jstore.save(self._json) - - @staticmethod - def get_new_developer(): - """ - :return: a new developer dict - """ - - return {'access_token': '', 'refresh_token': '', 'token_expires': -1, 'last_key_hash': ''} - - def get_developers(self): - """ - Returns developers - :return: dict, developers - """ - return self._json['access_manager'].get('developers', {}) - - def set_developers(self, developers): - """ - Updates the users - :param developers: dict, developers - :return: - """ - self._json = self._jstore.get_data() - self._json['access_manager']['developers'] = developers - self._jstore.save(self._json) - - def get_dev_access_token(self, addon_id): - """ - Returns the access token for some API - :param addon_id: addon id - :return: access_token - """ - self._json = self._jstore.get_data() - return self._json['access_manager']['developers'].get(addon_id, {}).get('access_token', '') - - def get_dev_refresh_token(self, addon_id): - """ - Returns the refresh token - :return: refresh token - """ - self._json = self._jstore.get_data() - return self._json['access_manager']['developers'].get(addon_id, {}).get('refresh_token', '') - - def developer_has_refresh_token(self, addon_id): - return self.get_dev_refresh_token(addon_id) != '' - - def is_dev_access_token_expired(self, addon_id): - """ - Returns True if the access_token is expired otherwise False. - If no expiration date was provided and an access_token exists - this method will always return True - :return: - """ - self._json = self._jstore.get_data() - access_token = self._json['access_manager']['developers'].get(addon_id, {}).get('access_token', '') - expires = int(self._json['access_manager']['developers'].get(addon_id, {}).get('token_expires', -1)) - - # with no access_token it must be expired - if not access_token: - return True - - # in this case no expiration date was set - if expires == -1: - return False - - now = int(time.time()) - return expires <= now - - def update_dev_access_token(self, addon_id, access_token, unix_timestamp=None, refresh_token=None): - """ - Updates the old access token with the new one. - :param addon_id: - :param access_token: - :param unix_timestamp: - :param refresh_token: - :return: - """ - self._json = self._jstore.get_data() - self._json['access_manager']['developers'][addon_id]['access_token'] = access_token - - if unix_timestamp is not None: - self._json['access_manager']['developers'][addon_id]['token_expires'] = int(unix_timestamp) - - if refresh_token is not None: - self._json['access_manager']['developers'][addon_id]['refresh_token'] = refresh_token - - self._jstore.save(self._json) - - def get_dev_last_key_hash(self, addon_id): - self._json = self._jstore.get_data() - return self._json['access_manager']['developers'][addon_id]['last_key_hash'] - - def set_dev_last_key_hash(self, addon_id, key_hash): - self._json = self._jstore.get_data() - self._json['access_manager']['developers'][addon_id]['last_key_hash'] = key_hash - self._jstore.save(self._json) - - def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): - self._json = self._jstore.get_data() - last_hash = self._json['access_manager']['developers'][addon_id]['last_key_hash'] - current_hash = self.__calc_key_hash(api_key, client_id, client_secret) - if not last_hash and current_hash: - self.set_dev_last_key_hash(addon_id, current_hash) - return False - if last_hash != current_hash: - self.set_dev_last_key_hash(addon_id, current_hash) - return True - return False - - @staticmethod - def __calc_key_hash(api_key, client_id, client_secret): - - m = md5() - try: - m.update(api_key.encode('utf-8')) - m.update(client_id.encode('utf-8')) - m.update(client_secret.encode('utf-8')) - except: - m.update(api_key) - m.update(client_id) - m.update(client_secret) - - return m.hexdigest() diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index c52bc2757..656ea8f5b 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -9,7 +9,7 @@ from base64 import b64decode from hashlib import md5 -from ...kodion.json_store import APIKeyStore, LoginTokenStore +from ...kodion.json_store import AccessManager, APIKeyStore from ...kodion import Context as __Context from ... import key_sets @@ -27,8 +27,8 @@ def __init__(self, context, settings): self._ui = context.get_ui() self._api_jstore = APIKeyStore() self._json_api = self._api_jstore.get_data() - self._am_jstore = LoginTokenStore() - self._json_am = self._am_jstore.get_data() + self._access_manager = AccessManager(context) + self._json_am = self._access_manager.get_data() self.changed = False self._on_init() @@ -92,7 +92,7 @@ def _on_init(self): if (last_hash == self._get_key_set_hash('own', True) or last_hash == own_key_hash): self._json_am['access_manager']['users'][user]['last_key_hash'] = own_key_hash - self._am_jstore.save(self._json_am) + self._access_manager.save(self._json_am) if access_token or refresh_token or last_hash: self._settings.set_string('kodion.access_token', '') self._settings.set_string('kodion.refresh_token', '') @@ -103,7 +103,7 @@ def _on_init(self): if updated_hash: self._context.log_warning('User: |%s| Switching API key set to |%s|' % (user, switch)) self._json_am['access_manager']['users'][user]['last_key_hash'] = updated_hash - self._am_jstore.save(self._json_am) + self._access_manager.save(self._json_am) self._context.log_debug('API key set changed: Signing out') self._context.execute('RunPlugin(plugin://plugin.video.youtube/sign/out/?confirmed=true)') else: @@ -113,7 +113,7 @@ def get_current_switch(self): return 'own' def get_current_user(self): - self._json_am = self._am_jstore.get_data() + self._json_am = self._access_manager.get_data() return self._json_am['access_manager'].get('current_user', 0) def has_own_api_keys(self): @@ -142,7 +142,7 @@ def get_api_keys(self, switch): return api_key, client_id, client_secret def _api_keys_changed(self, switch): - self._json_am = self._am_jstore.get_data() + self._json_am = self._access_manager.get_data() user = self.get_current_user() last_set_hash = self._json_am['access_manager']['users'].get(user, {}).get('last_key_hash', '') current_set_hash = self._get_key_set_hash(switch) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 4cfe07f5f..777fd3812 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -691,7 +691,7 @@ def _on_users(self, context, re_match): def select_user(reason, new_user=False): current_users = access_manager.get_users() - current_user = access_manager.get_user() + current_user = access_manager.get_current_user() usernames = [] for user, details in sorted(current_users.items()): username = details.get('name') or localize('user.unnamed') @@ -733,7 +733,7 @@ def switch_to_user(user): else: user = user_index_map[result] - if user is not None and user != access_manager.get_user(): + if user is not None and user != access_manager.get_current_user(): switch_to_user(user) elif action == 'add': @@ -758,7 +758,7 @@ def switch_to_user(user): if user == 0: access_manager.add_user(username=localize('user.default'), user=0) - if user == access_manager.get_user(): + if user == access_manager.get_current_user(): access_manager.set_user(0, switch_to=True) ui.show_notification(localize('removed') % username, localize('remove')) From 40869c6764d51f9aa8fa3edc2665022715fe8d67 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 02:15:42 +1100 Subject: [PATCH 075/141] Move sql storage based classes to new kodion.sql_store module - Originally located in kodion.utils - Prevents possible issues with circular imports --- .../kodion/context/abstract_context.py | 9 ++--- .../kodion/sql_store/__init__.py | 25 ++++++++++++++ .../kodion/{utils => sql_store}/data_cache.py | 3 +- .../{utils => sql_store}/favorite_list.py | 6 ++-- .../{utils => sql_store}/function_cache.py | 18 +++++----- .../{utils => sql_store}/playback_history.py | 0 .../{utils => sql_store}/search_history.py | 10 +++--- .../kodion/{utils => sql_store}/storage.py | 34 +++++++++---------- .../{utils => sql_store}/watch_later_list.py | 4 +-- .../youtube_plugin/kodion/utils/__init__.py | 12 ------- 10 files changed, 66 insertions(+), 55 deletions(-) create mode 100644 resources/lib/youtube_plugin/kodion/sql_store/__init__.py rename resources/lib/youtube_plugin/kodion/{utils => sql_store}/data_cache.py (93%) rename resources/lib/youtube_plugin/kodion/{utils => sql_store}/favorite_list.py (84%) rename resources/lib/youtube_plugin/kodion/{utils => sql_store}/function_cache.py (84%) rename resources/lib/youtube_plugin/kodion/{utils => sql_store}/playback_history.py (100%) rename resources/lib/youtube_plugin/kodion/{utils => sql_store}/search_history.py (87%) rename resources/lib/youtube_plugin/kodion/{utils => sql_store}/storage.py (91%) rename resources/lib/youtube_plugin/kodion/{utils => sql_store}/watch_later_list.py (91%) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 99af7c3fb..a671c34b9 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -11,20 +11,17 @@ import os from urllib.parse import urlencode -from .. import constants -from .. import logger +from .. import constants, logger from ..json_store import AccessManager -from ..utils import ( - create_path, - create_uri_path, +from ..sql_store import ( DataCache, FavoriteList, FunctionCache, PlaybackHistory, SearchHistory, - SystemVersion, WatchLaterList, ) +from ..utils import (SystemVersion, create_path, create_uri_path) class AbstractContext(object): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/__init__.py b/resources/lib/youtube_plugin/kodion/sql_store/__init__.py new file mode 100644 index 000000000..46da38d54 --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/sql_store/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from .data_cache import DataCache +from .favorite_list import FavoriteList +from .function_cache import FunctionCache +from .playback_history import PlaybackHistory +from .search_history import SearchHistory +from .watch_later_list import WatchLaterList + + +__all__ = ( + 'DataCache', + 'FavoriteList', + 'FunctionCache', + 'PlaybackHistory', + 'SearchHistory', + 'WatchLaterList', +) diff --git a/resources/lib/youtube_plugin/kodion/utils/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py similarity index 93% rename from resources/lib/youtube_plugin/kodion/utils/data_cache.py rename to resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index f3a29ed92..fb8ca4134 100644 --- a/resources/lib/youtube_plugin/kodion/utils/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -17,7 +17,8 @@ class DataCache(Storage): def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 - super(DataCache, self).__init__(filename, max_file_size_kb=max_file_size_kb) + super(DataCache, self).__init__(filename, + max_file_size_kb=max_file_size_kb) def is_empty(self): return self._is_empty() diff --git a/resources/lib/youtube_plugin/kodion/utils/favorite_list.py b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py similarity index 84% rename from resources/lib/youtube_plugin/kodion/utils/favorite_list.py rename to resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py index d18a013f6..11504727b 100644 --- a/resources/lib/youtube_plugin/kodion/utils/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py @@ -9,7 +9,7 @@ """ from .storage import Storage -from .. import items +from ..items import from_json, to_json class FavoriteList(Storage): @@ -24,11 +24,11 @@ def _sort_item(_item): return _item[2].get_name().upper() def get_items(self): - result = self._get_by_ids(process=items.from_json) + result = self._get_by_ids(process=from_json) return sorted(result, key=self._sort_item, reverse=False) def add(self, base_item): - item_json_data = items.to_json(base_item) + item_json_data = to_json(base_item) self._set(base_item.get_id(), item_json_data) def remove(self, base_item): diff --git a/resources/lib/youtube_plugin/kodion/utils/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py similarity index 84% rename from resources/lib/youtube_plugin/kodion/utils/function_cache.py rename to resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index 3929878dc..7b71ad10a 100644 --- a/resources/lib/youtube_plugin/kodion/utils/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -9,7 +9,7 @@ """ from functools import partial -import hashlib +from hashlib import md5 from .storage import Storage @@ -45,12 +45,12 @@ def _create_id_from_func(partial_func): :param partial_func: :return: id for the given function """ - m = hashlib.md5() - m.update(partial_func.func.__module__.encode('utf-8')) - m.update(partial_func.func.__name__.encode('utf-8')) - m.update(str(partial_func.args).encode('utf-8')) - m.update(str(partial_func.keywords).encode('utf-8')) - return m.hexdigest() + md5_hash = md5() + md5_hash.update(partial_func.func.__module__.encode('utf-8')) + md5_hash.update(partial_func.func.__name__.encode('utf-8')) + md5_hash.update(str(partial_func.args).encode('utf-8')) + md5_hash.update(str(partial_func.keywords).encode('utf-8')) + return md5_hash.hexdigest() def _get_cached_data(self, partial_func): cache_id = self._create_id_from_func(partial_func) @@ -103,6 +103,6 @@ def get(self, func, seconds, *args, **keywords): return cached_data def _optimize_item_count(self): - # override method from resources/lib/youtube_plugin/kodion/utils/storage.py - # for function cache do not optimize by item count, using database size. + # override method Storage._optimize_item_count + # for function cache do not optimize by item count, use database size. pass diff --git a/resources/lib/youtube_plugin/kodion/utils/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py similarity index 100% rename from resources/lib/youtube_plugin/kodion/utils/playback_history.py rename to resources/lib/youtube_plugin/kodion/sql_store/playback_history.py diff --git a/resources/lib/youtube_plugin/kodion/utils/search_history.py b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py similarity index 87% rename from resources/lib/youtube_plugin/kodion/utils/search_history.py rename to resources/lib/youtube_plugin/kodion/sql_store/search_history.py index 9eae48318..a06d8c427 100644 --- a/resources/lib/youtube_plugin/kodion/utils/search_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -8,10 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -import hashlib +from hashlib import md5 from .storage import Storage -from .methods import to_utf8 +from ..utils import to_utf8 class SearchHistory(Storage): @@ -32,9 +32,9 @@ def clear(self): @staticmethod def _make_id(search_text): - m = hashlib.md5() - m.update(to_utf8(search_text)) - return m.hexdigest() + md5_hash = md5() + md5_hash.update(to_utf8(search_text)) + return md5_hash.hexdigest() def rename(self, old_search_text, new_search_text): self.remove(old_search_text) diff --git a/resources/lib/youtube_plugin/kodion/utils/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py similarity index 91% rename from resources/lib/youtube_plugin/kodion/utils/storage.py rename to resources/lib/youtube_plugin/kodion/sql_store/storage.py index 0306ee053..28b7bc705 100644 --- a/resources/lib/youtube_plugin/kodion/utils/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -8,15 +8,15 @@ See LICENSES/GPL-2.0-only for more information. """ -import datetime import json import os import pickle import sqlite3 import time -import traceback +from datetime import datetime +from traceback import print_exc -from .. import logger +from ..logger import log_error class Storage(object): @@ -155,7 +155,7 @@ def _sync(self): def _set(self, item_id, item): # add 1 microsecond, required for dbapi2 - now = datetime.datetime.now().timestamp() + 0.000001 + now = datetime.now().timestamp() + 0.000001 self._open() self._execute(True, self._set_query, values=[item_id, now, @@ -165,7 +165,7 @@ def _set(self, item_id, item): def _set_all(self, items): # add 1 microsecond, required for dbapi2 - now = datetime.datetime.now().timestamp() + 0.000001 + now = datetime.now().timestamp() + 0.000001 self._open() self._execute(True, self._set_query, values=[(key, now, self._encode(json.dumps(item))) @@ -278,27 +278,27 @@ def _convert_timestamp(cls, val): val = val.decode('utf-8') if '-' in val or ':' in val: return cls._parse_datetime_string(val) - return datetime.datetime.fromtimestamp(float(val)) + return datetime.fromtimestamp(float(val)) @classmethod def _parse_datetime_string(cls, current_stamp): for stamp_format in ['%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S']: try: - stamp_datetime = datetime.datetime( + stamp_datetime = datetime( *(cls.strptime(current_stamp, stamp_format)[0:6]) ) break except ValueError: # current_stamp has no microseconds continue except TypeError: - logger.log_error('Exception while parsing timestamp:\n' - 'current_stamp |{cs}|{cst}|\n' - 'stamp_format |{sf}|{sft}|\n{tb}' - .format(cs=current_stamp, - cst=type(current_stamp), - sf=stamp_format, - sft=type(stamp_format), - tb=traceback.print_exc())) + log_error('Exception while parsing timestamp:\n' + 'current_stamp |{cs}|{cst}|\n' + 'stamp_format |{sf}|{sft}|\n{tb}' + .format(cs=current_stamp, + cst=type(current_stamp), + sf=stamp_format, + sft=type(stamp_format), + tb=print_exc())) else: return None return stamp_datetime @@ -307,8 +307,8 @@ def get_seconds_diff(self, current_stamp): if not current_stamp: return 86400 # 24 hrs - current_datetime = datetime.datetime.now() - if isinstance(current_stamp, datetime.datetime): + current_datetime = datetime.now() + if isinstance(current_stamp, datetime): time_delta = current_datetime - current_stamp return time_delta.total_seconds() diff --git a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py similarity index 91% rename from resources/lib/youtube_plugin/kodion/utils/watch_later_list.py rename to resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index 20425d513..5da49a5d3 100644 --- a/resources/lib/youtube_plugin/kodion/utils/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -8,7 +8,7 @@ See LICENSES/GPL-2.0-only for more information. """ -import datetime +from datetime import datetime from .storage import Storage from .. import items @@ -30,7 +30,7 @@ def get_items(self): return sorted(result, key=self._sort_item, reverse=False) def add(self, base_item): - base_item.set_date_from_datetime(datetime.datetime.now()) + base_item.set_date_from_datetime(datetime.now()) item_json_data = items.to_json(base_item) self._set(base_item.get_id(), item_json_data) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 9a5afdfac..af9d750b9 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -26,14 +26,8 @@ to_unicode, to_utf8, ) -from .search_history import SearchHistory -from .favorite_list import FavoriteList -from .watch_later_list import WatchLaterList -from .function_cache import FunctionCache from .monitor import YouTubeMonitor from .player import YouTubePlayer -from .playback_history import PlaybackHistory -from .data_cache import DataCache from .system_version import SystemVersion @@ -54,13 +48,7 @@ 'to_str', 'to_unicode', 'to_utf8', - 'DataCache', - 'FavoriteList', - 'FunctionCache', - 'PlaybackHistory', - 'SearchHistory', 'SystemVersion', - 'WatchLaterList', 'YouTubeMonitor', 'YouTubePlayer', ) From e136f43d65bdbd977a7d67fb8b75efdc88ca3a3f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 19:57:47 +1100 Subject: [PATCH 076/141] Use single instance of SystemVersion --- .../kodion/context/abstract_context.py | 4 +-- .../youtube_plugin/kodion/utils/__init__.py | 4 +-- .../kodion/utils/system_version.py | 27 ++++++++++++++----- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index a671c34b9..a46327767 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -21,7 +21,7 @@ SearchHistory, WatchLaterList, ) -from ..utils import (SystemVersion, create_path, create_uri_path) +from ..utils import (create_path, create_uri_path, current_system_version) class AbstractContext(object): @@ -204,7 +204,7 @@ def get_ui(self): def get_system_version(self): if not self._system_version: - self._system_version = SystemVersion(version='', releasename='', appname='') + self._system_version = current_system_version return self._system_version diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index af9d750b9..9130dae36 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -28,12 +28,13 @@ ) from .monitor import YouTubeMonitor from .player import YouTubePlayer -from .system_version import SystemVersion +from .system_version import current_system_version __all__ = ( 'create_path', 'create_uri_path', + 'current_system_version', 'datetime_parser', 'duration_to_seconds', 'find_best_fit', @@ -48,7 +49,6 @@ 'to_str', 'to_unicode', 'to_utf8', - 'SystemVersion', 'YouTubeMonitor', 'YouTubePlayer', ) diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py index de01118ce..d51db45fa 100644 --- a/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -14,7 +14,7 @@ class SystemVersion(object): - def __init__(self, version, releasename, appname): + def __init__(self, version=None, releasename=None, appname=None): self._version = ( version if version and isinstance(version, tuple) else (0, 0, 0, 0) @@ -31,19 +31,25 @@ def __init__(self, version, releasename, appname): ) try: - json_query = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Application.GetProperties", ' - '"params": {"properties": ["version", "name"]}, "id": 1 }') + json_query = xbmc.executeJSONRPC(json.dumps({ + 'jsonrpc': '2.0', + 'method': 'Application.GetProperties', + 'params': { + 'properties': ['version', 'name'] + }, + 'id': 1, + })) json_query = str(json_query) json_query = json.loads(json_query) version_installed = json_query['result']['version'] - self._version = (version_installed.get('major', 1), version_installed.get('minor', 0)) + self._version = (version_installed.get('major', 1), + version_installed.get('minor', 0)) self._appname = json_query['result']['name'] except: self._version = (1, 0) # Frodo self._appname = 'Unknown Application' - self._releasename = 'Unknown Release' if self._version >= (21, 0): self._releasename = 'Omega' elif self._version >= (20, 0): @@ -64,9 +70,15 @@ def __init__(self, version, releasename, appname): self._releasename = 'Gotham' elif self._version >= (12, 0): self._releasename = 'Frodo' + else: + self._releasename = 'Unknown Release' def __str__(self): - obj_str = "%s (%s-%s)" % (self._releasename, self._appname, '.'.join(map(str, self._version))) + obj_str = '{releasename} ({appname}-{version[0]}.{version[1]})'.format( + releasename=self._releasename, + appname=self._appname, + version=self._version + ) return obj_str def get_release_name(self): @@ -77,3 +89,6 @@ def get_version(self): def get_app_name(self): return self._appname + + +current_system_version = SystemVersion() From 7a0ce7b7a4fd7cef41b0ba6893826d54806bbfaf Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 20:25:41 +1100 Subject: [PATCH 077/141] Add get_kodi_setting method --- .../lib/youtube_plugin/kodion/utils/__init__.py | 2 ++ .../lib/youtube_plugin/kodion/utils/methods.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 9130dae36..a74ac558a 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -16,6 +16,7 @@ find_best_fit, find_video_id, friendly_number, + get_kodi_setting, loose_version, make_dirs, merge_dicts, @@ -40,6 +41,7 @@ 'find_best_fit', 'find_video_id', 'friendly_number', + 'get_kodi_setting', 'loose_version', 'make_dirs', 'merge_dicts', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index a648a81eb..745603376 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -8,14 +8,16 @@ See LICENSES/GPL-2.0-only for more information. """ -import os import copy +import json +import os import re from datetime import timedelta from math import floor, log from urllib.parse import quote import xbmcvfs +from xbmc import executeJSONRPC __all__ = ( @@ -25,6 +27,7 @@ 'find_best_fit', 'find_video_id', 'friendly_number', + 'get_kodi_setting', 'loose_version', 'make_dirs', 'merge_dicts', @@ -315,3 +318,13 @@ def merge_dicts(item1, item2, templates=None, _=Ellipsis): templates['{0}.{1}'.format(id(new), key)] = (new, key, value) new[key] = value return new or _ + +def get_kodi_setting(setting): + json_query = executeJSONRPC(json.dumps({ + 'jsonrpc': '2.0', + 'method': 'Settings.GetSettingValue', + 'params': {'setting': setting}, + 'id': 1, + })) + json_query = json.loads(json_query) + return json_query.get('result', {}).get('value') From bbc8995d50cf313d50ad8fef7abb0dc4fb2eeba5 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 21:29:53 +1100 Subject: [PATCH 078/141] Update settings to use xbmcaddon.Settings class - Used in Kodi v20+ - Backwards compatibility also maintained - Values are cached, flushed when onSettingsChanged callback runs - Values are logged when debug logging is enabled - TODO: Use single instance of XbmcPluginSettings --- .../kodion/settings/abstract_settings.py | 63 +++-- .../settings/xbmc/xbmc_plugin_settings.py | 215 +++++++++++++++++- .../youtube_plugin/kodion/utils/monitor.py | 7 +- 3 files changed, 239 insertions(+), 46 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 6e1c65e97..e3bb7fd23 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -11,56 +11,49 @@ import sys from ..constants import setting as SETTINGS -from ..logger import log_debug class AbstractSettings(object): - def __init__(self): - super(AbstractSettings, self).__init__() + VALUE_FROM_STR = { + 'false': False, + 'true': True, + } - def get_string(self, setting_id, default_value=None): - raise NotImplementedError() + _echo = False + _cache = {} + _funcs = {} + _store = None - def set_string(self, setting_id, value): + @classmethod + def flush(cls, xbmc_addon): raise NotImplementedError() - def open_settings(self): + def get_bool(self, setting, default=None, echo=None): raise NotImplementedError() - def get_int(self, setting_id, default_value, converter=None): - if not converter: - def converter(x): - return x - - value = self.get_string(setting_id) - if value is None or value == '': - return default_value + def set_bool(self, setting, value, echo=None): + raise NotImplementedError() - try: - return converter(int(value)) - except Exception as ex: - log_debug("Failed to get setting '%s' as 'int' (%s)" % setting_id, ex.__str__()) + def get_int(self, setting, default=-1, converter=None, echo=None): + raise NotImplementedError() - return default_value + def set_int(self, setting, value, echo=None): + raise NotImplementedError() - def set_int(self, setting_id, value): - self.set_string(setting_id, str(value)) + def get_string(self, setting, default='', echo=None): + raise NotImplementedError() - def set_bool(self, setting_id, value): - if value: - self.set_string(setting_id, 'true') - else: - self.set_string(setting_id, 'false') + def set_string(self, setting, value, echo=None): + raise NotImplementedError() - def get_bool(self, setting_id, default_value): - value = self.get_string(setting_id) - if value is None or value == '': - return default_value + def get_string_list(self, setting, default=None, echo=None): + raise NotImplementedError() - if value not in {'false', 'true'}: - return default_value + def set_string_list(self, setting, value, echo=None): + raise NotImplementedError() - return value == 'true' + def open_settings(self): + raise NotImplementedError() def get_items_per_page(self): return self.get_int(SETTINGS.ITEMS_PER_PAGE, 50) @@ -293,7 +286,7 @@ def get_mpd_video_qualities(self): if selected >= key] def stream_features(self): - return self.get_string(SETTINGS.MPD_STREAM_FEATURES, '').split(',') + return self.get_string_list(SETTINGS.MPD_STREAM_FEATURES) _STREAM_SELECT = { 1: 'auto', diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index 6e5632690..be5b888e7 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -8,20 +8,221 @@ See LICENSES/GPL-2.0-only for more information. """ +import xbmcaddon + from ..abstract_settings import AbstractSettings +from ...logger import log_debug +from ...utils.methods import get_kodi_setting +from ...utils.system_version import current_system_version class XbmcPluginSettings(AbstractSettings): def __init__(self, xbmc_addon): super(XbmcPluginSettings, self).__init__() - self._xbmc_addon = xbmc_addon + self.flush(xbmc_addon) + + if self._funcs: + return + if current_system_version.get_version() >= (20, 0): + _class = xbmcaddon.Settings + + self._funcs.update({ + 'get_bool': _class.getBool, + 'set_bool': _class.setBool, + 'get_int': _class.getInt, + 'set_int': _class.setInt, + 'get_str': _class.getString, + 'set_str': _class.setString, + 'get_str_list': _class.getStringList, + 'set_str_list': _class.setStringList, + }) + else: + _class = xbmcaddon.Addon + + def _get_string_list(store, setting): + return _class.getSettingString(store, setting).split(',') + + def _set_string_list(store, setting, value): + value = ','.join(value) + return _class.setSettingString(store, setting, value) + + self._funcs.update({ + 'get_bool': _class.getSettingBool, + 'set_bool': _class.setSettingBool, + 'get_int': _class.getSettingInt, + 'set_int': _class.setSettingInt, + 'get_str': _class.getSettingString, + 'set_str': _class.setSettingString, + 'get_str_list': _get_string_list, + 'set_str_list': _set_string_list, + }) + + @classmethod + def flush(cls, xbmc_addon): + cls._echo = get_kodi_setting('debug.showloginfo') + cls._cache = {} + if current_system_version.get_version() >= (20, 0): + cls._store = xbmc_addon.getSettings() + else: + cls._store = xbmc_addon + + def get_bool(self, setting, default=None, echo=None): + if setting in self._cache: + return self._cache[setting] + + error = False + try: + value = bool(self._funcs['get_bool'](self._store, setting)) + except (AttributeError, TypeError) as ex: + error = ex + value = self.get_string(setting, echo=False) + value = AbstractSettings.VALUE_FROM_STR.get(value.lower(), default) + except RuntimeError as ex: + error = ex + value = default + + if self._echo and echo is not False: + log_debug('Get |{setting}|: {value} (bool, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + self._cache[setting] = value + return value + + def set_bool(self, setting, value, echo=None): + try: + error = not self._funcs['set_bool'](self._store, setting, value) + if not error: + self._cache[setting] = value + except RuntimeError as ex: + error = ex + + if self._echo and echo is not False: + log_debug('Set |{setting}|: {value} (bool, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + return not error + + def get_int(self, setting, default=-1, process=None, echo=None): + if setting in self._cache: + return self._cache[setting] + + error = False + try: + value = int(self._funcs['get_int'](self._store, setting)) + if process: + value = process(value) + except (AttributeError, TypeError, ValueError) as ex: + error = ex + value = self.get_string(setting, echo=False) + try: + value = int(value) + except (TypeError, ValueError) as ex: + error = ex + value = default + except RuntimeError as ex: + error = ex + value = default + + if self._echo and echo is not False: + log_debug('Get |{setting}|: {value} (int, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + self._cache[setting] = value + return value + + def set_int(self, setting, value, echo=None): + try: + error = not self._funcs['set_int'](self._store, setting, value) + if not error: + self._cache[setting] = value + except RuntimeError as ex: + error = ex + + if self._echo and echo is not False: + log_debug('Set |{setting}|: {value} (int, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + return not error + + def get_string(self, setting, default='', echo=None): + if setting in self._cache: + return self._cache[setting] + + error = False + try: + value = self._funcs['get_str'](self._store, setting) or default + except RuntimeError as ex: + error = ex + value = default + + if self._echo and echo is not False: + log_debug('Get |{setting}|: "{value}" (str, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + self._cache[setting] = value + return value + + def set_string(self, setting, value, echo=None): + try: + error = not self._funcs['set_str'](self._store, setting, value) + if not error: + self._cache[setting] = value + except RuntimeError as ex: + error = ex + + if self._echo and echo is not False: + log_debug('Set |{setting}|: "{value}" (str, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + return not error + + def get_string_list(self, setting, default=None, echo=None): + if setting in self._cache: + return self._cache[setting] + + error = False + try: + value = self._funcs['get_str_list'](self._store, setting) + if not value: + value = [] if default is None else default + except RuntimeError as ex: + error = ex + value = default - def get_string(self, setting_id, default_value=None): - return self._xbmc_addon.getSetting(setting_id) + if self._echo and echo is not False: + log_debug('Get |{setting}|: "{value}" (str list, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + self._cache[setting] = value + return value - def set_string(self, setting_id, value): - self._xbmc_addon.setSetting(setting_id, value) + def set_string_list(self, setting, value, echo=None): + try: + error = not self._funcs['set_str_list'](self._store, setting, value) + if not error: + self._cache[setting] = value + except RuntimeError as ex: + error = ex - def open_settings(self): - self._xbmc_addon.openSetting() + if self._echo and echo is not False: + log_debug('Set |{setting}|: "{value}" (str list, {status})'.format( + setting=setting, + value=value, + status=error if error else 'success' + )) + return not error diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index 1cd981580..e25e22bdc 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -24,8 +24,7 @@ class YouTubeMonitor(xbmc.Monitor): _addon_id = 'plugin.video.youtube' - _addon = Addon(_addon_id) - _settings = Settings(_addon) + _settings = Settings(Addon(_addon_id)) # noinspection PyUnusedLocal,PyMissingConstructor def __init__(self, *args, **kwargs): @@ -89,8 +88,8 @@ def onNotification(self, sender, method, data): .format(method=method)) def onSettingsChanged(self): - YouTubeMonitor._addon = Addon(self._addon_id) - YouTubeMonitor._settings = Settings(self._addon) + self._settings.flush(Addon(self._addon_id)) + data = { 'use_httpd': (self._settings.use_mpd_videos() or self._settings.api_config_page()), From b3b08d2d3148992e926ca9e13384930b20d8f281 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 10 Dec 2023 23:18:25 +1100 Subject: [PATCH 079/141] Make better use of new AccessManager - Update youtube.client.__config__ - Follow up to f5742c8 --- .../kodion/constants/const_settings.py | 5 + .../kodion/json_store/access_manager.py | 7 +- .../kodion/settings/abstract_settings.py | 24 +++ .../youtube/client/__config__.py | 182 ++++++++++-------- 4 files changed, 134 insertions(+), 84 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 0b299f170..70501103b 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -55,6 +55,11 @@ API_KEY = 'youtube.api.key' # (string) API_ID = 'youtube.api.id' # (string) API_SECRET = 'youtube.api.secret' # (string) +API_LAST_HASH = 'youtube.api.last.hash' # (string) + +USER_ACCESS_TOKEN = 'kodion.access_token' # (string) +USER_REFRESH_TOKEN = 'kodion.refresh_token' # (string) +USER_TOKEN_EXPIRATION = 'kodion.access_token.expires' # (int) CLIENT_SELECTION = 'youtube.client.selection' # (int) diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index c5264ca67..e54b0dc62 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -425,12 +425,14 @@ def is_access_token_expired(self): def update_access_token(self, access_token, unix_timestamp=None, - refresh_token=None): + refresh_token=None, + last_key_hash=None): """ Updates the old access token with the new one. :param access_token: :param unix_timestamp: :param refresh_token: + :param last_key_hash: :return: """ current_user = self.get_current_user_details() @@ -442,6 +444,9 @@ def update_access_token(self, if refresh_token is not None: current_user['refresh_token'] = refresh_token + if last_key_hash is not None: + current_user['last_key_hash'] = last_key_hash + data = { 'access_manager': { 'users': { diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index e3bb7fd23..15a7d9b28 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -228,6 +228,30 @@ def api_secret(self, new_secret=None): return new_secret return self.get_string(SETTINGS.API_SECRET) + def api_last_hash(self, new_hash=None): + if new_hash is not None: + self.set_string(SETTINGS.API_LAST_HASH, new_hash) + return new_hash + return self.get_string(SETTINGS.API_LAST_HASH, '') + + def user_access_token(self, new_access_token=None): + if new_access_token is not None: + self.set_string(SETTINGS.USER_ACCESS_TOKEN, new_access_token) + return new_access_token + return self.get_string(SETTINGS.USER_ACCESS_TOKEN, '') + + def user_refresh_token(self, new_refresh_token=None): + if new_refresh_token is not None: + self.set_string(SETTINGS.USER_REFRESH_TOKEN, new_refresh_token) + return new_refresh_token + return self.get_string(SETTINGS.USER_REFRESH_TOKEN, '') + + def user_token_expiration(self, new_token_expiration=None): + if new_token_expiration is not None: + self.set_int(SETTINGS.USER_TOKEN_EXPIRATION, new_token_expiration) + return new_token_expiration + return self.get_int(SETTINGS.USER_TOKEN_EXPIRATION, -1) + def get_location(self): location = self.get_string(SETTINGS.LOCATION, '').replace(' ', '').strip() coords = location.split(',') diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index 656ea8f5b..239475da3 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -9,26 +9,30 @@ from base64 import b64decode from hashlib import md5 -from ...kodion.json_store import AccessManager, APIKeyStore -from ...kodion import Context as __Context + from ... import key_sets +from ...kodion import Context +from ...kodion.json_store import APIKeyStore, AccessManager -DEFAULT_SWITCH = 1 -__context = __Context(plugin_id='plugin.video.youtube') -__settings = __context.get_settings() +DEFAULT_SWITCH = 1 class APICheck(object): - - def __init__(self, context, settings): + def __init__(self, context): + settings = context.get_settings() + context.send_notification('check_settings', { + 'use_httpd': settings.use_mpd_videos() or settings.api_config_page(), + 'httpd_port': settings.httpd_port(), + 'whitelist': settings.httpd_whitelist(), + 'httpd_address': settings.httpd_listen() + }) self._context = context self._settings = settings self._ui = context.get_ui() self._api_jstore = APIKeyStore() self._json_api = self._api_jstore.get_data() self._access_manager = AccessManager(context) - self._json_am = self._access_manager.get_data() self.changed = False self._on_init() @@ -49,16 +53,16 @@ def _on_init(self): self._json_api['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} self._api_jstore.save(self._json_api) - original_key = self._settings.get_string('youtube.api.key') - original_id = self._settings.get_string('youtube.api.id') - original_secret = self._settings.get_string('youtube.api.secret') + original_key = self._settings.api_key() + original_id = self._settings.api_id() + original_secret = self._settings.api_secret() if original_key and original_id and original_secret: own_key, own_id, own_secret = self._strip_api_keys(original_key, original_id, original_secret) if own_key and own_id and own_secret: if (original_key != own_key) or (original_id != own_id) or (original_secret != own_secret): - self._settings.set_string('youtube.api.key', own_key) - self._settings.set_string('youtube.api.id', own_id) - self._settings.set_string('youtube.api.secret', own_secret) + self._settings.api_key(own_key) + self._settings.api_id(own_id) + self._settings.api_secret(own_secret) if (j_key != own_key) or (j_id != own_id) or (j_secret != own_secret): self._json_api['keys']['personal'] = {'api_key': own_key, 'client_id': own_id, 'client_secret': own_secret} @@ -69,52 +73,65 @@ def _on_init(self): j_id = self._json_api['keys']['personal'].get('client_id', '') j_secret = self._json_api['keys']['personal'].get('client_secret', '') - if not original_key or not original_id or not original_secret and (j_key and j_secret and j_id): - self._settings.set_string('youtube.api.key', j_key) - self._settings.set_string('youtube.api.id', j_id) - self._settings.set_string('youtube.api.secret', j_secret) + if (not original_key or not original_id or not original_secret + and j_key and j_secret and j_id): + self._settings.api_key(j_key) + self._settings.api_id(j_id) + self._settings.api_secret(j_secret) switch = self.get_current_switch() - user = self.get_current_user() - - access_token = self._settings.get_string('kodion.access_token', '') - refresh_token = self._settings.get_string('kodion.refresh_token', '') - token_expires = self._settings.get_int('kodion.access_token.expires', -1) - last_hash = self._settings.get_string('youtube.api.last.hash', '') - if ((not self._json_am['access_manager']['users'].get(user, {}).get('access_token') - or not self._json_am['access_manager']['users'].get(user, {}).get('refresh_token')) - and access_token and refresh_token): - self._json_am['access_manager']['users'][user]['access_token'] = access_token - self._json_am['access_manager']['users'][user]['refresh_token'] = refresh_token - self._json_am['access_manager']['users'][user]['token_expires'] = token_expires + user_details = self._access_manager.get_current_user_details() + + access_token = self._settings.user_access_token() + refresh_token = self._settings.user_refresh_token() + token_expires = self._settings.user_token_expiration() + last_hash = self._settings.api_last_hash() + updated_hash = self._api_keys_changed(switch) + + if access_token or refresh_token or last_hash: + self._settings.user_access_token('') + self._settings.user_refresh_token('') + self._settings.user_token_expiration(-1) + self._settings.api_last_hash('') + + if updated_hash or (access_token and refresh_token + and not (user_details.get('access_token') + and user_details.get('refresh_token'))): if switch == 'own': own_key_hash = self._get_key_set_hash('own') if (last_hash == self._get_key_set_hash('own', True) or last_hash == own_key_hash): - self._json_am['access_manager']['users'][user]['last_key_hash'] = own_key_hash - self._access_manager.save(self._json_am) - if access_token or refresh_token or last_hash: - self._settings.set_string('kodion.access_token', '') - self._settings.set_string('kodion.refresh_token', '') - self._settings.set_int('kodion.access_token.expires', -1) - self._settings.set_string('youtube.api.last.hash', '') - - updated_hash = self._api_keys_changed(switch) - if updated_hash: - self._context.log_warning('User: |%s| Switching API key set to |%s|' % (user, switch)) - self._json_am['access_manager']['users'][user]['last_key_hash'] = updated_hash - self._access_manager.save(self._json_am) - self._context.log_debug('API key set changed: Signing out') - self._context.execute('RunPlugin(plugin://plugin.video.youtube/sign/out/?confirmed=true)') - else: - self._context.log_debug('User: |%s| Using API key set: |%s|' % (user, switch)) - - def get_current_switch(self): + last_hash = own_key_hash + else: + last_hash = None + else: + last_hash = None + + if updated_hash: + last_hash = updated_hash + self._context.log_warning('User: |{user}|, ' + 'Switching API key set to: |{switch}|' + .format(user=self.get_current_user(), + switch=switch)) + self._context.log_debug('API key set changed: Signing out') + self._context.execute('RunPlugin(plugin://plugin.video.youtube/' + 'sign/out/?confirmed=true)') + + self._access_manager.update_access_token( + access_token, token_expires, refresh_token, last_hash + ) + elif not updated_hash: + self._context.log_debug('User: |{user}|, ' + 'Using API key set: |{switch}|' + .format(user=self.get_current_user(), + switch=switch)) + + @staticmethod + def get_current_switch(): return 'own' def get_current_user(self): - self._json_am = self._access_manager.get_data() - return self._json_am['access_manager'].get('current_user', 0) + return self._access_manager.get_current_user() def has_own_api_keys(self): self._json_api = self._api_jstore.get_data() @@ -127,24 +144,36 @@ def get_api_keys(self, switch): self._json_api = self._api_jstore.get_data() if switch == 'developer': return self._json_api['keys'][switch] + + decode = True if switch == 'youtube-tv': - api_key = b64decode(key_sets[switch]['key']).decode('utf-8'), - client_id = ''.join([b64decode(key_sets[switch]['id']).decode('utf-8'), '.apps.googleusercontent.com']) - client_secret = b64decode(key_sets[switch]['secret']).decode('utf-8') + api_key = key_sets[switch]['key'] + client_id = key_sets[switch]['id'] + client_secret = key_sets[switch]['secret'] + elif switch == 'own': + decode = False api_key = self._json_api['keys']['personal']['api_key'] - client_id = ''.join([self._json_api['keys']['personal']['client_id'], '.apps.googleusercontent.com']) + client_id = self._json_api['keys']['personal']['client_id'] client_secret = self._json_api['keys']['personal']['client_secret'] + else: - api_key = b64decode(key_sets['provided'][switch]['key']).decode('utf-8') - client_id = ''.join([b64decode(key_sets['provided'][switch]['id']).decode('utf-8'), '.apps.googleusercontent.com']) - client_secret = b64decode(key_sets['provided'][switch]['secret']).decode('utf-8') - return api_key, client_id, client_secret + api_key = key_sets['provided'][switch]['key'] + client_id = key_sets['provided'][switch]['id'] + client_secret = key_sets['provided'][switch]['secret'] + + if decode: + api_key = b64decode(api_key).decode('utf-8') + client_id = b64decode(client_id).decode('utf-8') + client_secret = b64decode(client_secret).decode('utf-8') + + return {'key': api_key, + 'id': ''.join((client_id, '.apps.googleusercontent.com')), + 'secret': client_secret} def _api_keys_changed(self, switch): - self._json_am = self._access_manager.get_data() - user = self.get_current_user() - last_set_hash = self._json_am['access_manager']['users'].get(user, {}).get('last_key_hash', '') + user_details = self._access_manager.get_current_user_details() + last_set_hash = user_details.get('last_key_hash', '') current_set_hash = self._get_key_set_hash(switch) if last_set_hash != current_set_hash: self.changed = True @@ -156,12 +185,12 @@ def _get_key_set_hash(self, switch, old=False): api_key, client_id, client_secret = self.get_api_keys(switch) if old and switch == 'own': client_id = client_id.replace('.apps.googleusercontent.com', '') - m = md5() - m.update(api_key.encode('utf-8')) - m.update(client_id.encode('utf-8')) - m.update(client_secret.encode('utf-8')) + md5_hash = md5() + md5_hash.update(api_key.encode('utf-8')) + md5_hash.update(client_id.encode('utf-8')) + md5_hash.update(client_secret.encode('utf-8')) - return m.hexdigest() + return md5_hash.hexdigest() def _strip_api_keys(self, api_key, client_id, client_secret): @@ -205,24 +234,11 @@ def _strip_api_keys(self, api_key, client_id, client_secret): return return_key, return_id, return_secret -notification_data = {'use_httpd': (__settings.use_mpd_videos() - or __settings.api_config_page()), - 'httpd_port': __settings.httpd_port(), - 'whitelist': __settings.httpd_whitelist(), - 'httpd_address': __settings.httpd_listen() - } - -__context.send_notification('check_settings', notification_data) - -_api_check = APICheck(__context, __settings) +_api_check = APICheck(Context(plugin_id='plugin.video.youtube')) keys_changed = _api_check.changed current_user = _api_check.get_current_user() -api = {} -api['key'], api['id'], api['secret'] = _api_check.get_api_keys(_api_check.get_current_switch()) - -youtube_tv = {} -youtube_tv['key'], youtube_tv['id'], youtube_tv['secret'] = _api_check.get_api_keys('youtube-tv') - +api = _api_check.get_api_keys(_api_check.get_current_switch()) +youtube_tv = _api_check.get_api_keys('youtube-tv') developer_keys = _api_check.get_api_keys('developer') From 34851601fee177bc33cc35c4fdf8200e2b0f3f16 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:01:30 +1100 Subject: [PATCH 080/141] Remove/add Unicode encoding/decoding - Additions required for Python 2/3 compatibility - Removals not required for Python 3 - Will require use of kodi-six in Kodi Leia --- .../kodion/abstract_provider.py | 22 ++++++------- .../kodion/context/abstract_context.py | 2 +- resources/lib/youtube_plugin/kodion/debug.py | 7 ++-- .../kodion/json_store/json_store.py | 12 ++++--- .../kodion/sql_store/search_history.py | 3 +- .../kodion/sql_store/storage.py | 19 ++++++++--- .../kodion/ui/xbmc/xbmc_context_ui.py | 32 +++++++------------ .../youtube_plugin/kodion/utils/__init__.py | 2 -- .../youtube_plugin/kodion/utils/methods.py | 18 ++--------- 9 files changed, 52 insertions(+), 65 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 61fec5695..cf49f8732 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -10,19 +10,18 @@ import json import re -from urllib.parse import quote -from urllib.parse import unquote +from urllib.parse import quote, unquote +from . import constants from .exceptions import KodionException from .items import ( - from_json, - to_jsons, DirectoryItem, NewSearchItem, - SearchHistoryItem + SearchHistoryItem, + from_json, + to_jsons, ) -from .utils import to_unicode, to_utf8 -from . import constants +from .utils import to_unicode class AbstractProvider(object): @@ -245,12 +244,9 @@ def _internal_search(self, context, re_match): incognito = context.get_param('incognito', False) channel_id = context.get_param('channel_id', '') - query = to_utf8(query) - try: - encoded = json.dumps({'query': quote(query)}) - except KeyError: - encoded = json.dumps({'query': quote(query.encode('utf8'))}) - self._data_cache.set_item('search_query', encoded) + self._data_cache.set_item('search_query', + json.dumps({'query': quote(query)}, + ensure_ascii=False)) if not incognito and not channel_id: try: diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index a46327767..aff45b4d1 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -219,7 +219,7 @@ def create_uri(self, path='/', params=None): uri = "%s://%s/" % ('plugin', str(self._plugin_id)) if params: - uri = '?'.join([uri, urlencode(params, encoding='utf-8')]) + uri = '?'.join([uri, urlencode(params)]) return uri diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index cf12d73b4..b1f22a02c 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -8,8 +8,9 @@ See LICENSES/GPL-2.0-only for more information. """ -import os import json +import os +from io import open from .logger import log_debug @@ -41,10 +42,10 @@ def runtime(context, addon_version, elapsed, single_file=True): with open(debug_file, 'a') as _: pass # touch - with open(debug_file, 'r') as f: + with open(debug_file, 'r', encoding='utf-8') as f: contents = f.read() - with open(debug_file, 'w') as f: + with open(debug_file, 'w', encoding='utf-8') as f: contents = json.loads(contents) if contents else default_contents if not single_file: items = contents.get('runtimes', []) diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 09d5e5f9b..87e1d6180 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -9,16 +9,17 @@ import json import os +from io import open -from xbmcaddon import Addon +import xbmcaddon import xbmcvfs from ..logger import log_debug, log_error -from ..utils import make_dirs, merge_dicts +from ..utils import make_dirs, merge_dicts, to_unicode _addon_id = 'plugin.video.youtube' -_addon = Addon(_addon_id) +_addon = xbmcaddon.Addon(_addon_id) class JSONStore(object): @@ -55,7 +56,10 @@ def save(self, data, update=False, process=None): raise ValueError _data = json.loads(json.dumps(data)) with open(self.filename, mode='w', encoding='utf-8') as jsonfile: - json.dump(_data, jsonfile, indent=4, sort_keys=True) + jsonfile.write(to_unicode(json.dumps(_data, + ensure_ascii=False, + indent=4, + sort_keys=True))) self._data = process(_data) if process is not None else _data except (IOError, OSError): log_error('JSONStore.save |{filename}| no access to file'.format( diff --git a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py index a06d8c427..90c8d1cb9 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -11,7 +11,6 @@ from hashlib import md5 from .storage import Storage -from ..utils import to_utf8 class SearchHistory(Storage): @@ -33,7 +32,7 @@ def clear(self): @staticmethod def _make_id(search_text): md5_hash = md5() - md5_hash.update(to_utf8(search_text)) + md5_hash.update(search_text.encode('utf-8')) return md5_hash.hexdigest() def rename(self, old_search_text, new_search_text): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 28b7bc705..8983a00b7 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -26,6 +26,11 @@ class Storage(object): ONE_WEEK = 7 * ONE_DAY ONE_MONTH = 4 * ONE_WEEK + _key = str('key') + _time = str('time') + _value = str('value') + _timestamp = str('timestamp') + _table_name = 'storage' _clear_query = 'DELETE FROM %s' % _table_name _create_table_query = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % _table_name @@ -51,7 +56,7 @@ def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._table_created = False self._needs_commit = False - sqlite3.register_converter('timestamp', self._convert_timestamp) + sqlite3.register_converter(self._timestamp, self._convert_timestamp) def set_max_item_count(self, max_item_count): self._max_item_count = max_item_count @@ -184,7 +189,8 @@ def _optimize_item_count(self): query = self._optimize_item_query.format(self._max_item_count) self._open() item_ids = self._execute(False, query) - item_ids = [item_id['key'] for item_id in item_ids] + key = self._key + item_ids = [item_id[key] for item_id in item_ids] if item_ids: self._remove_all(item_ids) self._close() @@ -210,7 +216,7 @@ def _is_empty(self): @staticmethod def _decode(obj, process=None): - decoded_obj = pickle.loads(obj, encoding='utf-8') + decoded_obj = pickle.loads(obj) if process: return process(decoded_obj) return decoded_obj @@ -228,7 +234,7 @@ def _get(self, item_id): result = result.fetchone() self._close() if result: - return self._decode(result['value']), result['time'] + return self._decode(result[self._value]), result[self._time] return None def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, @@ -246,8 +252,11 @@ def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, self._open() result = self._execute(False, query, item_ids) + key = self._key + time = self._time + value = self._value result = [ - (item['key'], item['time'], self._decode(item['value'], process)) + (item[key], item[time], self._decode(item[value], process)) for item in result ] self._close() diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index ce6eb53cc..954b3af18 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -110,35 +110,27 @@ def on_select(self, title, items=None): return _dict.get(result, -1) - def show_notification(self, message, header='', image_uri='', time_milliseconds=5000, audible=True): + def show_notification(self, + message, + header='', + image_uri='', + time_milliseconds=5000, + audible=True): _header = header if not _header: _header = self._context.get_name() - _header = utils.to_utf8(_header) _image = image_uri if not _image: _image = self._context.get_icon() - if isinstance(message, str): - message = utils.to_unicode(message) + _message = message.replace(',', ' ').replace('\n', ' ') - try: - _message = utils.to_utf8(message.decode('unicode-escape')) - except (AttributeError, UnicodeEncodeError): - _message = utils.to_utf8(message) - - try: - _message = _message.replace(',', ' ') - _message = _message.replace('\n', ' ') - except TypeError: - _message = _message.replace(b',', b' ') - _message = _message.replace(b'\n', b' ') - _message = utils.to_unicode(_message) - _header = utils.to_unicode(_header) - - # xbmc.executebuiltin("Notification(%s, %s, %d, %s)" % (_header, _message, time_milliseconds, _image)) - xbmcgui.Dialog().notification(_header, _message, _image, time_milliseconds, audible) + xbmcgui.Dialog().notification(_header, + _message, + _image, + time_milliseconds, + audible) def open_settings(self): self._xbmc_addon.openSettings() diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index a74ac558a..e586f8a59 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -25,7 +25,6 @@ strip_html_from_text, to_str, to_unicode, - to_utf8, ) from .monitor import YouTubeMonitor from .player import YouTubePlayer @@ -50,7 +49,6 @@ 'strip_html_from_text', 'to_str', 'to_unicode', - 'to_utf8', 'YouTubeMonitor', 'YouTubePlayer', ) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 745603376..adb6455e2 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -16,8 +16,8 @@ from math import floor, log from urllib.parse import quote +import xbmc import xbmcvfs -from xbmc import executeJSONRPC __all__ = ( @@ -37,7 +37,6 @@ 'strip_html_from_text', 'to_str', 'to_unicode', - 'to_utf8', ) @@ -54,23 +53,12 @@ def to_str(text): return text -def to_utf8(text): - result = text - if isinstance(text, str): - try: - result = text.encode('utf-8', 'ignore') - except UnicodeDecodeError: - pass - - return result - - def to_unicode(text): result = text if isinstance(text, (bytes, str)): try: result = text.decode('utf-8', 'ignore') - except (AttributeError, UnicodeEncodeError): + except (AttributeError, UnicodeError): pass return result @@ -320,7 +308,7 @@ def merge_dicts(item1, item2, templates=None, _=Ellipsis): return new or _ def get_kodi_setting(setting): - json_query = executeJSONRPC(json.dumps({ + json_query = xbmc.executeJSONRPC(json.dumps({ 'jsonrpc': '2.0', 'method': 'Settings.GetSettingValue', 'params': {'setting': setting}, From ef9ca693c6f8f4b712ae1f5d60f794bbd55ad5cf Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:05:37 +1100 Subject: [PATCH 081/141] Workaround for missing datetime.timestamp method in Python < v3.3 - Also update default fmt parameter in strptime wrapper --- .../youtube_plugin/kodion/sql_store/storage.py | 7 ++++--- .../kodion/utils/datetime_parser.py | 12 ++++++------ .../youtube_plugin/youtube/client/youtube.py | 17 +++++++++-------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 8983a00b7..0487ea00e 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -17,6 +17,7 @@ from traceback import print_exc from ..logger import log_error +from ..utils.datetime_parser import since_epoch class Storage(object): @@ -160,7 +161,7 @@ def _sync(self): def _set(self, item_id, item): # add 1 microsecond, required for dbapi2 - now = datetime.now().timestamp() + 0.000001 + now = since_epoch(datetime.now()) + 0.000001 self._open() self._execute(True, self._set_query, values=[item_id, now, @@ -170,7 +171,7 @@ def _set(self, item_id, item): def _set_all(self, items): # add 1 microsecond, required for dbapi2 - now = datetime.now().timestamp() + 0.000001 + now = since_epoch(datetime.now()) + 0.000001 self._open() self._execute(True, self._set_query, values=[(key, now, self._encode(json.dumps(item))) @@ -322,7 +323,7 @@ def get_seconds_diff(self, current_stamp): return time_delta.total_seconds() if isinstance(current_stamp, (float, int)): - return current_datetime.timestamp() - current_stamp + return since_epoch(current_datetime) - current_stamp stamp_datetime = self._parse_datetime_string(current_stamp) if not stamp_datetime: diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 16d4cefbb..9890f15b9 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -173,14 +173,14 @@ def datetime_to_since(context, dt): return ' '.join((context.format_date_short(dt), context.format_time(dt))) -def strptime(s, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): +def strptime(s, fmt='%Y-%m-%dT%H:%M:%S'): # noinspection PyUnresolvedReferences - ms_precision = '.' in s[-5:-1] - if fmt == '%Y-%m-%dT%H:%M:%S.%fZ' and not ms_precision: - fmt = '%Y-%m-%dT%H:%M:%SZ' - elif fmt == '%Y-%m-%dT%H:%M:%SZ' and ms_precision: - fmt = '%Y-%m-%dT%H:%M:%S.%fZ' + ms_precision_required = '.' in s[-5:-1] + if ms_precision_required: + fmt.replace('%S', '%S.%f') + else: + fmt.replace('%S.%f', '%S') import _strptime diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index cb86ad423..88294de7c 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -417,12 +417,13 @@ def helper(video_id, responses): items.append(item) # Finally sort items per page by date for a better distribution - sorted_items.sort( - key=lambda a: ( - a['page_number'], - -datetime_parser.parse(a['snippet']['publishedAt']).timestamp() - ), - ) + def _sort_by_date_time(item): + return (item['page_number'], + -datetime_parser.since_epoch(datetime_parser.parse( + item['snippet']['publishedAt'] + ))) + + sorted_items.sort(key=_sort_by_date_time) # Finalize result payload['items'] = sorted_items @@ -873,9 +874,9 @@ def fetch_xml(_url, _responses): _result['items'].append(entry_data) # sorting by publish date - def _sort_by_date_time(e): + def _sort_by_date_time(item): return datetime_parser.since_epoch( - datetime_parser.strptime(e["published"][0:19], "%Y-%m-%dT%H:%M:%S") + datetime_parser.strptime(item['published'][0:19]) ) _result['items'].sort(reverse=True, key=_sort_by_date_time) From f4d41dba2a082f11347f3705d88c84bbfb21a99a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:12:46 +1100 Subject: [PATCH 082/141] Add Python2 and Kodi 18 compatibility tweaks - Ensure new style classes are used - Use correct inputstream property for listitems - Use correct infolabel date formatting - Use correct setting type getter/setter for str list - Ignore labelmask parameter in sort methods - Use traditional packages rather than namespace packages --- .../youtube_plugin/kodion/context/xbmc/__init__.py | 0 .../kodion/context/xbmc/xbmc_context.py | 13 +++++++------ .../youtube_plugin/kodion/network/http_server.py | 9 +++++---- .../youtube_plugin/kodion/player/xbmc/__init__.py | 0 .../youtube_plugin/kodion/plugin/xbmc/__init__.py | 0 .../youtube_plugin/kodion/settings/xbmc/__init__.py | 0 .../kodion/settings/xbmc/xbmc_plugin_settings.py | 8 ++++---- .../lib/youtube_plugin/kodion/ui/xbmc/__init__.py | 0 .../youtube_plugin/kodion/ui/xbmc/info_labels.py | 8 +++++--- .../lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py | 11 ++++++----- .../youtube_plugin/kodion/utils/system_version.py | 3 +++ 11 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 resources/lib/youtube_plugin/kodion/context/xbmc/__init__.py create mode 100644 resources/lib/youtube_plugin/kodion/player/xbmc/__init__.py create mode 100644 resources/lib/youtube_plugin/kodion/plugin/xbmc/__init__.py create mode 100644 resources/lib/youtube_plugin/kodion/settings/xbmc/__init__.py create mode 100644 resources/lib/youtube_plugin/kodion/ui/xbmc/__init__.py diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/__init__.py b/resources/lib/youtube_plugin/kodion/context/xbmc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index ecfbb28e9..bfe2cc640 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -20,11 +20,11 @@ import xbmcvfs from ..abstract_context import AbstractContext -from ... import utils from ...player.xbmc.xbmc_player import XbmcPlayer from ...player.xbmc.xbmc_playlist import XbmcPlaylist from ...settings.xbmc.xbmc_plugin_settings import XbmcPluginSettings from ...ui.xbmc.xbmc_context_ui import XbmcContextUI +from ...utils import current_system_version, loose_version, to_unicode class XbmcContext(AbstractContext): @@ -403,7 +403,7 @@ def localize(self, text_id, default_text=''): """ source = self._addon if 30000 <= text_id < 31000 else xbmc result = source.getLocalizedString(text_id) - result = utils.to_unicode(result) if result else default_text + result = to_unicode(result) if result else default_text return result def set_content_type(self, content_type): @@ -411,8 +411,9 @@ def set_content_type(self, content_type): xbmcplugin.setContent(self._plugin_handle, content_type) def add_sort_method(self, *sort_methods): + args = slice(None if current_system_version.compatible(19, 0) else 2) for sort_method in sort_methods: - xbmcplugin.addSortMethod(self._plugin_handle, *sort_method) + xbmcplugin.addSortMethod(self._plugin_handle, *sort_method[args]) def clone(self, new_path=None, new_params=None): if not new_path: @@ -524,16 +525,16 @@ def inputstream_adaptive_capabilities(self, capability=None): if not self.use_inputstream_adaptive() or not inputstream_version: return frozenset() if capability is None else None - isa_loose_version = utils.loose_version(inputstream_version) + isa_loose_version = loose_version(inputstream_version) if capability is None: capabilities = frozenset( capability for capability, version in self._ISA_CAPABILITIES.items() if version is True - or version and isa_loose_version >= utils.loose_version(version) + or version and isa_loose_version >= loose_version(version) ) return capabilities version = self._ISA_CAPABILITIES.get(capability) - return version is True or version and isa_loose_version >= utils.loose_version(version) + return version is True or version and isa_loose_version >= loose_version(version) @staticmethod def inputstream_adaptive_auto_stream_selection(): diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index e5fd5422e..56c5353a9 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -10,15 +10,16 @@ import json import os import re -from socket import error as socket_error from http import server as BaseHTTPServer +from io import open +from socket import error as socket_error from textwrap import dedent from urllib.parse import parse_qs, urlparse -from xbmc import getCondVisibility, executebuiltin +from xbmc import executebuiltin, getCondVisibility +from xbmcaddon import Addon from xbmcgui import Dialog, Window from xbmcvfs import translatePath -from xbmcaddon import Addon from .requests import BaseRequestsClass from ..logger import log_debug @@ -32,7 +33,7 @@ _server_requests = BaseRequestsClass() -class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): +class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): base_path = translatePath('special://temp/{0}'.format(_addon_id)) chunk_size = 1024 * 64 local_ranges = ( diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/__init__.py b/resources/lib/youtube_plugin/kodion/player/xbmc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/__init__.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/__init__.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index be5b888e7..1cf235bc4 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -24,7 +24,7 @@ def __init__(self, xbmc_addon): if self._funcs: return - if current_system_version.get_version() >= (20, 0): + if current_system_version.compatible(20, 0): _class = xbmcaddon.Settings self._funcs.update({ @@ -41,11 +41,11 @@ def __init__(self, xbmc_addon): _class = xbmcaddon.Addon def _get_string_list(store, setting): - return _class.getSettingString(store, setting).split(',') + return _class.getSetting(store, setting).split(',') def _set_string_list(store, setting, value): value = ','.join(value) - return _class.setSettingString(store, setting, value) + return _class.setSetting(store, setting, value) self._funcs.update({ 'get_bool': _class.getSettingBool, @@ -62,7 +62,7 @@ def _set_string_list(store, setting, value): def flush(cls, xbmc_addon): cls._echo = get_kodi_setting('debug.showloginfo') cls._cache = {} - if current_system_version.get_version() >= (20, 0): + if current_system_version.compatible(20, 0): cls._store = xbmc_addon.getSettings() else: cls._store = xbmc_addon diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/__init__.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index 35d74db9c..b3d13fc9d 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -8,8 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ -from ... import utils from ...items import AudioItem, DirectoryItem, ImageItem, VideoItem +from ...utils import current_system_version, datetime_parser def _process_date_value(info_labels, name, param): @@ -19,7 +19,9 @@ def _process_date_value(info_labels, name, param): def _process_datetime_value(info_labels, name, param): if param: - info_labels[name] = param.isoformat('T') + info_labels[name] = (param.isoformat('T') + if current_system_version.compatible(19, 0) else + param.strftime('%d.%d.%Y')) def _process_int_value(info_labels, name, param): @@ -64,7 +66,7 @@ def _process_video_rating(info_labels, param): def _process_date_string(info_labels, name, param): if param: - date = utils.datetime_parser.parse(param) + date = datetime_parser.parse(param) info_labels[name] = date.isoformat() diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index e16416ee6..380c7868b 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -12,7 +12,7 @@ from . import info_labels from ...items import AudioItem, UriItem, VideoItem -from ...utils import datetime_parser +from ...utils import current_system_version, datetime_parser try: @@ -34,10 +34,8 @@ def add_stream_info(self, *args, **kwargs): def set_resume_point(self, infoproperties, - *_args, resume_key='ResumeTime', - total_key='TotalTime', - **_kwargs): + total_key='TotalTime',): if resume_key in infoproperties: infoproperties[resume_key] = str(infoproperties[resume_key]) if total_key in infoproperties: @@ -95,7 +93,10 @@ def video_playback_item(context, video_item): manifest_type = 'hls' mime_type = 'application/x-mpegURL' - props['inputstream'] = 'inputstream.adaptive' + inputstream_property = ('inputstream' + if current_system_version.compatible(19, 0) else + 'inputstreamaddon') + props[inputstream_property] = 'inputstream.adaptive' props['inputstream.adaptive.manifest_type'] = manifest_type if headers: diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py index d51db45fa..51e19a760 100644 --- a/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -90,5 +90,8 @@ def get_version(self): def get_app_name(self): return self._appname + def compatible(self, *version): + return self._version >= version + current_system_version = SystemVersion() From 91e38eb6ca45e97132fbf00993981436059d9aa7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:14:07 +1100 Subject: [PATCH 083/141] Ensure current_user value is loaded as int --- .../youtube_plugin/kodion/json_store/access_manager.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index e54b0dc62..f4d152802 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -111,6 +111,11 @@ def _process_data(data): int(key): value for key, value in users.items() } + current_user = data['access_manager']['current_user'] + try: + data['access_manager']['current_user'] = int(current_user) + except (TypeError, ValueError): + pass return data def get_data(self, process=_process_data.__func__): @@ -216,6 +221,11 @@ def set_user(self, user, switch_to=False): :param switch_to: boolean, change current user :return: """ + try: + user = int(user) + except (TypeError, ValueError): + pass + self._user = user if switch_to: data = { From b16e5ac5b11e61359bb9a2edc2b9c6fa2904c3cd Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 9 Dec 2023 16:45:53 +1100 Subject: [PATCH 084/141] Kodi Leia compatibility --- resources/lib/__init__.py | 3 + resources/lib/default.py | 2 + resources/lib/startup.py | 2 + resources/lib/youtube_authentication.py | 2 + resources/lib/youtube_plugin/__init__.py | 3 + .../kodion/abstract_provider.py | 4 +- .../kodion/compatibility/__init__.py | 116 ++++++++++++++++++ .../kodion/constants/__init__.py | 2 + .../kodion/constants/const_content_types.py | 2 + .../kodion/constants/const_paths.py | 2 + .../kodion/constants/const_settings.py | 2 + .../kodion/constants/const_sort_methods.py | 5 +- .../youtube_plugin/kodion/context/__init__.py | 2 + .../kodion/context/abstract_context.py | 4 +- .../kodion/context/xbmc/xbmc_context.py | 18 ++- resources/lib/youtube_plugin/kodion/debug.py | 2 + .../youtube_plugin/kodion/items/__init__.py | 2 + .../youtube_plugin/kodion/items/audio_item.py | 5 +- .../youtube_plugin/kodion/items/base_item.py | 4 +- .../kodion/items/directory_item.py | 2 + .../kodion/items/favorites_item.py | 2 + .../youtube_plugin/kodion/items/image_item.py | 2 + .../kodion/items/new_search_item.py | 2 + .../kodion/items/next_page_item.py | 2 + .../kodion/items/search_history_item.py | 2 + .../kodion/items/search_item.py | 2 + .../youtube_plugin/kodion/items/uri_item.py | 2 + .../lib/youtube_plugin/kodion/items/utils.py | 2 + .../youtube_plugin/kodion/items/video_item.py | 4 +- .../kodion/items/watch_later_item.py | 2 + .../kodion/json_store/__init__.py | 2 + .../kodion/json_store/api_keys.py | 2 + .../kodion/json_store/json_store.py | 6 +- resources/lib/youtube_plugin/kodion/logger.py | 6 +- .../youtube_plugin/kodion/network/__init__.py | 2 + .../kodion/network/http_server.py | 38 +++--- .../youtube_plugin/kodion/network/ip_api.py | 2 + .../youtube_plugin/kodion/network/requests.py | 8 +- .../youtube_plugin/kodion/player/__init__.py | 2 + .../kodion/player/xbmc/xbmc_player.py | 4 +- .../kodion/player/xbmc/xbmc_playlist.py | 4 +- .../youtube_plugin/kodion/plugin/__init__.py | 2 + .../kodion/plugin/xbmc/xbmc_runner.py | 4 +- resources/lib/youtube_plugin/kodion/runner.py | 2 + .../lib/youtube_plugin/kodion/service.py | 2 + .../kodion/settings/__init__.py | 2 + .../kodion/settings/abstract_settings.py | 2 + .../settings/xbmc/xbmc_plugin_settings.py | 3 +- .../kodion/sql_store/data_cache.py | 2 + .../kodion/sql_store/favorite_list.py | 2 + .../kodion/sql_store/function_cache.py | 2 + .../kodion/sql_store/playback_history.py | 2 + .../kodion/sql_store/search_history.py | 2 + .../kodion/sql_store/storage.py | 2 + .../kodion/sql_store/watch_later_list.py | 2 + .../lib/youtube_plugin/kodion/ui/__init__.py | 2 + .../kodion/ui/xbmc/info_labels.py | 2 + .../kodion/ui/xbmc/xbmc_context_ui.py | 4 +- .../kodion/ui/xbmc/xbmc_items.py | 13 +- .../kodion/ui/xbmc/xbmc_progress_dialog.py | 4 +- .../youtube_plugin/kodion/utils/__init__.py | 2 + .../kodion/utils/datetime_parser.py | 2 + .../youtube_plugin/kodion/utils/methods.py | 6 +- .../youtube_plugin/kodion/utils/monitor.py | 12 +- .../lib/youtube_plugin/kodion/utils/player.py | 4 +- .../kodion/utils/system_version.py | 4 +- resources/lib/youtube_plugin/refresh.py | 3 + .../lib/youtube_plugin/youtube/__init__.py | 2 + .../youtube/client/__config__.py | 2 + .../youtube_plugin/youtube/client/__init__.py | 2 + .../youtube/client/login_client.py | 4 +- .../youtube/client/request_client.py | 2 + .../youtube_plugin/youtube/client/youtube.py | 6 +- .../youtube_plugin/youtube/helper/__init__.py | 2 + .../youtube/helper/ratebypass/__init__.py | 2 + .../youtube/helper/ratebypass/ratebypass.py | 2 + .../youtube/helper/resource_manager.py | 2 + .../youtube/helper/signature/__init__.py | 2 + .../youtube/helper/signature/cipher.py | 2 + .../youtube/helper/subtitles.py | 16 ++- .../lib/youtube_plugin/youtube/helper/tv.py | 2 + .../youtube/helper/url_resolver.py | 5 +- .../youtube/helper/url_to_item_converter.py | 4 +- .../youtube_plugin/youtube/helper/utils.py | 2 + .../lib/youtube_plugin/youtube/helper/v3.py | 2 + .../youtube/helper/video_info.py | 22 ++-- .../youtube/helper/yt_context_menu.py | 2 + .../youtube_plugin/youtube/helper/yt_login.py | 2 + .../youtube/helper/yt_old_actions.py | 2 + .../youtube_plugin/youtube/helper/yt_play.py | 2 + .../youtube/helper/yt_playlist.py | 2 + .../youtube/helper/yt_setup_wizard.py | 2 + .../youtube/helper/yt_specials.py | 2 + .../youtube/helper/yt_subscriptions.py | 2 + .../youtube_plugin/youtube/helper/yt_video.py | 2 + .../lib/youtube_plugin/youtube/provider.py | 6 +- .../youtube/youtube_exceptions.py | 2 + resources/lib/youtube_registration.py | 2 + resources/lib/youtube_requests.py | 2 + resources/lib/youtube_resolver.py | 2 + 100 files changed, 396 insertions(+), 90 deletions(-) create mode 100644 resources/lib/youtube_plugin/kodion/compatibility/__init__.py diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py index d2d4112fb..2c7daf8bc 100644 --- a/resources/lib/__init__.py +++ b/resources/lib/__init__.py @@ -8,4 +8,7 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + + __all__ = ['youtube_plugin'] diff --git a/resources/lib/default.py b/resources/lib/default.py index 10385f513..ecb6f2d3a 100644 --- a/resources/lib/default.py +++ b/resources/lib/default.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from youtube_plugin.kodion import runner from youtube_plugin import youtube diff --git a/resources/lib/startup.py b/resources/lib/startup.py index 36f71ca9c..31b84ed00 100644 --- a/resources/lib/startup.py +++ b/resources/lib/startup.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from youtube_plugin.kodion import service service.run() diff --git a/resources/lib/youtube_authentication.py b/resources/lib/youtube_authentication.py index 08abf56c9..e29822074 100644 --- a/resources/lib/youtube_authentication.py +++ b/resources/lib/youtube_authentication.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from youtube_plugin.youtube.provider import Provider from youtube_plugin.kodion.context import Context from youtube_plugin.youtube.helper import yt_login diff --git a/resources/lib/youtube_plugin/__init__.py b/resources/lib/youtube_plugin/__init__.py index 02f46fa36..84fc92d21 100644 --- a/resources/lib/youtube_plugin/__init__.py +++ b/resources/lib/youtube_plugin/__init__.py @@ -8,6 +8,9 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + + key_sets = { 'youtube-tv': { 'id': 'ODYxNTU2NzA4NDU0LWQ2ZGxtM2xoMDVpZGQ4bnBlazE4azZiZThiYTNvYzY4', diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index cf49f8732..b346a196e 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -8,11 +8,13 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import re -from urllib.parse import quote, unquote from . import constants +from .compatibility import quote, unquote from .exceptions import KodionException from .items import ( DirectoryItem, diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py new file mode 100644 index 000000000..904da9afc --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2023-present plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +try: + from html import unescape + from http import server as BaseHTTPServer + from urllib.parse import ( + parse_qs, + parse_qsl, + quote, + unquote, + urlencode, + urljoin, + urlparse, + urlsplit, + urlunsplit, + ) + + import xbmc + import xbmcaddon + import xbmcgui + import xbmcplugin + import xbmcvfs + +except ImportError: + import BaseHTTPServer + from contextlib import contextmanager as _contextmanager + from urllib import ( + quote as _quote, + unquote as _unquote, + urlencode as _urlencode, + ) + from urlparse import ( + parse_qs, + parse_qsl, + urljoin, + urlparse, + urlsplit, + urlunsplit, + ) + from xml.sax.saxutils import unescape + + from kodi_six import ( + xbmc, + xbmcaddon, + xbmcgui, + xbmcplugin, + xbmcvfs, + ) + + + def quote(data, *args, **kwargs): + return _quote(data.encode('utf-8'), *args, **kwargs) + + + def unquote(data): + return _unquote(data.encode('utf-8')) + + + def urlencode(data, *args, **kwargs): + if isinstance(data, dict): + data = data.items() + return _urlencode({ + key.encode('utf-8'): ( + [part.encode('utf-8') if isinstance(part, unicode) + else str(part) + for part in value] if isinstance(value, (list, tuple)) + else value.encode('utf-8') if isinstance(value, unicode) + else str(value) + ) + for key, value in data + }, *args, **kwargs) + + + _File = xbmcvfs.File + + + @_contextmanager + def _file_closer(*args, **kwargs): + file = None + try: + file = _File(*args, **kwargs) + yield file + finally: + if file: + file.close() + + + xbmcvfs.File = _file_closer + xbmcvfs.translatePath = xbmc.translatePath + + +__all__ = ( + 'BaseHTTPServer', + 'parse_qs', + 'parse_qsl', + 'quote', + 'unescape', + 'unquote', + 'urlencode', + 'urljoin', + 'urlparse', + 'urlsplit', + 'urlunsplit', + 'xbmc', + 'xbmcaddon', + 'xbmcgui', + 'xbmcplugin', + 'xbmcvfs', +) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 655985e9b..91e90d688 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from . import const_settings as setting from . import const_sort_methods as sort_method from . import const_content_types as content_type diff --git a/resources/lib/youtube_plugin/kodion/constants/const_content_types.py b/resources/lib/youtube_plugin/kodion/constants/const_content_types.py index 0c2998b59..5990ee1f6 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_content_types.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_content_types.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + FILES = 'files' SONGS = 'songs' ARTISTS = 'artists' diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index 59e254ac4..8903eb34d 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + SEARCH = 'kodion/search' FAVORITES = 'kodion/favorites' WATCH_LATER = 'kodion/watch_later' diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 70501103b..16ddff484 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + THUMB_SIZE = 'kodion.thumbnail.size' # (int) SHOW_FANART = 'kodion.fanart.show' # (bool) SAFE_SEARCH = 'kodion.safe.search' # (int) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py b/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py index c8d2cdb8e..8b2f6437d 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_sort_methods.py @@ -8,11 +8,14 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import sys -from xbmcplugin import __dict__ as xbmcplugin +from ..compatibility import xbmcplugin +xbmcplugin = xbmcplugin.__dict__ namespace = sys.modules[__name__] names = [ 'NONE', # 0 diff --git a/resources/lib/youtube_plugin/kodion/context/__init__.py b/resources/lib/youtube_plugin/kodion/context/__init__.py index 89d69a6ec..1d90e85c3 100644 --- a/resources/lib/youtube_plugin/kodion/context/__init__.py +++ b/resources/lib/youtube_plugin/kodion/context/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .xbmc.xbmc_context import XbmcContext as Context diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index aff45b4d1..765cc61e4 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -8,10 +8,12 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import os -from urllib.parse import urlencode from .. import constants, logger +from ..compatibility import urlencode from ..json_store import AccessManager from ..sql_store import ( DataCache, diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index bfe2cc640..65e652b4f 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -8,18 +8,24 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import os import sys import weakref -from urllib.parse import parse_qsl, quote, unquote, urlparse - -import xbmc -import xbmcaddon -import xbmcplugin -import xbmcvfs from ..abstract_context import AbstractContext +from ...compatibility import ( + parse_qsl, + quote, + unquote, + urlparse, + xbmc, + xbmcaddon, + xbmcplugin, + xbmcvfs, +) from ...player.xbmc.xbmc_player import XbmcPlayer from ...player.xbmc.xbmc_playlist import XbmcPlaylist from ...settings.xbmc.xbmc_plugin_settings import XbmcPluginSettings diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index b1f22a02c..3160c47ec 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import os from io import open diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index be32dbd74..fff783f35 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .utils import to_json, from_json, to_jsons from .uri_item import UriItem diff --git a/resources/lib/youtube_plugin/kodion/items/audio_item.py b/resources/lib/youtube_plugin/kodion/items/audio_item.py index a7170fc5a..226113940 100644 --- a/resources/lib/youtube_plugin/kodion/items/audio_item.py +++ b/resources/lib/youtube_plugin/kodion/items/audio_item.py @@ -8,9 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -from .base_item import BaseItem +from __future__ import absolute_import, division, unicode_literals -from html import unescape +from .base_item import BaseItem +from ..compatibility import unescape class AudioItem(BaseItem): diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 5054f3a29..44d8a89d3 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -8,10 +8,12 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import hashlib import datetime -from html import unescape +from ..compatibility import unescape class BaseItem(object): diff --git a/resources/lib/youtube_plugin/kodion/items/directory_item.py b/resources/lib/youtube_plugin/kodion/items/directory_item.py index 14b1702f8..2738c0b03 100644 --- a/resources/lib/youtube_plugin/kodion/items/directory_item.py +++ b/resources/lib/youtube_plugin/kodion/items/directory_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .base_item import BaseItem diff --git a/resources/lib/youtube_plugin/kodion/items/favorites_item.py b/resources/lib/youtube_plugin/kodion/items/favorites_item.py index a32c5b756..8e25552bb 100644 --- a/resources/lib/youtube_plugin/kodion/items/favorites_item.py +++ b/resources/lib/youtube_plugin/kodion/items/favorites_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .directory_item import DirectoryItem from .. import constants diff --git a/resources/lib/youtube_plugin/kodion/items/image_item.py b/resources/lib/youtube_plugin/kodion/items/image_item.py index 2a1217135..1edb640e6 100644 --- a/resources/lib/youtube_plugin/kodion/items/image_item.py +++ b/resources/lib/youtube_plugin/kodion/items/image_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .base_item import BaseItem diff --git a/resources/lib/youtube_plugin/kodion/items/new_search_item.py b/resources/lib/youtube_plugin/kodion/items/new_search_item.py index f0985d96d..18f79c97e 100644 --- a/resources/lib/youtube_plugin/kodion/items/new_search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/new_search_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .directory_item import DirectoryItem from .. import constants diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index fc4b83576..ee266e52b 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .directory_item import DirectoryItem diff --git a/resources/lib/youtube_plugin/kodion/items/search_history_item.py b/resources/lib/youtube_plugin/kodion/items/search_history_item.py index 42e9f7c1f..e53d020aa 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_history_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_history_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .directory_item import DirectoryItem from ..constants.const_paths import SEARCH diff --git a/resources/lib/youtube_plugin/kodion/items/search_item.py b/resources/lib/youtube_plugin/kodion/items/search_item.py index 39869c2c0..2ff9bbd1f 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .directory_item import DirectoryItem from ..constants.const_paths import SEARCH diff --git a/resources/lib/youtube_plugin/kodion/items/uri_item.py b/resources/lib/youtube_plugin/kodion/items/uri_item.py index e30df6e7e..c5926afb2 100644 --- a/resources/lib/youtube_plugin/kodion/items/uri_item.py +++ b/resources/lib/youtube_plugin/kodion/items/uri_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .base_item import BaseItem diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index cabfc1885..64e0966ba 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json from .audio_item import AudioItem diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 234b5c160..2e40a3f6a 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -8,11 +8,13 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import datetime import re -from html import unescape from .base_item import BaseItem +from ..compatibility import unescape from ..utils import duration_to_seconds, seconds_to_duration diff --git a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py index 0a10c27f0..01cfc0455 100644 --- a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py +++ b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .directory_item import DirectoryItem from .. import constants diff --git a/resources/lib/youtube_plugin/kodion/json_store/__init__.py b/resources/lib/youtube_plugin/kodion/json_store/__init__.py index 96383f1c0..0e05ebe41 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/__init__.py +++ b/resources/lib/youtube_plugin/kodion/json_store/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .access_manager import AccessManager from .api_keys import APIKeyStore diff --git a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py index 3c20a22c4..488ea8ab4 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py +++ b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .json_store import JSONStore diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 87e1d6180..b6dea6368 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -7,13 +7,13 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import os from io import open -import xbmcaddon -import xbmcvfs - +from ..compatibility import xbmcaddon, xbmcvfs from ..logger import log_debug, log_error from ..utils import make_dirs, merge_dicts, to_unicode diff --git a/resources/lib/youtube_plugin/kodion/logger.py b/resources/lib/youtube_plugin/kodion/logger.py index df709b3bb..0940faf42 100644 --- a/resources/lib/youtube_plugin/kodion/logger.py +++ b/resources/lib/youtube_plugin/kodion/logger.py @@ -8,8 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -import xbmc -import xbmcaddon +from __future__ import absolute_import, division, unicode_literals + +from .compatibility import xbmc, xbmcaddon + DEBUG = xbmc.LOGDEBUG INFO = xbmc.LOGINFO diff --git a/resources/lib/youtube_plugin/kodion/network/__init__.py b/resources/lib/youtube_plugin/kodion/network/__init__.py index 8e1c3dccf..960dd0e91 100644 --- a/resources/lib/youtube_plugin/kodion/network/__init__.py +++ b/resources/lib/youtube_plugin/kodion/network/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .http_server import get_client_ip_address, get_http_server, is_httpd_live from .ip_api import Locator from .requests import BaseRequestsClass diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 56c5353a9..ea06ddff7 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -7,34 +7,38 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import os import re -from http import server as BaseHTTPServer from io import open from socket import error as socket_error from textwrap import dedent -from urllib.parse import parse_qs, urlparse - -from xbmc import executebuiltin, getCondVisibility -from xbmcaddon import Addon -from xbmcgui import Dialog, Window -from xbmcvfs import translatePath from .requests import BaseRequestsClass +from ..compatibility import ( + BaseHTTPServer, + parse_qs, + urlparse, + xbmc, + xbmcaddon, + xbmcgui, + xbmcvfs, +) from ..logger import log_debug from ..settings import Settings _addon_id = 'plugin.video.youtube' -_addon = Addon(_addon_id) +_addon = xbmcaddon.Addon(_addon_id) _settings = Settings(_addon) _i18n = _addon.getLocalizedString _server_requests = BaseRequestsClass() class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): - base_path = translatePath('special://temp/{0}'.format(_addon_id)) + base_path = xbmcvfs.translatePath('special://temp/{0}'.format(_addon_id)) chunk_size = 1024 * 64 local_ranges = ( '10.', @@ -124,7 +128,7 @@ def do_GET(self): self.wfile.write(chunk) elif api_config_enabled and stripped_path.startswith('/api_submit'): - executebuiltin('Dialog.Close(addonsettings, true)') + xbmc.executebuiltin('Dialog.Close(addonsettings, true)') query = urlparse(self.path).query params = parse_qs(query) @@ -215,7 +219,7 @@ def do_POST(self): if not self.connection_allowed(): self.send_error(403) elif self.path.startswith('/widevine'): - home = Window(10000) + home = xbmcgui.Window(10000) lic_url = home.getProperty('plugin.video.youtube-license_url') if not lic_url: @@ -267,7 +271,7 @@ def do_POST(self): if 'HD' in authorized_types: size_limit = fmt_to_px['HD'] elif 'HD720' in authorized_types: - if getCondVisibility('system.platform.android') == 1: + if xbmc.getCondVisibility('system.platform.android') == 1: size_limit = fmt_to_px['HD720'] else: size_limit = fmt_to_px['SD'] @@ -528,11 +532,11 @@ def get_http_server(address=None, port=None): except socket_error as e: log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|' .format(address=address, port=port, response=str(e))) - Dialog().notification(_addon.getAddonInfo('name'), - str(e), - _addon.getAddonInfo('icon'), - time=5000, - sound=False) + xbmcgui.Dialog().notification(_addon.getAddonInfo('name'), + str(e), + _addon.getAddonInfo('icon'), + time=5000, + sound=False) return None diff --git a/resources/lib/youtube_plugin/kodion/network/ip_api.py b/resources/lib/youtube_plugin/kodion/network/ip_api.py index 79892c6fe..ed009455e 100644 --- a/resources/lib/youtube_plugin/kodion/network/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/network/ip_api.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .requests import BaseRequestsClass from .. import logger diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index ad83e1f44..19e8b570c 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -7,21 +7,21 @@ See LICENSES/GPL-2.0-only for more information. """ -import atexit +from __future__ import absolute_import, division, unicode_literals +import atexit from traceback import format_exc, format_stack from requests import Session from requests.adapters import HTTPAdapter, Retry from requests.exceptions import RequestException +from ..compatibility import xbmcaddon from ..logger import log_error from ..settings import Settings -from xbmcaddon import Addon - -_settings = Settings(Addon(id='plugin.video.youtube')) +_settings = Settings(xbmcaddon.Addon(id='plugin.video.youtube')) class BaseRequestsClass(object): diff --git a/resources/lib/youtube_plugin/kodion/player/__init__.py b/resources/lib/youtube_plugin/kodion/player/__init__.py index e30f5d82f..4c8c94b61 100644 --- a/resources/lib/youtube_plugin/kodion/player/__init__.py +++ b/resources/lib/youtube_plugin/kodion/player/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .xbmc.xbmc_player import XbmcPlayer as Player from .xbmc.xbmc_playlist import XbmcPlaylist as Playlist diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_player.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_player.py index 4b44cfaa7..498acb85b 100644 --- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_player.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_player.py @@ -8,8 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -import xbmc +from __future__ import absolute_import, division, unicode_literals + from ..abstract_player import AbstractPlayer +from ...compatibility import xbmc class XbmcPlayer(AbstractPlayer): diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py index bc171be58..29e03de19 100644 --- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py @@ -8,10 +8,12 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json -import xbmc from ..abstract_playlist import AbstractPlaylist +from ...compatibility import xbmc from ...items import VideoItem from ...ui.xbmc import xbmc_items diff --git a/resources/lib/youtube_plugin/kodion/plugin/__init__.py b/resources/lib/youtube_plugin/kodion/plugin/__init__.py index 19ec2ef97..19fca2ffd 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/__init__.py +++ b/resources/lib/youtube_plugin/kodion/plugin/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .xbmc.xbmc_runner import XbmcRunner as Runner diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index c4b178b6c..131451578 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -8,10 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -import xbmcgui -import xbmcplugin +from __future__ import absolute_import, division, unicode_literals from ..abstract_provider_runner import AbstractProviderRunner +from ...compatibility import xbmcgui, xbmcplugin from ...exceptions import KodionException from ...items import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem from ...player import Playlist diff --git a/resources/lib/youtube_plugin/kodion/runner.py b/resources/lib/youtube_plugin/kodion/runner.py index 63ae7b367..d03d44d69 100644 --- a/resources/lib/youtube_plugin/kodion/runner.py +++ b/resources/lib/youtube_plugin/kodion/runner.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import copy import timeit diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index 29d2a90e3..e50d59d71 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from datetime import datetime import time diff --git a/resources/lib/youtube_plugin/kodion/settings/__init__.py b/resources/lib/youtube_plugin/kodion/settings/__init__.py index 06ed1a6ba..d5432f4b3 100644 --- a/resources/lib/youtube_plugin/kodion/settings/__init__.py +++ b/resources/lib/youtube_plugin/kodion/settings/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .xbmc.xbmc_plugin_settings import XbmcPluginSettings as Settings diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 15a7d9b28..a0009b0ba 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import sys from ..constants import setting as SETTINGS diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index 1cf235bc4..d19342c02 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -8,9 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -import xbmcaddon +from __future__ import absolute_import, division, unicode_literals from ..abstract_settings import AbstractSettings +from ...compatibility import xbmcaddon from ...logger import log_debug from ...utils.methods import get_kodi_setting from ...utils.system_version import current_system_version diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index fb8ca4134..8eb571ce0 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json from datetime import datetime diff --git a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py index 11504727b..788ba1ff4 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .storage import Storage from ..items import from_json, to_json diff --git a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index 7b71ad10a..91839a64b 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from functools import partial from hashlib import md5 diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index 3acdb6400..0f9a263ad 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .storage import Storage diff --git a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py index 90c8d1cb9..bf83574ac 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from hashlib import md5 from .storage import Storage diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 0487ea00e..d8a41b7d1 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import os import pickle diff --git a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index 5da49a5d3..cb8e39743 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from datetime import datetime from .storage import Storage diff --git a/resources/lib/youtube_plugin/kodion/ui/__init__.py b/resources/lib/youtube_plugin/kodion/ui/__init__.py index b173ebd14..8f09da745 100644 --- a/resources/lib/youtube_plugin/kodion/ui/__init__.py +++ b/resources/lib/youtube_plugin/kodion/ui/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .xbmc.xbmc_context_ui import XbmcContextUI as ContextUI diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index b3d13fc9d..674d2f2bc 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ...items import AudioItem, DirectoryItem, ImageItem, VideoItem from ...utils import current_system_version, datetime_parser diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 954b3af18..b3ffb53a0 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -8,12 +8,12 @@ See LICENSES/GPL-2.0-only for more information. """ -import xbmc -import xbmcgui +from __future__ import absolute_import, division, unicode_literals from .xbmc_progress_dialog import XbmcProgressDialog, XbmcProgressDialogBG from ..abstract_context_ui import AbstractContextUI from ... import utils +from ...compatibility import xbmc, xbmcgui class XbmcContextUI(AbstractContextUI): diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index 380c7868b..0d3a0e615 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -8,9 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -from xbmcgui import ListItem +from __future__ import absolute_import, division, unicode_literals from . import info_labels +from ...compatibility import xbmcgui from ...items import AudioItem, UriItem, VideoItem from ...utils import current_system_version, datetime_parser @@ -35,7 +36,7 @@ def add_stream_info(self, *args, **kwargs): def set_resume_point(self, infoproperties, resume_key='ResumeTime', - total_key='TotalTime',): + total_key='TotalTime'): if resume_key in infoproperties: infoproperties[resume_key] = str(infoproperties[resume_key]) if total_key in infoproperties: @@ -115,7 +116,7 @@ def video_playback_item(context, video_item): if not alternative_player and headers and uri.startswith('http'): video_item.set_uri('|'.join([uri, headers])) - list_item = ListItem(**kwargs) + list_item = xbmcgui.ListItem(**kwargs) if mime_type: list_item.setContentLookup(False) list_item.setMimeType(mime_type) @@ -170,7 +171,7 @@ def audio_listitem(context, audio_item): 'ForceResolvePlugin': 'true', } - list_item = ListItem(**kwargs) + list_item = xbmcgui.ListItem(**kwargs) fanart = (context.get_settings().show_fanart() and audio_item.get_fanart() @@ -206,7 +207,7 @@ def uri_listitem(context, uri_item): 'ForceResolvePlugin': 'true', } - list_item = ListItem(**kwargs) + list_item = xbmcgui.ListItem(**kwargs) list_item.setProperties(props) return list_item @@ -226,7 +227,7 @@ def video_listitem(context, video_item): 'ForceResolvePlugin': 'true', } - list_item = ListItem(**kwargs) + list_item = xbmcgui.ListItem(**kwargs) published_at = video_item.get_added_utc() scheduled_start = video_item.get_scheduled_start_utc() diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py index fda3cde27..596ada365 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py @@ -8,8 +8,10 @@ See LICENSES/GPL-2.0-only for more information. """ -import xbmcgui +from __future__ import absolute_import, division, unicode_literals + from ..abstract_progress_dialog import AbstractProgressDialog +from ...compatibility import xbmcgui class XbmcProgressDialog(AbstractProgressDialog): diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index e586f8a59..19b0e80e7 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from . import datetime_parser from .methods import ( create_path, diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 9890f15b9..83c0cf6f2 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re import time from datetime import date, datetime, time as dt_time, timedelta diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index adb6455e2..de4d03f9f 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -8,16 +8,16 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import copy import json import os import re from datetime import timedelta from math import floor, log -from urllib.parse import quote -import xbmc -import xbmcvfs +from ..compatibility import quote, xbmc, xbmcvfs __all__ = ( diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index e25e22bdc..ecf05bb61 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -7,16 +7,14 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import os import shutil import threading -from urllib.parse import unquote - -import xbmc -import xbmcvfs -from xbmcaddon import Addon +from ..compatibility import unquote, xbmc, xbmcaddon, xbmcvfs from ..logger import log_debug from ..network import get_http_server, is_httpd_live from ..settings import Settings @@ -24,7 +22,7 @@ class YouTubeMonitor(xbmc.Monitor): _addon_id = 'plugin.video.youtube' - _settings = Settings(Addon(_addon_id)) + _settings = Settings(xbmcaddon.Addon(_addon_id)) # noinspection PyUnusedLocal,PyMissingConstructor def __init__(self, *args, **kwargs): @@ -88,7 +86,7 @@ def onNotification(self, sender, method, data): .format(method=method)) def onSettingsChanged(self): - self._settings.flush(Addon(self._addon_id)) + self._settings.flush(xbmcaddon.Addon(self._addon_id)) data = { 'use_httpd': (self._settings.use_mpd_videos() diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index b2dcce605..830f299c6 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -7,11 +7,13 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import re import threading -import xbmc +from ..compatibility import xbmc class PlaybackMonitorThread(threading.Thread): diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py index 51e19a760..b758dacb5 100644 --- a/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -8,9 +8,11 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json -import xbmc +from ..compatibility import xbmc class SystemVersion(object): diff --git a/resources/lib/youtube_plugin/refresh.py b/resources/lib/youtube_plugin/refresh.py index 15789ab5f..981231c3a 100644 --- a/resources/lib/youtube_plugin/refresh.py +++ b/resources/lib/youtube_plugin/refresh.py @@ -7,7 +7,10 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import xbmc + if __name__ == '__main__': xbmc.executebuiltin("Container.Refresh") diff --git a/resources/lib/youtube_plugin/youtube/__init__.py b/resources/lib/youtube_plugin/youtube/__init__.py index 597a18f32..f1edbe0f9 100644 --- a/resources/lib/youtube_plugin/youtube/__init__.py +++ b/resources/lib/youtube_plugin/youtube/__init__.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .provider import Provider diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index 239475da3..e6279c3f1 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from base64 import b64decode from hashlib import md5 diff --git a/resources/lib/youtube_plugin/youtube/client/__init__.py b/resources/lib/youtube_plugin/youtube/client/__init__.py index a97c2a226..8a0c95b2d 100644 --- a/resources/lib/youtube_plugin/youtube/client/__init__.py +++ b/resources/lib/youtube_plugin/youtube/client/__init__.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .youtube import YouTube diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 70edd8319..cf37fcfac 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -8,8 +8,9 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import time -from urllib.parse import parse_qsl from requests.exceptions import InvalidJSONError @@ -20,6 +21,7 @@ youtube_tv, ) from .request_client import YouTubeRequestClient +from ...kodion.compatibility import parse_qsl from ...kodion.logger import log_debug from ...youtube.youtube_exceptions import ( InvalidGrant, diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 603bc3233..4bace16e9 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ...kodion.utils import merge_dicts from ...kodion.network import BaseRequestsClass from ...youtube.youtube_exceptions import YouTubeException diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 88294de7c..b6874c97b 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import copy import json import re @@ -17,8 +19,8 @@ from .login_client import LoginClient from ..helper.video_info import VideoInfo from ...kodion import Context -from ...kodion.utils import datetime_parser -from ...kodion.utils import to_unicode +from ...kodion.utils import datetime_parser, to_unicode + _context = Context(plugin_id='plugin.video.youtube') diff --git a/resources/lib/youtube_plugin/youtube/helper/__init__.py b/resources/lib/youtube_plugin/youtube/helper/__init__.py index 6e400683d..12b93382c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/__init__.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from .resource_manager import ResourceManager from .url_resolver import UrlResolver from .url_to_item_converter import UrlToItemConverter diff --git a/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py b/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py index 8afb79d19..2dcf59c7d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ....youtube.helper.ratebypass import ratebypass diff --git a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py index b3632a734..bd924f1a9 100644 --- a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py +++ b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py @@ -9,6 +9,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re try: diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 0ec5e7e3e..fab7c216e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ..youtube_exceptions import YouTubeException from ...kodion.utils import strip_html_from_text diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py index 886aaaa2e..4370d5ee9 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ....youtube.helper.signature.cipher import Cipher diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py index fa52385c6..b031daf16 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re from .json_script_engine import JsonScriptEngine diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 48019112c..70ed8a8bb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -6,11 +6,17 @@ See LICENSES/GPL-2.0-only for more information. """ -from html import unescape -from urllib.parse import parse_qs, urlsplit, urlunsplit, urlencode, urljoin - -import xbmcvfs - +from __future__ import absolute_import, division, unicode_literals + +from ...kodion.compatibility import ( + parse_qs, + unescape, + urlencode, + urljoin, + urlsplit, + urlunsplit, + xbmcvfs, +) from ...kodion.network import BaseRequestsClass from ...kodion.utils import make_dirs diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index c280beb54..0d5541ff7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ...kodion.items import DirectoryItem, NextPageItem, VideoItem from ...youtube.helper import utils diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index 0a28d1f93..08e8aceeb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -8,10 +8,11 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re -from html import unescape -from urllib.parse import parse_qsl, urlencode, urlparse +from ...kodion.compatibility import parse_qsl, unescape, urlencode, urlparse from ...kodion.network import BaseRequestsClass diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index dbe3f50be..c03177a64 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -8,10 +8,12 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re -from urllib.parse import parse_qsl, urlparse from . import utils +from ...kodion.compatibility import parse_qsl, urlparse from ...kodion.items import DirectoryItem, UriItem, VideoItem from ...kodion.utils import duration_to_seconds diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index c7a3d3acf..24e503383 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re import time from math import log10 diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 6fe14423b..1fa2c409b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ...youtube.helper import yt_context_menu from ... import kodion from ...kodion import items diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 12c9d50bb..80b91a6d6 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -8,29 +8,29 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import random import re import traceback - -from html import unescape from json import dumps as json_dumps, loads as json_loads -from urllib.parse import ( + +from .ratebypass import ratebypass +from .signature.cipher import Cipher +from .subtitles import Subtitles +from ..client.request_client import YouTubeRequestClient +from ..youtube_exceptions import YouTubeException +from ...kodion.compatibility import ( parse_qs, quote, + unescape, unquote, urlencode, urljoin, urlsplit, urlunsplit, + xbmcvfs, ) - -import xbmcvfs - -from .ratebypass import ratebypass -from .signature.cipher import Cipher -from .subtitles import Subtitles -from ..client.request_client import YouTubeRequestClient -from ..youtube_exceptions import YouTubeException from ...kodion.network import is_httpd_live from ...kodion.utils import make_dirs diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py b/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py index d35080dd6..3159ebc49 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ... import kodion diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index fc96c5662..81bcece3c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import copy import json import time diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py b/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py index a380e982c..0ed81368e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ... import kodion diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 07d22c2c4..88a327afb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import random import traceback diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 4d639a6d4..c0dfa676d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ... import kodion from ...youtube.helper import v3 diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 372097040..e6bdda954 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ...kodion.network import Locator diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 77f9bfe72..6b143dd51 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from . import utils from ...kodion import KodionException, constants from ...kodion.items import DirectoryItem, UriItem diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index fd01629bd..24da6925c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ...kodion.items import UriItem from ... import kodion from ...youtube.helper import v3 diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index ecc4a32d5..699263a9e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ... import kodion from ...youtube.helper import v3 diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 777fd3812..d4fbb8039 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import json import os import re @@ -15,9 +17,6 @@ import socket from base64 import b64decode -import xbmcaddon -import xbmcvfs - from .helper import ( ResourceManager, UrlResolver, @@ -34,6 +33,7 @@ ) from .youtube_exceptions import InvalidGrant, LoginException from ..kodion import (AbstractProvider, RegisterProviderPath, constants) +from ..kodion.compatibility import xbmcaddon, xbmcvfs from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem from ..kodion.network import get_client_ip_address, is_httpd_live from ..kodion.utils import find_video_id, strip_html_from_text diff --git a/resources/lib/youtube_plugin/youtube/youtube_exceptions.py b/resources/lib/youtube_plugin/youtube/youtube_exceptions.py index 1e7cda824..fe4e24f91 100644 --- a/resources/lib/youtube_plugin/youtube/youtube_exceptions.py +++ b/resources/lib/youtube_plugin/youtube/youtube_exceptions.py @@ -8,6 +8,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from ..kodion import KodionException diff --git a/resources/lib/youtube_registration.py b/resources/lib/youtube_registration.py index 429420ba0..548be86bb 100644 --- a/resources/lib/youtube_registration.py +++ b/resources/lib/youtube_registration.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + from base64 import b64encode from youtube_plugin.kodion.json_store import APIKeyStore from youtube_plugin.kodion.context import Context diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index 7f427348e..b25204c74 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re from youtube_plugin.youtube.provider import Provider diff --git a/resources/lib/youtube_resolver.py b/resources/lib/youtube_resolver.py index 69d5d411c..53c771a3e 100644 --- a/resources/lib/youtube_resolver.py +++ b/resources/lib/youtube_resolver.py @@ -7,6 +7,8 @@ See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + import re from youtube_plugin.youtube.provider import Provider From 64a942c8f4d55529f2bbce66108f6e27d1c21d1b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 23:48:13 +1100 Subject: [PATCH 085/141] Prevent possible memory leak and/or freeze on exit --- .../kodion/json_store/json_store.py | 4 +- .../kodion/network/http_server.py | 8 ++- .../kodion/settings/abstract_settings.py | 3 +- .../settings/xbmc/xbmc_plugin_settings.py | 58 +++++++++---------- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index b6dea6368..cf39c9e78 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -20,11 +20,13 @@ _addon_id = 'plugin.video.youtube' _addon = xbmcaddon.Addon(_addon_id) +_addon_data_path = _addon.getAddonInfo('profile') +del _addon class JSONStore(object): def __init__(self, filename): - self.base_path = xbmcvfs.translatePath(_addon.getAddonInfo('profile')) + self.base_path = xbmcvfs.translatePath(_addon_data_path) if not xbmcvfs.exists(self.base_path) and not make_dirs(self.base_path): log_error('JSONStore.__init__ |{path}| invalid path'.format( diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index ea06ddff7..cd48e76d9 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -34,6 +34,10 @@ _addon = xbmcaddon.Addon(_addon_id) _settings = Settings(_addon) _i18n = _addon.getLocalizedString +_addon_name = _addon.getAddonInfo('name') +_addon_icon = _addon.getAddonInfo('icon') +del _addon + _server_requests = BaseRequestsClass() @@ -532,9 +536,9 @@ def get_http_server(address=None, port=None): except socket_error as e: log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|' .format(address=address, port=port, response=str(e))) - xbmcgui.Dialog().notification(_addon.getAddonInfo('name'), + xbmcgui.Dialog().notification(_addon_name, str(e), - _addon.getAddonInfo('icon'), + _addon_icon, time=5000, sound=False) return None diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index a0009b0ba..527ab40ff 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -23,8 +23,7 @@ class AbstractSettings(object): _echo = False _cache = {} - _funcs = {} - _store = None + _type = None @classmethod def flush(cls, xbmc_addon): diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index d19342c02..577d72bb2 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -23,20 +23,18 @@ def __init__(self, xbmc_addon): self.flush(xbmc_addon) - if self._funcs: - return if current_system_version.compatible(20, 0): _class = xbmcaddon.Settings - self._funcs.update({ - 'get_bool': _class.getBool, - 'set_bool': _class.setBool, - 'get_int': _class.getInt, - 'set_int': _class.setInt, - 'get_str': _class.getString, - 'set_str': _class.setString, - 'get_str_list': _class.getStringList, - 'set_str_list': _class.setStringList, + self.__dict__.update({ + '_get_bool': _class.getBool, + '_set_bool': _class.setBool, + '_get_int': _class.getInt, + '_set_int': _class.setInt, + '_get_str': _class.getString, + '_set_str': _class.setString, + '_get_str_list': _class.getStringList, + '_set_str_list': _class.setStringList, }) else: _class = xbmcaddon.Addon @@ -48,15 +46,15 @@ def _set_string_list(store, setting, value): value = ','.join(value) return _class.setSetting(store, setting, value) - self._funcs.update({ - 'get_bool': _class.getSettingBool, - 'set_bool': _class.setSettingBool, - 'get_int': _class.getSettingInt, - 'set_int': _class.setSettingInt, - 'get_str': _class.getSettingString, - 'set_str': _class.setSettingString, - 'get_str_list': _get_string_list, - 'set_str_list': _set_string_list, + self.__dict__.update({ + '_get_bool': _class.getSettingBool, + '_set_bool': _class.setSettingBool, + '_get_int': _class.getSettingInt, + '_set_int': _class.setSettingInt, + '_get_str': _class.getSettingString, + '_set_str': _class.setSettingString, + '_get_str_list': _get_string_list, + '_set_str_list': _set_string_list, }) @classmethod @@ -64,9 +62,9 @@ def flush(cls, xbmc_addon): cls._echo = get_kodi_setting('debug.showloginfo') cls._cache = {} if current_system_version.compatible(20, 0): - cls._store = xbmc_addon.getSettings() + cls._type = xbmc_addon.getSettings else: - cls._store = xbmc_addon + cls._type = xbmcaddon.Addon def get_bool(self, setting, default=None, echo=None): if setting in self._cache: @@ -74,7 +72,7 @@ def get_bool(self, setting, default=None, echo=None): error = False try: - value = bool(self._funcs['get_bool'](self._store, setting)) + value = bool(self._get_bool(self._type(), setting)) except (AttributeError, TypeError) as ex: error = ex value = self.get_string(setting, echo=False) @@ -94,7 +92,7 @@ def get_bool(self, setting, default=None, echo=None): def set_bool(self, setting, value, echo=None): try: - error = not self._funcs['set_bool'](self._store, setting, value) + error = not self._set_bool(self._type(), setting, value) if not error: self._cache[setting] = value except RuntimeError as ex: @@ -114,7 +112,7 @@ def get_int(self, setting, default=-1, process=None, echo=None): error = False try: - value = int(self._funcs['get_int'](self._store, setting)) + value = int(self._get_int(self._type(), setting)) if process: value = process(value) except (AttributeError, TypeError, ValueError) as ex: @@ -140,7 +138,7 @@ def get_int(self, setting, default=-1, process=None, echo=None): def set_int(self, setting, value, echo=None): try: - error = not self._funcs['set_int'](self._store, setting, value) + error = not self._set_int(self._type(), setting, value) if not error: self._cache[setting] = value except RuntimeError as ex: @@ -160,7 +158,7 @@ def get_string(self, setting, default='', echo=None): error = False try: - value = self._funcs['get_str'](self._store, setting) or default + value = self._get_str(self._type(), setting) or default except RuntimeError as ex: error = ex value = default @@ -176,7 +174,7 @@ def get_string(self, setting, default='', echo=None): def set_string(self, setting, value, echo=None): try: - error = not self._funcs['set_str'](self._store, setting, value) + error = not self._set_str(self._type(), setting, value) if not error: self._cache[setting] = value except RuntimeError as ex: @@ -196,7 +194,7 @@ def get_string_list(self, setting, default=None, echo=None): error = False try: - value = self._funcs['get_str_list'](self._store, setting) + value = self._get_str_list(self._type(), setting) if not value: value = [] if default is None else default except RuntimeError as ex: @@ -214,7 +212,7 @@ def get_string_list(self, setting, default=None, echo=None): def set_string_list(self, setting, value, echo=None): try: - error = not self._funcs['set_str_list'](self._store, setting, value) + error = not self._set_str_list(self._type(), setting, value) if not error: self._cache[setting] = value except RuntimeError as ex: From f9113905c81a1dca0a4a1916fcbdf69b62cfb696 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 23:49:01 +1100 Subject: [PATCH 086/141] Fix incorrect string id --- resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index e6bdda954..39f0695f9 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -108,7 +108,7 @@ def _process_language(provider, context): def _process_geo_location(context): - if not context.get_ui().on_yes_no_input(context.get_name(), context.localize('perform.geolocation')): + if not context.get_ui().on_yes_no_input(context.get_name(), context.localize('perform_geolocation')): return locator = Locator() From b7ae94c07b1785a9a81137fc5a12466f42899474 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 23:52:30 +1100 Subject: [PATCH 087/141] Remove need for urlparse and urlunsplit usage - Replace urlparse usage with urlsplit instead - Replace urlunsplit with _replace().geturl() --- .../youtube_plugin/kodion/compatibility/__init__.py | 6 ------ .../kodion/context/xbmc/xbmc_context.py | 4 ++-- .../youtube_plugin/kodion/network/http_server.py | 4 ++-- .../lib/youtube_plugin/youtube/helper/subtitles.py | 13 +++++-------- .../youtube_plugin/youtube/helper/url_resolver.py | 10 +++++----- .../youtube/helper/url_to_item_converter.py | 4 ++-- .../lib/youtube_plugin/youtube/helper/video_info.py | 7 +------ 7 files changed, 17 insertions(+), 31 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py index 904da9afc..b8cc23a3f 100644 --- a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py +++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -17,9 +17,7 @@ unquote, urlencode, urljoin, - urlparse, urlsplit, - urlunsplit, ) import xbmc @@ -40,9 +38,7 @@ parse_qs, parse_qsl, urljoin, - urlparse, urlsplit, - urlunsplit, ) from xml.sax.saxutils import unescape @@ -105,9 +101,7 @@ def _file_closer(*args, **kwargs): 'unquote', 'urlencode', 'urljoin', - 'urlparse', 'urlsplit', - 'urlunsplit', 'xbmc', 'xbmcaddon', 'xbmcgui', diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 65e652b4f..6ba3d588d 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -20,7 +20,7 @@ parse_qsl, quote, unquote, - urlparse, + urlsplit, xbmc, xbmcaddon, xbmcplugin, @@ -264,7 +264,7 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override # first the path of the uri if override: self._uri = sys.argv[0] - parsed_url = urlparse(self._uri) + parsed_url = urlsplit(self._uri) self._path = unquote(parsed_url.path) # after that try to get the params diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index cd48e76d9..48836c372 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -20,7 +20,7 @@ from ..compatibility import ( BaseHTTPServer, parse_qs, - urlparse, + urlsplit, xbmc, xbmcaddon, xbmcgui, @@ -134,7 +134,7 @@ def do_GET(self): elif api_config_enabled and stripped_path.startswith('/api_submit'): xbmc.executebuiltin('Dialog.Close(addonsettings, true)') - query = urlparse(self.path).query + query = urlsplit(self.path).query params = parse_qs(query) updated = [] diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 70ed8a8bb..485d538e5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -14,7 +14,6 @@ urlencode, urljoin, urlsplit, - urlunsplit, xbmcvfs, ) from ...kodion.network import BaseRequestsClass @@ -306,18 +305,16 @@ def _set_query_param(url, *pairs): else: return url - scheme, netloc, path, query_string, fragment = urlsplit(url) - query_params = parse_qs(query_string) + components = urlsplit(url) + query_params = parse_qs(components.query) for name, value in pairs: if name: query_params[name] = [value] - new_query_string = urlencode(query_params, doseq=True) - if isinstance(scheme, bytes): - new_query_string = new_query_string.encode('utf-8') - - return urlunsplit((scheme, netloc, path, new_query_string, fragment)) + return components._replace( + query=urlencode(query_params, doseq=True) + ).geturl() @staticmethod def _normalize_url(url): diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index 08e8aceeb..a6d94cfa8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -12,7 +12,7 @@ import re -from ...kodion.compatibility import parse_qsl, unescape, urlencode, urlparse +from ...kodion.compatibility import parse_qsl, unescape, urlencode, urlsplit from ...kodion.network import BaseRequestsClass @@ -111,7 +111,7 @@ def resolve(self, url, url_components, method='HEAD'): # top-level query string params = dict(parse_qsl(url_components.query)) # components of next_url - next_components = urlparse(params.pop('next_url', '')) + next_components = urlsplit(params.pop('next_url', '')) if not next_components.scheme or not next_components.netloc: return url # query string encoded inside next_url @@ -141,7 +141,7 @@ def resolve(self, url, url_components, method='HEAD'): url = matches['video_url'] if url: num_matched += 1 - url_components = urlparse(unescape(url)) + url_components = urlsplit(unescape(url)) params = dict(parse_qsl(url_components.query)) if not num_matched & 2: @@ -174,7 +174,7 @@ def resolve(self, url, url_components, method='HEAD'): if match: url = match.group('channel_url') if path.endswith(('/live', '/streams')): - url_components = urlparse(unescape(url)) + url_components = urlsplit(unescape(url)) params = dict(parse_qsl(url_components.query)) params['live'] = 1 return url_components._replace( @@ -223,7 +223,7 @@ def _resolve(self, url): resolved_url = url for resolver_name in self._resolvers: resolver = self._resolver_map[resolver_name] - url_components = urlparse(resolved_url) + url_components = urlsplit(resolved_url) method = resolver.supports_url(resolved_url, url_components) if not method: continue diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index c03177a64..c81aef0eb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -13,7 +13,7 @@ import re from . import utils -from ...kodion.compatibility import parse_qsl, urlparse +from ...kodion.compatibility import parse_qsl, urlsplit from ...kodion.items import DirectoryItem, UriItem, VideoItem from ...kodion.utils import duration_to_seconds @@ -41,7 +41,7 @@ def __init__(self, flatten=True): self._channel_ids = [] def add_url(self, url, provider, context): - parsed_url = urlparse(url) + parsed_url = urlsplit(url) if parsed_url.hostname.lower() not in self.VALID_HOSTNAMES: context.log_debug('Unknown hostname "{0}" in url "{1}"'.format( parsed_url.hostname, url diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 80b91a6d6..2326c5f1b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -28,7 +28,6 @@ urlencode, urljoin, urlsplit, - urlunsplit, xbmcvfs, ) from ...kodion.network import is_httpd_live @@ -980,11 +979,7 @@ def _process_url_params(self, url): elif not update_url: return url - return urlunsplit((parts.scheme, - parts.netloc, - parts.path, - urlencode(query, doseq=True), - parts.fragment)) + return parts._replace(query=urlencode(query, doseq=True)).geturl() def _get_error_details(self, playability_status, details=None): if not playability_status: From 54bdc195cf46284905c62b4ec7e36262a17264b6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 11 Dec 2023 23:54:50 +1100 Subject: [PATCH 088/141] Import tidy up --- .../kodion/context/abstract_context.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 38 +++++++++++-------- .../lib/youtube_plugin/youtube/helper/v3.py | 4 +- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 765cc61e4..576384437 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -23,7 +23,7 @@ SearchHistory, WatchLaterList, ) -from ..utils import (create_path, create_uri_path, current_system_version) +from ..utils import create_path, create_uri_path, current_system_version class AbstractContext(object): diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 24e503383..c3104520a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -14,10 +14,16 @@ import time from math import log10 -from ...kodion import utils from ...kodion.items import DirectoryItem +from ...kodion.utils import ( + create_path, + datetime_parser, + friendly_number, + strip_html_from_text, +) from ...youtube.helper import yt_context_menu + try: from inputstreamhelper import Helper as ISHelper except ImportError: @@ -71,7 +77,7 @@ def make_comment_item(context, snippet, uri, total_replies=0): like_count = snippet['likeCount'] if like_count: - like_count, _ = utils.friendly_number(like_count) + like_count, _ = friendly_number(like_count) color = __COLOR_MAP['likeCount'] label_likes = ui.color(color, ui.bold(like_count)) plot_likes = ui.color(color, ui.bold(' '.join(( @@ -81,7 +87,7 @@ def make_comment_item(context, snippet, uri, total_replies=0): plot_props.append(plot_likes) if total_replies: - total_replies, _ = utils.friendly_number(total_replies) + total_replies, _ = friendly_number(total_replies) color = __COLOR_MAP['commentCount'] label_replies = ui.color(color, ui.bold(total_replies)) plot_replies = ui.color(color, ui.bold(' '.join(( @@ -124,13 +130,13 @@ def make_comment_item(context, snippet, uri, total_replies=0): comment_item = DirectoryItem(label, uri) comment_item.set_plot(plot) - datetime = utils.datetime_parser.parse(published_at, as_utc=True) + datetime = datetime_parser.parse(published_at, as_utc=True) comment_item.set_added_utc(datetime) - local_datetime = utils.datetime_parser.utc_to_local(datetime) + local_datetime = datetime_parser.utc_to_local(datetime) comment_item.set_dateadded_from_datetime(local_datetime) if edited: - datetime = utils.datetime_parser.parse(updated_at, as_utc=True) - local_datetime = utils.datetime_parser.utc_to_local(datetime) + datetime = datetime_parser.parse(updated_at, as_utc=True) + local_datetime = datetime_parser.utc_to_local(datetime) comment_item.set_date_from_datetime(local_datetime) if not uri: @@ -375,7 +381,7 @@ def update_video_infos(provider, context, video_id_dict, else: duration = yt_item.get('contentDetails', {}).get('duration') if duration: - duration = utils.datetime_parser.parse(duration) + duration = datetime_parser.parse(duration) # subtract 1s because YouTube duration is +1s too long duration = (duration.seconds - 1) if duration.seconds else None if duration: @@ -402,9 +408,9 @@ def update_video_infos(provider, context, video_id_dict, else: start_at = None if start_at: - datetime = utils.datetime_parser.parse(start_at, as_utc=True) + datetime = datetime_parser.parse(start_at, as_utc=True) video_item.set_scheduled_start_utc(datetime) - local_datetime = utils.datetime_parser.utc_to_local(datetime) + local_datetime = datetime_parser.utc_to_local(datetime) video_item.set_year_from_datetime(local_datetime) video_item.set_aired_from_datetime(local_datetime) video_item.set_premiered_from_datetime(local_datetime) @@ -413,7 +419,7 @@ def update_video_infos(provider, context, video_id_dict, else 'upcoming') start_at = '{type_label} {start_at}'.format( type_label=type_label, - start_at=utils.datetime_parser.get_scheduled_start( + start_at=datetime_parser.get_scheduled_start( context, local_datetime ) ) @@ -427,7 +433,7 @@ def update_video_infos(provider, context, video_id_dict, if not label: continue - str_value, value = utils.friendly_number(value) + str_value, value = friendly_number(value) if not value: continue @@ -489,7 +495,7 @@ def update_video_infos(provider, context, video_id_dict, # plot channel_name = snippet.get('channelTitle', '') - description = utils.strip_html_from_text(snippet['description']) + description = strip_html_from_text(snippet['description']) if show_details: description = ''.join(( ui.bold(channel_name, cr_after=2) if channel_name else '', @@ -507,9 +513,9 @@ def update_video_infos(provider, context, video_id_dict, # date time published_at = snippet.get('publishedAt') if published_at: - datetime = utils.datetime_parser.parse(published_at, as_utc=True) + datetime = datetime_parser.parse(published_at, as_utc=True) video_item.set_added_utc(datetime) - local_datetime = utils.datetime_parser.utc_to_local(datetime) + local_datetime = datetime_parser.utc_to_local(datetime) video_item.set_dateadded_from_datetime(local_datetime) if not start_at: video_item.set_year_from_datetime(local_datetime) @@ -601,7 +607,7 @@ def update_video_infos(provider, context, video_id_dict, # got to [CHANNEL], only if we are not directly in the channel provide a jump to the channel if (channel_id and channel_name and - utils.create_path('channel', channel_id) != path): + create_path('channel', channel_id) != path): video_item.set_channel_id(channel_id) yt_context_menu.append_go_to_channel( context_menu, context, channel_id, channel_name diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 1fa2c409b..04f8ae424 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -10,10 +10,10 @@ from __future__ import absolute_import, division, unicode_literals -from ...youtube.helper import yt_context_menu +from . import utils from ... import kodion from ...kodion import items -from . import utils +from ...youtube.helper import yt_context_menu def _process_list_response(provider, context, json_data): From b6407f726836b23712b036b7cf81fbbfd7392feb Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 12 Dec 2023 12:52:04 +1100 Subject: [PATCH 089/141] Update filter_short_videos - Caller must check whether Settings.hide_short_videos() is True - update_video_infos will short circuit if short videos are hidden --- .../lib/youtube_plugin/youtube/helper/tv.py | 6 ++++-- .../lib/youtube_plugin/youtube/helper/utils.py | 17 +++++++++-------- .../lib/youtube_plugin/youtube/helper/v3.py | 3 ++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index 0d5541ff7..56a0ca9ea 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -53,7 +53,8 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): utils.update_video_infos(provider, context, video_id_dict, channel_items_dict=channel_item_dict, use_play_data=use_play_data) utils.update_fanarts(provider, context, channel_item_dict) - result = utils.filter_short_videos(context, result) + if context.get_settings().hide_short_videos(): + result = utils.filter_short_videos(result) # next page next_page_token = json_data.get('next_page_token', '') @@ -99,7 +100,8 @@ def tv_videos_to_items(provider, context, json_data): utils.update_video_infos(provider, context, video_id_dict, channel_items_dict=channel_item_dict, use_play_data=use_play_data) utils.update_fanarts(provider, context, channel_item_dict) - result = utils.filter_short_videos(context, result) + if context.get_settings().hide_short_videos(): + result = utils.filter_short_videos(result) # next page next_page_token = json_data.get('next_page_token', '') diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index c3104520a..5239475c1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -353,6 +353,7 @@ def update_video_infos(provider, context, video_id_dict, alternate_player = settings.is_support_alternative_player_enabled() logged_in = provider.is_logged_in() path = context.get_path() + hide_shorts = settings.hide_short_videos() show_details = settings.show_detailed_description() thumb_size = settings.use_thumbnail_size() thumb_stamp = get_thumb_timestamp() @@ -386,6 +387,8 @@ def update_video_infos(provider, context, video_id_dict, duration = (duration.seconds - 1) if duration.seconds else None if duration: video_item.set_duration_from_seconds(duration) + if hide_shorts and duration <= 60: + continue if not video_item.live and play_data: if 'play_count' in play_data: @@ -808,11 +811,9 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id): break -def filter_short_videos(context, items): - if context.get_settings().hide_short_videos(): - items = [ - item - for item in items - if item.playable and not 0 <= item.get_duration() <= 60 - ] - return items +def filter_short_videos(items): + return [ + item + for item in items + if item.playable and not 0 <= item.get_duration() <= 60 + ] diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 04f8ae424..4420b5560 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -297,7 +297,8 @@ def response_to_items(provider, context, json_data, sort=None, reverse_sort=Fals if sort is not None: result = sorted(result, key=sort, reverse=reverse_sort) - result = utils.filter_short_videos(context, result) + if context.get_settings().hide_short_videos(): + result = utils.filter_short_videos(result) # no processing of next page item if not process_next_page: From a3af85f01e8fc515158a3fba2882d65d5fbf5908 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:07:11 +1100 Subject: [PATCH 090/141] Improve default sorting of live videos - aired infolabel does not include time, date does - sort by date/time ascending --- resources/lib/youtube_plugin/youtube/helper/v3.py | 2 +- resources/lib/youtube_plugin/youtube/helper/yt_specials.py | 4 ++-- resources/lib/youtube_plugin/youtube/provider.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 4420b5560..ca82bb24b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -278,7 +278,7 @@ def _process_list_response(provider, context, json_data): return result -def response_to_items(provider, context, json_data, sort=None, reverse_sort=False, process_next_page=True): +def response_to_items(provider, context, json_data, sort=None, reverse=False, process_next_page=True): result = [] is_youtube, kind = _parse_kind(json_data) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 6b143dd51..de8873dc4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -131,7 +131,7 @@ def _process_disliked_videos(provider, context): def _process_live_events(provider, context, event_type='live'): def _sort(x): - return x.get_aired() + return x.get_date() provider.set_content_type(context, constants.content_type.VIDEOS) result = [] @@ -143,7 +143,7 @@ def _sort(x): json_data = provider.get_client(context).get_live_events(event_type=event_type, page_token=page_token, location=location) if not v3.handle_error(context, json_data): return False - result.extend(v3.response_to_items(provider, context, json_data, sort=_sort, reverse_sort=True)) + result.extend(v3.response_to_items(provider, context, json_data, sort=_sort)) return result diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index d4fbb8039..8c70cb240 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -494,7 +494,7 @@ def _on_channel(self, context, re_match): return False result.extend( - v3.response_to_items(self, context, json_data, sort=lambda x: x.get_aired(), reverse_sort=True)) + v3.response_to_items(self, context, json_data, sort=lambda x: x.get_date())) return result From 0889e7bd3a3e5a3d8b8d0b8883ceef7ad06efede Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:14:17 +1100 Subject: [PATCH 091/141] Minor speed improvements to creation of listings - TODO: Proper thread management - Reduce requests backoff factor --- .../youtube_plugin/kodion/network/requests.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 18 +- .../lib/youtube_plugin/youtube/helper/v3.py | 199 +++++++--- .../youtube/helper/yt_specials.py | 340 ++++++++---------- 4 files changed, 317 insertions(+), 242 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 19e8b570c..c41473b6a 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -30,7 +30,7 @@ class BaseRequestsClass(object): pool_block=True, max_retries=Retry( total=3, - backoff_factor=1, + backoff_factor=0.1, status_forcelist={500, 502, 503, 504}, allowed_methods=None, ) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 5239475c1..2262b6f9c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -349,16 +349,21 @@ def update_video_infos(provider, context, video_id_dict, if not playlist_item_id_dict: playlist_item_id_dict = {} - settings = context.get_settings() - alternate_player = settings.is_support_alternative_player_enabled() logged_in = provider.is_logged_in() - path = context.get_path() + if logged_in: + wl_playlist_id = context.get_access_manager().get_watch_later_id() + else: + wl_playlist_id = None + + settings = context.get_settings() hide_shorts = settings.hide_short_videos() + alternate_player = settings.is_support_alternative_player_enabled() show_details = settings.show_detailed_description() thumb_size = settings.use_thumbnail_size() thumb_stamp = get_thumb_timestamp() + + path = context.get_path() ui = context.get_ui() - watch_later_playlist_id = context.get_access_manager().get_watch_later_id() for video_id, yt_item in data.items(): video_item = video_id_dict[video_id] @@ -579,10 +584,9 @@ def update_video_infos(provider, context, video_id_dict, if logged_in: # add 'Watch Later' only if we are not in my 'Watch Later' list - if (watch_later_playlist_id - and watch_later_playlist_id != playlist_id): + if wl_playlist_id and wl_playlist_id != playlist_id: yt_context_menu.append_watch_later( - context_menu, context, watch_later_playlist_id, video_id + context_menu, context, wl_playlist_id, video_id ) # provide 'remove' for videos in my playlists diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index ca82bb24b..713435b16 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -10,13 +10,29 @@ from __future__ import absolute_import, division, unicode_literals -from . import utils -from ... import kodion -from ...kodion import items +from threading import Thread + +from .utils import ( + filter_short_videos, + get_thumbnail, + make_comment_item, + update_channel_infos, + update_fanarts, + update_playlist_infos, + update_video_infos, +) +from ...kodion import KodionException +from ...kodion.items import DirectoryItem, NextPageItem, VideoItem +from ...kodion.utils import strip_html_from_text from ...youtube.helper import yt_context_menu def _process_list_response(provider, context, json_data): + yt_items = json_data.get('items', []) + if not yt_items: + context.log_warning('List of search result is empty') + return [] + video_id_dict = {} channel_id_dict = {} playlist_id_dict = {} @@ -25,15 +41,13 @@ def _process_list_response(provider, context, json_data): result = [] - thumb_size = context.get_settings().use_thumbnail_size() - yt_items = json_data.get('items', []) - if not yt_items: - context.log_warning('List of search result is empty') - return result - incognito = context.get_param('incognito', False) addon_id = context.get_param('addon_id', '') + settings = context.get_settings() + thumb_size = settings.use_thumbnail_size() + use_play_data = not incognito and settings.use_local_history() + for yt_item in yt_items: is_youtube, kind = _parse_kind(yt_item) @@ -45,14 +59,14 @@ def _process_list_response(provider, context, json_data): video_id = yt_item['id'] snippet = yt_item['snippet'] title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: item_params['incognito'] = incognito if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) - video_item = items.VideoItem(title, item_uri, image=image) + video_item = VideoItem(title, item_uri, image=image) video_item.video_id = video_id if incognito: video_item.set_play_count(0) @@ -63,14 +77,14 @@ def _process_list_response(provider, context, json_data): channel_id = yt_item['id'] snippet = yt_item['snippet'] title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {} if incognito: item_params['incognito'] = incognito if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) - channel_item = items.DirectoryItem(title, item_uri, image=image) + channel_item = DirectoryItem(title, item_uri, image=image) channel_item.set_fanart(provider.get_fanart(context)) # if logged in => provide subscribing to the channel @@ -90,13 +104,13 @@ def _process_list_response(provider, context, json_data): if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['special', 'browse_channels'], item_params) - guide_item = items.DirectoryItem(title, item_uri) + guide_item = DirectoryItem(title, item_uri) guide_item.set_fanart(provider.get_fanart(context)) result.append(guide_item) elif kind == 'subscription': snippet = yt_item['snippet'] title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) channel_id = snippet['resourceId']['channelId'] item_params = {} if incognito: @@ -104,7 +118,7 @@ def _process_list_response(provider, context, json_data): if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) - channel_item = items.DirectoryItem(title, item_uri, image=image) + channel_item = DirectoryItem(title, item_uri, image=image) channel_item.set_fanart(provider.get_fanart(context)) channel_item.set_channel_id(channel_id) # map channel id with subscription id - we need it for the unsubscription @@ -116,7 +130,7 @@ def _process_list_response(provider, context, json_data): playlist_id = yt_item['id'] snippet = yt_item['snippet'] title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) channel_id = snippet['channelId'] @@ -129,7 +143,7 @@ def _process_list_response(provider, context, json_data): if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) - playlist_item = items.DirectoryItem(title, item_uri, image=image) + playlist_item = DirectoryItem(title, item_uri, image=image) playlist_item.set_fanart(provider.get_fanart(context)) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_item @@ -141,14 +155,14 @@ def _process_list_response(provider, context, json_data): playlist_item_id_dict[video_id] = yt_item['id'] title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: item_params['incognito'] = incognito if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) - video_item = items.VideoItem(title, item_uri, image=image) + video_item = VideoItem(title, item_uri, image=image) video_item.video_id = video_id if incognito: video_item.set_play_count(0) @@ -172,14 +186,14 @@ def _process_list_response(provider, context, json_data): continue title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: item_params['incognito'] = incognito if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) - video_item = items.VideoItem(title, item_uri, image=image) + video_item = VideoItem(title, item_uri, image=image) video_item.video_id = video_id if incognito: video_item.set_play_count(0) @@ -196,10 +210,10 @@ def _process_list_response(provider, context, json_data): item_uri = context.create_uri(['special', 'child_comments'], item_params) else: item_uri = '' - result.append(utils.make_comment_item(context, snippet, item_uri, total_replies)) + result.append(make_comment_item(context, snippet, item_uri, total_replies)) elif kind == 'comment': - result.append(utils.make_comment_item(context, yt_item['snippet'], uri='')) + result.append(make_comment_item(context, yt_item['snippet'], uri='')) elif kind == 'searchresult': _, kind = _parse_kind(yt_item.get('id', {})) @@ -209,14 +223,14 @@ def _process_list_response(provider, context, json_data): video_id = yt_item['id']['videoId'] snippet = yt_item.get('snippet', {}) title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {'video_id': video_id} if incognito: item_params['incognito'] = incognito if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['play'], item_params) - video_item = items.VideoItem(title, item_uri, image=image) + video_item = VideoItem(title, item_uri, image=image) video_item.video_id = video_id if incognito: video_item.set_play_count(0) @@ -228,7 +242,7 @@ def _process_list_response(provider, context, json_data): playlist_id = yt_item['id']['playlistId'] snippet = yt_item['snippet'] title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) channel_id = snippet['channelId'] # if the path directs to a playlist of our own, we correct the channel id to 'mine' @@ -241,7 +255,7 @@ def _process_list_response(provider, context, json_data): if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) - playlist_item = items.DirectoryItem(title, item_uri, image=image) + playlist_item = DirectoryItem(title, item_uri, image=image) playlist_item.set_fanart(provider.get_fanart(context)) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_item @@ -249,56 +263,141 @@ def _process_list_response(provider, context, json_data): channel_id = yt_item['id']['channelId'] snippet = yt_item['snippet'] title = snippet.get('title', context.localize('untitled')) - image = utils.get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) item_params = {} if incognito: item_params['incognito'] = incognito if addon_id: item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) - channel_item = items.DirectoryItem(title, item_uri, image=image) + channel_item = DirectoryItem(title, item_uri, image=image) channel_item.set_fanart(provider.get_fanart(context)) result.append(channel_item) channel_id_dict[channel_id] = channel_item else: - raise kodion.KodionException("Unknown kind '%s'" % kind) + raise KodionException("Unknown kind '%s'" % kind) else: - raise kodion.KodionException("Unknown kind '%s'" % kind) - - use_play_data = not incognito and context.get_settings().use_local_history() + raise KodionException("Unknown kind '%s'" % kind) # this will also update the channel_id_dict with the correct channel id for each video. channel_items_dict = {} - utils.update_video_infos(provider, context, video_id_dict, playlist_item_id_dict, channel_items_dict, - live_details=True, use_play_data=use_play_data) - utils.update_playlist_infos(provider, context, playlist_id_dict, channel_items_dict) - utils.update_channel_infos(provider, context, channel_id_dict, subscription_id_dict, channel_items_dict) - if video_id_dict or playlist_id_dict: - utils.update_fanarts(provider, context, channel_items_dict) + + running = 0 + resource_manager = provider.get_resource_manager(context) + resources = [ + { + 'fetcher': resource_manager.get_videos, + 'args': (video_id_dict.keys(), ), + 'kwargs': {'live_details': True, 'suppress_errors': True}, + 'thread': None, + 'updater': update_video_infos, + 'upd_args': (provider, context, video_id_dict, playlist_item_id_dict, channel_items_dict), + 'upd_kwargs': {'data': None, 'live_details': True, 'use_play_data': use_play_data}, + 'complete': False, + 'defer': False, + }, + { + 'fetcher': resource_manager.get_playlists, + 'args': (playlist_id_dict.keys(), ), + 'kwargs': {}, + 'thread': None, + 'updater': update_playlist_infos, + 'upd_args': (provider, context, playlist_id_dict, channel_items_dict), + 'upd_kwargs': {'data': None}, + 'complete': False, + 'defer': False, + }, + { + 'fetcher': resource_manager.get_channels, + 'args': (channel_id_dict.keys(), ), + 'kwargs': {}, + 'thread': None, + 'updater': update_channel_infos, + 'upd_args': (provider, context, channel_id_dict, subscription_id_dict, channel_items_dict), + 'upd_kwargs': {'data': None}, + 'complete': False, + 'defer': False, + }, + { + 'fetcher': resource_manager.get_fanarts, + 'args': (channel_items_dict.keys(), ), + 'kwargs': {}, + 'thread': None, + 'updater': update_fanarts, + 'upd_args': (provider, context, channel_items_dict), + 'upd_kwargs': {'data': None}, + 'complete': False, + 'defer': True, + }, + ] + + def _fetch(resource): + data = resource['fetcher']( + *resource['args'], **resource['kwargs'] + ) + if not data: + return + resource['upd_kwargs']['data'] = data + resource['updater'](*resource['upd_args'], **resource['upd_kwargs']) + + for resource in resources: + if not resource['args'][0]: + resource['complete'] = True + continue + + running += 1 + if not resource['defer']: + # _fetch(resource) + thread = Thread(target=_fetch, args=(resource, )) + thread.daemon = True + thread.start() + resource['thread'] = thread + + while running > 0: + for resource in resources: + if resource['complete']: + continue + + thread = resource['thread'] + if thread: + thread.join(30) + if not thread.is_alive(): + resource['thread'] = None + resource['complete'] = True + running -= 1 + elif resource['defer']: + resource['defer'] = False + # _fetch(resource) + thread = Thread(target=_fetch, args=(resource, )) + thread.daemon = True + thread.start() + resource['thread'] = thread + else: + running -= 1 + resource['complete'] = True + return result def response_to_items(provider, context, json_data, sort=None, reverse=False, process_next_page=True): - result = [] - is_youtube, kind = _parse_kind(json_data) if not is_youtube: context.log_debug('v3 response: Response discarded, is_youtube=False') - return result + return [] if kind in ['searchlistresponse', 'playlistitemlistresponse', 'playlistlistresponse', 'subscriptionlistresponse', 'guidecategorylistresponse', 'channellistresponse', 'videolistresponse', 'activitylistresponse', 'commentthreadlistresponse', 'commentlistresponse']: - result.extend(_process_list_response(provider, context, json_data)) + result = _process_list_response(provider, context, json_data) else: - raise kodion.KodionException("Unknown kind '%s'" % kind) + raise KodionException("Unknown kind '%s'" % kind) if sort is not None: - result = sorted(result, key=sort, reverse=reverse_sort) + result.sort(key=sort, reverse=reverse) if context.get_settings().hide_short_videos(): - result = utils.filter_short_videos(result) + result = filter_short_videos(result) # no processing of next page item if not process_next_page: @@ -326,7 +425,7 @@ def response_to_items(provider, context, json_data, sort=None, reverse=False, pr new_context = context.clone(new_params=new_params) current_page = new_context.get_param('page', 1) - next_page_item = items.NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) result.append(next_page_item) return result @@ -337,8 +436,8 @@ def handle_error(context, json_data): ok_dialog = False message_timeout = 5000 - message = kodion.utils.strip_html_from_text(json_data['error'].get('message', '')) - log_message = kodion.utils.strip_html_from_text(json_data['error'].get('message', '')) + message = strip_html_from_text(json_data['error'].get('message', '')) + log_message = strip_html_from_text(json_data['error'].get('message', '')) reason = json_data['error']['errors'][0].get('reason', '') title = '%s: %s' % (context.get_name(), reason) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index de8873dc4..f7744173d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -11,7 +11,8 @@ from __future__ import absolute_import, division, unicode_literals from . import utils -from ...kodion import KodionException, constants +from ...kodion import KodionException +from ...kodion.constants import content_type from ...kodion.items import DirectoryItem, UriItem from ...kodion.utils import strip_html_from_text from ...youtube.helper import ( @@ -24,148 +25,131 @@ def _process_related_videos(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - result = [] - - page_token = context.get_param('page_token', '') + provider.set_content_type(context, content_type.VIDEOS) video_id = context.get_param('video_id', '') - if video_id: - json_data = provider.get_client(context).get_related_videos(video_id=video_id, page_token=page_token) - if not v3.handle_error(context, json_data): - return False - result.extend(v3.response_to_items(provider, context, json_data, process_next_page=False)) + if not video_id: + return [] - return result + json_data = provider.get_client(context).get_related_videos( + video_id=video_id, page_token=context.get_param('page_token', '') + ) + if not v3.handle_error(context, json_data): + return False + return v3.response_to_items(provider, + context, + json_data, + process_next_page=False) def _process_parent_comments(provider, context): - provider.set_content_type(context, constants.content_type.FILES) - result = [] - - page_token = context.get_param('page_token', '') + provider.set_content_type(context, content_type.FILES) video_id = context.get_param('video_id', '') - if video_id: - json_data = provider.get_client(context).get_parent_comments(video_id=video_id, page_token=page_token) - if not v3.handle_error(context, json_data): - return False - result.extend(v3.response_to_items(provider, context, json_data)) + if not video_id: + return [] - return result + json_data = provider.get_client(context).get_parent_comments( + video_id=video_id, page_token=context.get_param('page_token', '') + ) + if not v3.handle_error(context, json_data): + return False + return v3.response_to_items(provider, context, json_data) def _process_child_comments(provider, context): - provider.set_content_type(context, constants.content_type.FILES) - result = [] - - page_token = context.get_param('page_token', '') + provider.set_content_type(context, content_type.FILES) parent_id = context.get_param('parent_id', '') - if parent_id: - json_data = provider.get_client(context).get_child_comments(parent_id=parent_id, page_token=page_token) - if not v3.handle_error(context, json_data): - return False - result.extend(v3.response_to_items(provider, context, json_data)) + if not parent_id: + return [] - return result + json_data = provider.get_client(context).get_child_comments( + parent_id=parent_id, page_token=context.get_param('page_token', '') + ) + if not v3.handle_error(context, json_data): + return False + return v3.response_to_items(provider, context, json_data) def _process_recommendations(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - result = [] - - page_token = context.get_param('page_token', '') - json_data = provider.get_client(context).get_activities('home', page_token=page_token) + provider.set_content_type(context, content_type.VIDEOS) + json_data = provider.get_client(context).get_activities( + channel_id='home', page_token=context.get_param('page_token', '') + ) if not v3.handle_error(context, json_data): return False - result.extend(v3.response_to_items(provider, context, json_data)) - return result + return v3.response_to_items(provider, context, json_data) def _process_popular_right_now(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - result = [] - - page_token = context.get_param('page_token', '') - json_data = provider.get_client(context).get_popular_videos(page_token=page_token) + provider.set_content_type(context, content_type.VIDEOS) + json_data = provider.get_client(context).get_popular_videos( + page_token=context.get_param('page_token', '') + ) if not v3.handle_error(context, json_data): return False - result.extend(v3.response_to_items(provider, context, json_data)) - - return result + return v3.response_to_items(provider, context, json_data) def _process_browse_channels(provider, context): - provider.set_content_type(context, constants.content_type.FILES) - result = [] - - # page_token = context.get_param('page_token', '') - guide_id = context.get_param('guide_id', '') + provider.set_content_type(context, content_type.FILES) client = provider.get_client(context) - + guide_id = context.get_param('guide_id', '') if guide_id: json_data = client.get_guide_category(guide_id) if not v3.handle_error(context, json_data): return False - result.extend(v3.response_to_items(provider, context, json_data)) - else: - function_cache = context.get_function_cache() - json_data = function_cache.get(client.get_guide_categories, - function_cache.ONE_MONTH) - if not v3.handle_error(context, json_data): - return False - result.extend(v3.response_to_items(provider, context, json_data)) + return v3.response_to_items(provider, context, json_data) - return result + function_cache = context.get_function_cache() + json_data = function_cache.get(client.get_guide_categories, + function_cache.ONE_MONTH) + if not v3.handle_error(context, json_data): + return False + return v3.response_to_items(provider, context, json_data) def _process_disliked_videos(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - result = [] - - page_token = context.get_param('page_token', '') - json_data = provider.get_client(context).get_disliked_videos(page_token=page_token) + provider.set_content_type(context, content_type.VIDEOS) + json_data = provider.get_client(context).get_disliked_videos( + page_token=context.get_param('page_token', '') + ) if not v3.handle_error(context, json_data): return False - result.extend(v3.response_to_items(provider, context, json_data)) - return result + return v3.response_to_items(provider, context, json_data) def _process_live_events(provider, context, event_type='live'): def _sort(x): return x.get_date() - provider.set_content_type(context, constants.content_type.VIDEOS) - result = [] - + provider.set_content_type(context, content_type.VIDEOS) # TODO: cache result - page_token = context.get_param('page_token', '') - location = context.get_param('location', False) - - json_data = provider.get_client(context).get_live_events(event_type=event_type, page_token=page_token, location=location) + json_data = provider.get_client(context).get_live_events( + event_type=event_type, + page_token=context.get_param('page_token', ''), + location=context.get_param('location', False), + ) if not v3.handle_error(context, json_data): return False - result.extend(v3.response_to_items(provider, context, json_data, sort=_sort)) - - return result + return v3.response_to_items(provider, context, json_data, sort=_sort) def _process_description_links(provider, context): - incognito = context.get_param('incognito', False) - addon_id = context.get_param('addon_id', '') + params = context.get_params() + incognito = params.get('incognito', False) + addon_id = params.get('addon_id', '') - def _extract_urls(_video_id): - provider.set_content_type(context, constants.content_type.VIDEOS) + def _extract_urls(video_id): + provider.set_content_type(context, content_type.VIDEOS) url_resolver = UrlResolver(context) - result = [] - - progress_dialog = \ - context.get_ui().create_progress_dialog(heading=context.localize('please_wait'), - background=False) + progress_dialog = context.get_ui().create_progress_dialog( + heading=context.localize('please_wait'), background=False + ) resource_manager = provider.get_resource_manager(context) - video_data = resource_manager.get_videos([_video_id]) - yt_item = video_data[_video_id] + video_data = resource_manager.get_videos([video_id]) + yt_item = video_data[video_id] snippet = yt_item['snippet'] # crash if not conform description = strip_html_from_text(snippet['description']) @@ -190,153 +174,141 @@ def _extract_urls(_video_id): url_to_item_converter = UrlToItemConverter() url_to_item_converter.add_urls(res_urls, provider, context) - - result.extend(url_to_item_converter.get_items(provider, context)) + result = url_to_item_converter.get_items(provider, context) progress_dialog.close() - if not result: - progress_dialog.close() - context.get_ui().on_ok(title=context.localize('video.description.links'), - text=context.localize('video.description.links.not_found')) - return False - - return result - - def _display_channels(_channel_ids): - _channel_id_dict = {} + if result: + return result + context.get_ui().on_ok( + title=context.localize('video.description.links'), + text=context.localize('video.description.links.not_found') + ) + return False + def _display_channels(channel_ids): item_params = {} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id - for channel_id in _channel_ids: - item_uri = context.create_uri(['channel', channel_id], item_params) - channel_item = DirectoryItem('', item_uri) + channel_id_dict = {} + for channel_id in channel_ids: + channel_item = DirectoryItem( + '', context.create_uri(['channel', channel_id], item_params) + ) channel_item.set_fanart(provider.get_fanart(context)) - _channel_id_dict[channel_id] = channel_item + channel_id_dict[channel_id] = channel_item - _channel_item_dict = {} - utils.update_channel_infos(provider, context, _channel_id_dict, channel_items_dict=_channel_item_dict) + channel_item_dict = {} + utils.update_channel_infos(provider, + context, + channel_id_dict, + channel_items_dict=channel_item_dict) # clean up - remove empty entries - _result = [] - for key in _channel_id_dict: - _channel_item = _channel_id_dict[key] - if _channel_item.get_name(): - _result.append(_channel_item) - return _result - - def _display_playlists(_playlist_ids): - _playlist_id_dict = {} + return [channel_item + for channel_item in channel_id_dict.values() + if channel_item.get_name()] + def _display_playlists(playlist_ids): item_params = {} if incognito: - item_params.update({'incognito': incognito}) + item_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + item_params['addon_id'] = addon_id - for playlist_id in _playlist_ids: - item_uri = context.create_uri(['playlist', playlist_id], item_params) - playlist_item = DirectoryItem('', item_uri) + playlist_id_dict = {} + for playlist_id in playlist_ids: + playlist_item = DirectoryItem( + '', context.create_uri(['playlist', playlist_id], item_params) + ) playlist_item.set_fanart(provider.get_fanart(context)) - _playlist_id_dict[playlist_id] = playlist_item + playlist_id_dict[playlist_id] = playlist_item - _channel_item_dict = {} - utils.update_playlist_infos(provider, context, _playlist_id_dict, _channel_item_dict) - utils.update_fanarts(provider, context, _channel_item_dict) + channel_item_dict = {} + utils.update_playlist_infos(provider, + context, + playlist_id_dict, + channel_items_dict=channel_item_dict) + utils.update_fanarts(provider, context, channel_item_dict) # clean up - remove empty entries - _result = [] - for key in _playlist_id_dict: - _playlist_item = _playlist_id_dict[key] - if _playlist_item.get_name(): - _result.append(_playlist_item) + return [playlist_item + for playlist_item in playlist_id_dict.values() + if playlist_item.get_name()] - return _result - - video_id = context.get_param('video_id', '') + video_id = params.get('video_id', '') if video_id: return _extract_urls(video_id) - channel_ids = context.get_param('channel_ids', []) + channel_ids = params.get('channel_ids', []) if channel_ids: return _display_channels(channel_ids) - playlist_ids = context.get_param('playlist_ids', []) + playlist_ids = params.get('playlist_ids', []) if playlist_ids: return _display_playlists(playlist_ids) context.log_error('Missing video_id or playlist_ids for description links') - return False def _process_saved_playlists_tv(provider, context): - provider.set_content_type(context, constants.content_type.FILES) - - result = [] - next_page_token = context.get_param('next_page_token', '') - offset = context.get_param('offset', 0) - json_data = provider.get_client(context).get_saved_playlists(page_token=next_page_token, offset=offset) - result.extend(tv.saved_playlists_to_items(provider, context, json_data)) - - return result + provider.set_content_type(context, content_type.FILES) + json_data = provider.get_client(context).get_saved_playlists( + page_token=context.get_param('next_page_token', ''), + offset=context.get_param('offset', 0) + ) + return tv.saved_playlists_to_items(provider, context, json_data) def _process_watch_history_tv(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - - result = [] - next_page_token = context.get_param('next_page_token', '') - offset = context.get_param('offset', 0) - json_data = provider.get_client(context).get_watch_history(page_token=next_page_token, offset=offset) - result.extend(tv.tv_videos_to_items(provider, context, json_data)) - - return result + provider.set_content_type(context, content_type.VIDEOS) + json_data = provider.get_client(context).get_watch_history( + page_token=context.get_param('next_page_token', ''), + offset=context.get_param('offset', 0) + ) + return tv.tv_videos_to_items(provider, context, json_data) def _process_purchases_tv(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - - result = [] - next_page_token = context.get_param('next_page_token', '') - offset = context.get_param('offset', 0) - json_data = provider.get_client(context).get_purchases(page_token=next_page_token, offset=offset) - result.extend(tv.tv_videos_to_items(provider, context, json_data)) - - return result + provider.set_content_type(context, content_type.VIDEOS) + json_data = provider.get_client(context).get_purchases( + page_token=context.get_param('next_page_token', ''), + offset=context.get_param('offset', 0) + ) + return tv.tv_videos_to_items(provider, context, json_data) def _process_new_uploaded_videos_tv(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - - result = [] - next_page_token = context.get_param('next_page_token', '') - offset = context.get_param('offset', 0) - json_data = provider.get_client(context).get_my_subscriptions(page_token=next_page_token, offset=offset) - result.extend(tv.my_subscriptions_to_items(provider, context, json_data)) - - return result + provider.set_content_type(context, content_type.VIDEOS) + json_data = provider.get_client(context).get_my_subscriptions( + page_token=context.get_param('next_page_token', ''), + offset=context.get_param('offset', 0) + ) + return tv.my_subscriptions_to_items(provider, context, json_data) def _process_new_uploaded_videos_tv_filtered(provider, context): - provider.set_content_type(context, constants.content_type.VIDEOS) - - result = [] - next_page_token = context.get_param('next_page_token', '') - offset = context.get_param('offset', 0) - json_data = provider.get_client(context).get_my_subscriptions(page_token=next_page_token, offset=offset) - result.extend(tv.my_subscriptions_to_items(provider, context, json_data, do_filter=True)) - - return result + provider.set_content_type(context, content_type.VIDEOS) + json_data = provider.get_client(context).get_my_subscriptions( + page_token=context.get_param('next_page_token', ''), + offset=context.get_param('offset', 0) + ) + return tv.my_subscriptions_to_items(provider, + context, + json_data, + do_filter=True) def process(category, provider, context): _ = provider.get_client(context) # required for provider.is_logged_in() - if not provider.is_logged_in() and category in ['new_uploaded_videos_tv', 'new_uploaded_videos_tv_filtered', 'disliked_videos']: + if (not provider.is_logged_in() + and category in ['new_uploaded_videos_tv', + 'new_uploaded_videos_tv_filtered', + 'disliked_videos']): return UriItem(context.create_uri(['sign', 'in'])) if category == 'related_videos': From 697c9de1fdb2c5b4b9caab7023495969d2b689f2 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:10:57 +1100 Subject: [PATCH 092/141] Avoid JSONStore.save if data is unchanged on update - Also update log messages --- .../kodion/json_store/json_store.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index cf39c9e78..1f5a98c29 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -29,7 +29,7 @@ def __init__(self, filename): self.base_path = xbmcvfs.translatePath(_addon_data_path) if not xbmcvfs.exists(self.base_path) and not make_dirs(self.base_path): - log_error('JSONStore.__init__ |{path}| invalid path'.format( + log_error('JSONStore.__init__ - invalid path:\n|{path}|'.format( path=self.base_path )) return @@ -45,12 +45,12 @@ def set_defaults(self, reset=False): def save(self, data, update=False, process=None): if update: data = merge_dicts(self._data, data) - elif data == self._data: - log_debug('JSONStore.save |{filename}| data unchanged'.format( + if data == self._data: + log_debug('JSONStore.save - data unchanged:\n|{filename}|'.format( filename=self.filename )) return - log_debug('JSONStore.save |{filename}|'.format( + log_debug('JSONStore.save - saving:\n|{filename}|'.format( filename=self.filename )) try: @@ -64,18 +64,18 @@ def save(self, data, update=False, process=None): sort_keys=True))) self._data = process(_data) if process is not None else _data except (IOError, OSError): - log_error('JSONStore.save |{filename}| no access to file'.format( + log_error('JSONStore.save - access error:\n|{filename}|'.format( filename=self.filename )) return except (TypeError, ValueError): - log_error('JSONStore.save |{data}| invalid data'.format( + log_error('JSONStore.save - invalid data:\n|{data}|'.format( data=data )) self.set_defaults(reset=True) def load(self, process=None): - log_debug('JSONStore.load |{filename}|'.format( + log_debug('JSONStore.load - loading:\n|{filename}|'.format( filename=self.filename )) try: @@ -86,11 +86,11 @@ def load(self, process=None): _data = json.loads(data) self._data = process(_data) if process is not None else _data except (IOError, OSError): - log_error('JSONStore.load |{filename}| no access to file'.format( + log_error('JSONStore.load - access error:\n|{filename}|'.format( filename=self.filename )) except (TypeError, ValueError): - log_error('JSONStore.load |{data}| invalid data'.format( + log_error('JSONStore.load - invalid data:\n|{data}|'.format( data=data )) @@ -101,7 +101,7 @@ def get_data(self, process=None): _data = json.loads(json.dumps(self._data)) return process(_data) if process is not None else _data except (TypeError, ValueError): - log_error('JSONStore.get_data |{data}| invalid data'.format( + log_error('JSONStore.get_data - invalid data:\n|{data}|'.format( data=self._data )) self.set_defaults(reset=True) From 9c19a29174c304a7ad2a29e9e03bfe57bb1b4a36 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:12:11 +1100 Subject: [PATCH 093/141] Add print_stats convenience method to debug.Profiler --- resources/lib/youtube_plugin/kodion/debug.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index 3160c47ec..4c7b9aa9b 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -218,3 +218,8 @@ def get_stats(self, flush=True, reuse=False): self.enable(flush) return output + + def print_stats(self): + log_debug('Profiling stats: {0}'.format(self.get_stats( + reuse=self._reuse + ))) From 13d71f01ccedfccd72c0544be34fe460ceb504bc Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:17:52 +1100 Subject: [PATCH 094/141] Fix re-requesting blocked video info - Cache empty video data - May result in failed requests being cached for a month - TODO: See whether this will require changes to cache expiration --- .../lib/youtube_plugin/youtube/helper/resource_manager.py | 8 ++++---- resources/lib/youtube_plugin/youtube/helper/utils.py | 4 ++-- .../lib/youtube_plugin/youtube/helper/yt_specials.py | 8 +++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index fab7c216e..a060b5981 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -110,11 +110,11 @@ def _update_videos(self, video_ids, live_details=False, suppress_errors=False): if video_ids_to_update: self._context.log_debug('No data for videos |%s| cached' % ', '.join(video_ids_to_update)) json_data = self._client.get_videos(video_ids_to_update, live_details) - video_data = { - yt_item['id']: yt_item + video_data = dict.fromkeys(video_ids_to_update, {}) + video_data.update({ + yt_item['id']: yt_item or {} for yt_item in json_data.get('items', []) - if yt_item - } + }) result.update(video_data) data_cache.set_items(video_data) self._context.log_debug('Cached data for videos |%s|' % ', '.join(video_data)) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 2262b6f9c..ab487387c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -371,10 +371,10 @@ def update_video_infos(provider, context, video_id_dict, # set mediatype video_item.set_mediatype('video') # using video - if not yt_item: + if not yt_item or 'snippet' not in yt_item: continue - snippet = yt_item['snippet'] # crash if not conform + snippet = yt_item['snippet'] play_data = use_play_data and yt_item.get('play_data') broadcast_type = snippet.get('liveBroadcastContent') video_item.live = broadcast_type == 'live' diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index f7744173d..5ddc33e9c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -150,7 +150,13 @@ def _extract_urls(video_id): video_data = resource_manager.get_videos([video_id]) yt_item = video_data[video_id] - snippet = yt_item['snippet'] # crash if not conform + if not yt_item or 'snippet' not in yt_item: + context.get_ui().on_ok( + title=context.localize('video.description.links'), + text=context.localize('video.description.links.not_found') + ) + return False + snippet = yt_item['snippet'] description = strip_html_from_text(snippet['description']) function_cache = context.get_function_cache() From 0f4f212ba12a6eab50b2f541e83f8e632af3af5c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:26:20 +1100 Subject: [PATCH 095/141] Misc tidy ups - time_milliseconds parameter for notifications renamed to time_ms - Updated imports --- .../kodion/ui/abstract_context_ui.py | 2 +- .../kodion/ui/xbmc/xbmc_context_ui.py | 4 +- .../youtube/client/login_client.py | 6 +-- .../youtube/client/request_client.py | 4 +- .../youtube/helper/ratebypass/__init__.py | 2 +- .../youtube/helper/resource_manager.py | 2 +- .../youtube/helper/signature/__init__.py | 2 +- .../lib/youtube_plugin/youtube/helper/tv.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 4 +- .../lib/youtube_plugin/youtube/helper/v3.py | 4 +- .../youtube_plugin/youtube/helper/yt_login.py | 3 +- .../youtube/helper/yt_old_actions.py | 2 +- .../youtube_plugin/youtube/helper/yt_play.py | 12 +++--- .../youtube/helper/yt_playlist.py | 41 ++++++++++--------- .../youtube/helper/yt_specials.py | 10 ++--- .../youtube/helper/yt_subscriptions.py | 10 ++--- .../youtube_plugin/youtube/helper/yt_video.py | 15 +++---- .../lib/youtube_plugin/youtube/provider.py | 6 +-- 18 files changed, 67 insertions(+), 64 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py index efca1c1b2..1f5c05769 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py @@ -41,7 +41,7 @@ def open_settings(self): raise NotImplementedError() def show_notification(self, message, header='', image_uri='', - time_milliseconds=5000, audible=True): + time_ms=5000, audible=True): raise NotImplementedError() @staticmethod diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index b3ffb53a0..da63599a7 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -114,7 +114,7 @@ def show_notification(self, message, header='', image_uri='', - time_milliseconds=5000, + time_ms=5000, audible=True): _header = header if not _header: @@ -129,7 +129,7 @@ def show_notification(self, xbmcgui.Dialog().notification(_header, _message, _image, - time_milliseconds, + time_ms, audible) def open_settings(self): diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index cf37fcfac..5249f4fc4 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -21,13 +21,13 @@ youtube_tv, ) from .request_client import YouTubeRequestClient -from ...kodion.compatibility import parse_qsl -from ...kodion.logger import log_debug -from ...youtube.youtube_exceptions import ( +from ..youtube_exceptions import ( InvalidGrant, LoginException, YouTubeException, ) +from ...kodion.compatibility import parse_qsl +from ...kodion.logger import log_debug class LoginClient(YouTubeRequestClient): diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 4bace16e9..42f438214 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -9,9 +9,9 @@ from __future__ import absolute_import, division, unicode_literals -from ...kodion.utils import merge_dicts +from ..youtube_exceptions import YouTubeException from ...kodion.network import BaseRequestsClass -from ...youtube.youtube_exceptions import YouTubeException +from ...kodion.utils import merge_dicts class YouTubeRequestClient(BaseRequestsClass): diff --git a/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py b/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py index 2dcf59c7d..3da983ffd 100644 --- a/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/ratebypass/__init__.py @@ -9,7 +9,7 @@ from __future__ import absolute_import, division, unicode_literals -from ....youtube.helper.ratebypass import ratebypass +from . import ratebypass __all__ = ('ratebypass',) diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index a060b5981..60e3aa4b4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -254,7 +254,7 @@ def handle_error(self, json_data, suppress_errors=False): context.get_ui().on_ok(title, message) else: context.get_ui().show_notification(message, title, - time_milliseconds=message_timeout) + time_ms=message_timeout) raise YouTubeException(error_message) diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py index 4370d5ee9..c3cf86311 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/__init__.py @@ -10,7 +10,7 @@ from __future__ import absolute_import, division, unicode_literals -from ....youtube.helper.signature.cipher import Cipher +from .cipher import Cipher __all__ = ('Cipher',) diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index 56a0ca9ea..716a151b1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -10,8 +10,8 @@ from __future__ import absolute_import, division, unicode_literals +from ..helper import utils from ...kodion.items import DirectoryItem, NextPageItem, VideoItem -from ...youtube.helper import utils def my_subscriptions_to_items(provider, context, json_data, do_filter=False): diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index ab487387c..331846962 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -14,6 +14,7 @@ import time from math import log10 +from ..helper import yt_context_menu from ...kodion.items import DirectoryItem from ...kodion.utils import ( create_path, @@ -21,7 +22,6 @@ friendly_number, strip_html_from_text, ) -from ...youtube.helper import yt_context_menu try: @@ -794,7 +794,7 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id): result_items = v3.response_to_items(provider, context, json_data, process_next_page=False) page_token = json_data.get('nextPageToken', '') except: - context.get_ui().show_notification('Failed to add a suggested video.', time_milliseconds=5000) + context.get_ui().show_notification('Failed to add a suggested video.', time_ms=5000) if result_items: add_item = next(( diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 713435b16..22e6ac0dd 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -21,10 +21,10 @@ update_playlist_infos, update_video_infos, ) +from ..helper import yt_context_menu from ...kodion import KodionException from ...kodion.items import DirectoryItem, NextPageItem, VideoItem from ...kodion.utils import strip_html_from_text -from ...youtube.helper import yt_context_menu def _process_list_response(provider, context, json_data): @@ -457,7 +457,7 @@ def handle_error(context, json_data): if ok_dialog: context.get_ui().on_ok(title, message) else: - context.get_ui().show_notification(message, title, time_milliseconds=message_timeout) + context.get_ui().show_notification(message, title, time_ms=message_timeout) return False diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index 81bcece3c..4afb9992a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -13,7 +13,8 @@ import copy import json import time -from ...youtube.youtube_exceptions import LoginException + +from ..youtube_exceptions import LoginException def process(mode, provider, context, sign_out_refresh=True): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py b/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py index 0ed81368e..c7501eb77 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_old_actions.py @@ -55,7 +55,7 @@ def process_old_action(provider, context, re_match): """ if context.get_system_version().get_version() >= (15, 0): message = u"You're using old YouTube-Plugin calls - please review the log for updated end points starting with Isengard" - context.get_ui().show_notification(message, time_milliseconds=15000) + context.get_ui().show_notification(message, time_ms=15000) """ action = context.get_param('action', '') diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 88a327afb..f6d4cdb76 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -14,10 +14,10 @@ import random import traceback -from ... import kodion +from ..helper import utils, v3 +from ..youtube_exceptions import YouTubeException from ...kodion.items import VideoItem -from ...youtube.helper import utils, v3 -from ...youtube.youtube_exceptions import YouTubeException +from ...kodion.utils import select_stream def play_video(provider, context): @@ -53,10 +53,10 @@ def play_video(provider, context): if not video_streams: message = context.localize('error.no_video_streams_found') - ui.show_notification(message, time_milliseconds=5000) + ui.show_notification(message, time_ms=5000) return False - video_stream = kodion.utils.select_stream( + video_stream = select_stream( context, video_streams, ask_for_quality=ask_for_quality, @@ -71,7 +71,7 @@ def play_video(provider, context): if is_video and video_stream['video'].get('rtmpe', False): message = context.localize('error.rtmpe_not_supported') - ui.show_notification(message, time_milliseconds=5000) + ui.show_notification(message, time_ms=5000) return False play_suggested = settings.get_bool('youtube.suggested_videos', False) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index c0dfa676d..ffb69fdb3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -10,8 +10,9 @@ from __future__ import absolute_import, division, unicode_literals -from ... import kodion -from ...youtube.helper import v3 +from ..helper import v3 +from ...kodion import KodionException +from ...kodion.utils import find_video_id def _process_add_video(provider, context, keymap_action=False): @@ -22,7 +23,7 @@ def _process_add_video(provider, context, keymap_action=False): playlist_id = context.get_param('playlist_id', '') if not playlist_id: - raise kodion.KodionException('Playlist/Add: missing playlist_id') + raise KodionException('Playlist/Add: missing playlist_id') if playlist_id.lower() == 'watch_later': playlist_id = watch_later_id @@ -30,10 +31,10 @@ def _process_add_video(provider, context, keymap_action=False): video_id = context.get_param('video_id', '') if not video_id: if context.is_plugin_path(listitem_path, 'play/'): - video_id = kodion.utils.find_video_id(listitem_path) + video_id = find_video_id(listitem_path) keymap_action = True if not video_id: - raise kodion.KodionException('Playlist/Add: missing video_id') + raise KodionException('Playlist/Add: missing video_id') if playlist_id != 'HL': json_data = client.add_video_to_playlist(playlist_id=playlist_id, video_id=video_id) @@ -47,7 +48,7 @@ def _process_add_video(provider, context, keymap_action=False): context.get_ui().show_notification( message=notify_message, - time_milliseconds=2500, + time_ms=2500, audible=False ) @@ -80,16 +81,16 @@ def _process_remove_video(provider, context): keymap_action = True if not playlist_id: - raise kodion.KodionException('Playlist/Remove: missing playlist_id') + raise KodionException('Playlist/Remove: missing playlist_id') if not video_id: - raise kodion.KodionException('Playlist/Remove: missing video_id') + raise KodionException('Playlist/Remove: missing video_id') if not video_name: if listitem_title: video_name = listitem_title else: - raise kodion.KodionException('Playlist/Remove: missing video_name') + raise KodionException('Playlist/Remove: missing video_name') if playlist_id != 'HL' and playlist_id.strip().lower() != 'wl': if context.get_ui().on_remove_content(video_name): @@ -102,7 +103,7 @@ def _process_remove_video(provider, context): context.get_ui().show_notification( message=context.localize('playlist.removed_from'), - time_milliseconds=2500, + time_ms=2500, audible=False ) @@ -119,11 +120,11 @@ def _process_remove_video(provider, context): def _process_remove_playlist(provider, context): playlist_id = context.get_param('playlist_id', '') if not playlist_id: - raise kodion.KodionException('Playlist/Remove: missing playlist_id') + raise KodionException('Playlist/Remove: missing playlist_id') playlist_name = context.get_param('playlist_name', '') if not playlist_name: - raise kodion.KodionException('Playlist/Remove: missing playlist_name') + raise KodionException('Playlist/Remove: missing playlist_name') if context.get_ui().on_delete_content(playlist_name): json_data = provider.get_client(context).remove_playlist(playlist_id=playlist_id) @@ -144,12 +145,12 @@ def _process_select_playlist(provider, context): video_id = context.get_param('video_id', '') if not video_id: if context.is_plugin_path(listitem_path, 'play/'): - video_id = kodion.utils.find_video_id(listitem_path) + video_id = find_video_id(listitem_path) if video_id: context.set_param('video_id', video_id) keymap_action = True if not video_id: - raise kodion.KodionException('Playlist/Select: missing video_id') + raise KodionException('Playlist/Select: missing video_id') function_cache = context.get_function_cache() client = provider.get_client(context) @@ -228,7 +229,7 @@ def _process_select_playlist(provider, context): def _process_rename_playlist(provider, context): playlist_id = context.get_param('playlist_id', '') if not playlist_id: - raise kodion.KodionException('playlist/rename: missing playlist_id') + raise KodionException('playlist/rename: missing playlist_id') current_playlist_name = context.get_param('playlist_name', '') result, text = context.get_ui().on_keyboard_input(context.localize('rename'), @@ -244,10 +245,10 @@ def _process_rename_playlist(provider, context): def _watchlater_playlist_id_change(context, method): playlist_id = context.get_param('playlist_id', '') if not playlist_id: - raise kodion.KodionException('watchlater_list/%s: missing playlist_id' % method) + raise KodionException('watchlater_list/%s: missing playlist_id' % method) playlist_name = context.get_param('playlist_name', '') if not playlist_name: - raise kodion.KodionException('watchlater_list/%s: missing playlist_name' % method) + raise KodionException('watchlater_list/%s: missing playlist_name' % method) if method == 'set': if context.get_ui().on_yes_no_input(context.get_name(), context.localize('watch_later.list.set.confirm') % playlist_name): @@ -267,10 +268,10 @@ def _watchlater_playlist_id_change(context, method): def _history_playlist_id_change(context, method): playlist_id = context.get_param('playlist_id', '') if not playlist_id: - raise kodion.KodionException('history_list/%s: missing playlist_id' % method) + raise KodionException('history_list/%s: missing playlist_id' % method) playlist_name = context.get_param('playlist_name', '') if not playlist_name: - raise kodion.KodionException('history_list/%s: missing playlist_name' % method) + raise KodionException('history_list/%s: missing playlist_name' % method) if method == 'set': if context.get_ui().on_yes_no_input(context.get_name(), context.localize('history.list.set.confirm') % playlist_name): @@ -302,4 +303,4 @@ def process(method, category, provider, context): return _watchlater_playlist_id_change(context, method) if method in {'set', 'remove'} and category == 'history': return _history_playlist_id_change(context, method) - raise kodion.KodionException("Unknown category '%s' or method '%s'" % (category, method)) + raise KodionException("Unknown category '%s' or method '%s'" % (category, method)) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 5ddc33e9c..836a708fb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -11,17 +11,17 @@ from __future__ import absolute_import, division, unicode_literals from . import utils -from ...kodion import KodionException -from ...kodion.constants import content_type -from ...kodion.items import DirectoryItem, UriItem -from ...kodion.utils import strip_html_from_text -from ...youtube.helper import ( +from ..helper import ( UrlResolver, UrlToItemConverter, extract_urls, tv, v3, ) +from ...kodion import KodionException +from ...kodion.constants import content_type +from ...kodion.items import DirectoryItem, UriItem +from ...kodion.utils import strip_html_from_text def _process_related_videos(provider, context): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index 24da6925c..c90893947 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -10,9 +10,9 @@ from __future__ import absolute_import, division, unicode_literals +from ..helper import v3 +from ...kodion import KodionException from ...kodion.items import UriItem -from ... import kodion -from ...youtube.helper import v3 def _process_list(provider, context): @@ -43,7 +43,7 @@ def _process_add(provider, context): context.get_ui().show_notification( context.localize('subscribed.to.channel'), - time_milliseconds=2500, + time_ms=2500, audible=False ) @@ -68,7 +68,7 @@ def _process_remove(provider, context): context.get_ui().show_notification( context.localize('unsubscribed.from.channel'), - time_milliseconds=2500, + time_ms=2500, audible=False ) @@ -92,6 +92,6 @@ def process(method, provider, context): elif method == 'remove': return _process_remove(provider, context) else: - raise kodion.KodionException("Unknown subscriptions method '%s'" % method) + raise KodionException("Unknown subscriptions method '%s'" % method) return result diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 699263a9e..63c517875 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -10,8 +10,9 @@ from __future__ import absolute_import, division, unicode_literals -from ... import kodion -from ...youtube.helper import v3 +from ..helper import v3 +from ...kodion import KodionException +from ...kodion.utils import find_video_id def _process_rate_video(provider, context, re_match): @@ -28,10 +29,10 @@ def _process_rate_video(provider, context, re_match): video_id = re_match.group('video_id') except IndexError: if context.is_plugin_path(listitem_path, 'play/'): - video_id = kodion.utils.find_video_id(listitem_path) + video_id = find_video_id(listitem_path) if not video_id: - raise kodion.KodionException('video/rate/: missing video_id') + raise KodionException('video/rate/: missing video_id') try: current_rating = re_match.group('rating') @@ -82,7 +83,7 @@ def _process_rate_video(provider, context, re_match): if notify_message: context.get_ui().show_notification( message=notify_message, - time_milliseconds=2500, + time_ms=2500, audible=False ) @@ -92,7 +93,7 @@ def _process_rate_video(provider, context, re_match): def _process_more_for_video(context): video_id = context.get_param('video_id', '') if not video_id: - raise kodion.KodionException('video/more/: missing video_id') + raise KodionException('video/more/: missing video_id') items = [] @@ -128,4 +129,4 @@ def process(method, provider, context, re_match): return _process_rate_video(provider, context, re_match) if method == 'more': return _process_more_for_video(context) - raise kodion.KodionException("Unknown method '%s'" % method) + raise KodionException("Unknown method '%s'" % method) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 8c70cb240..d0ae46420 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -17,6 +17,7 @@ import socket from base64 import b64decode +from .client import YouTube from .helper import ( ResourceManager, UrlResolver, @@ -29,6 +30,7 @@ yt_playlist, yt_setup_wizard, yt_specials, + yt_subscriptions, yt_video, ) from .youtube_exceptions import InvalidGrant, LoginException @@ -37,8 +39,6 @@ from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem from ..kodion.network import get_client_ip_address, is_httpd_live from ..kodion.utils import find_video_id, strip_html_from_text -from ..youtube.client import YouTube -from ..youtube.helper import yt_subscriptions class Provider(AbstractProvider): @@ -1513,7 +1513,7 @@ def handle_exception(self, context, exception_to_handle): if ok_dialog: context.get_ui().on_ok(title, message) else: - context.get_ui().show_notification(message, title, time_milliseconds=message_timeout) + context.get_ui().show_notification(message, title, time_ms=message_timeout) return False From 82d4441f108c4f4a5ece331b7cbe353891529dd8 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:27:21 +1100 Subject: [PATCH 096/141] Remove unused code - Introduced as part of reverted tags PR --- .../youtube/helper/yt_specials.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 836a708fb..ea2e29a1a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -270,24 +270,6 @@ def _process_saved_playlists_tv(provider, context): return tv.saved_playlists_to_items(provider, context, json_data) -def _process_watch_history_tv(provider, context): - provider.set_content_type(context, content_type.VIDEOS) - json_data = provider.get_client(context).get_watch_history( - page_token=context.get_param('next_page_token', ''), - offset=context.get_param('offset', 0) - ) - return tv.tv_videos_to_items(provider, context, json_data) - - -def _process_purchases_tv(provider, context): - provider.set_content_type(context, content_type.VIDEOS) - json_data = provider.get_client(context).get_purchases( - page_token=context.get_param('next_page_token', ''), - offset=context.get_param('offset', 0) - ) - return tv.tv_videos_to_items(provider, context, json_data) - - def _process_new_uploaded_videos_tv(provider, context): provider.set_content_type(context, content_type.VIDEOS) json_data = provider.get_client(context).get_my_subscriptions( From e2210d88c30a7eec9361809b75dd20c99ac5d195 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:49:16 +1100 Subject: [PATCH 097/141] Create custom InvalidJSON exception - Inherits from requests.exceptions.InvalidJSONError and KodionException --- resources/lib/youtube_plugin/kodion/network/__init__.py | 3 ++- resources/lib/youtube_plugin/kodion/network/requests.py | 5 ++++- resources/lib/youtube_plugin/youtube/client/login_client.py | 5 ++--- resources/lib/youtube_plugin/youtube/youtube_exceptions.py | 5 +++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/__init__.py b/resources/lib/youtube_plugin/kodion/network/__init__.py index 960dd0e91..45fe2ae78 100644 --- a/resources/lib/youtube_plugin/kodion/network/__init__.py +++ b/resources/lib/youtube_plugin/kodion/network/__init__.py @@ -11,7 +11,7 @@ from .http_server import get_client_ip_address, get_http_server, is_httpd_live from .ip_api import Locator -from .requests import BaseRequestsClass +from .requests import BaseRequestsClass, InvalidJSONError __all__ = ( @@ -19,5 +19,6 @@ 'get_http_server', 'is_httpd_live', 'BaseRequestsClass', + 'InvalidJSONError', 'Locator', ) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index c41473b6a..bdd2981a5 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -14,7 +14,7 @@ from requests import Session from requests.adapters import HTTPAdapter, Retry -from requests.exceptions import RequestException +from requests.exceptions import InvalidJSONError, RequestException from ..compatibility import xbmcaddon from ..logger import log_error @@ -143,3 +143,6 @@ def request(self, url, method='GET', raise self._default_exc(error_title)(exc) return response + + +__all__ = ('BaseRequestsClass', 'InvalidJSONError') diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 5249f4fc4..90205c3bb 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -12,8 +12,6 @@ import time -from requests.exceptions import InvalidJSONError - from .__config__ import ( api, developer_keys, @@ -23,6 +21,7 @@ from .request_client import YouTubeRequestClient from ..youtube_exceptions import ( InvalidGrant, + InvalidJSON, LoginException, YouTubeException, ) @@ -91,7 +90,7 @@ def _login_json_hook(response): json_data=json_data, response=response) except ValueError as error: - raise InvalidJSONError(error, response=response) + raise InvalidJSON(error, response=response) response.raise_for_status() return json_data diff --git a/resources/lib/youtube_plugin/youtube/youtube_exceptions.py b/resources/lib/youtube_plugin/youtube/youtube_exceptions.py index fe4e24f91..761187896 100644 --- a/resources/lib/youtube_plugin/youtube/youtube_exceptions.py +++ b/resources/lib/youtube_plugin/youtube/youtube_exceptions.py @@ -11,6 +11,7 @@ from __future__ import absolute_import, division, unicode_literals from ..kodion import KodionException +from ..kodion.network import InvalidJSONError class LoginException(KodionException): @@ -23,3 +24,7 @@ class YouTubeException(KodionException): class InvalidGrant(KodionException): pass + + +class InvalidJSON(KodionException, InvalidJSONError): + pass From ba686a520cb99c9cb27a944af5fb3d541daf6417 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:04:16 +1100 Subject: [PATCH 098/141] Fixup log out on key change after b3b08d2 --- .../youtube/client/__config__.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index e6279c3f1..f2946e7aa 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -88,7 +88,14 @@ def _on_init(self): refresh_token = self._settings.user_refresh_token() token_expires = self._settings.user_token_expiration() last_hash = self._settings.api_last_hash() - updated_hash = self._api_keys_changed(switch) + + current_set_hash = self._get_key_set_hash(switch) + if current_set_hash != user_details.get('last_key_hash', ''): + self.changed = True + updated_hash = current_set_hash + else: + self.changed = False + updated_hash = None if access_token or refresh_token or last_hash: self._settings.user_access_token('') @@ -99,26 +106,23 @@ def _on_init(self): if updated_hash or (access_token and refresh_token and not (user_details.get('access_token') and user_details.get('refresh_token'))): - if switch == 'own': - own_key_hash = self._get_key_set_hash('own') - if (last_hash == self._get_key_set_hash('own', True) - or last_hash == own_key_hash): - last_hash = own_key_hash - else: - last_hash = None - else: + if (last_hash and switch == 'own' and (last_hash == current_set_hash + or last_hash == self._get_key_set_hash('own', old=True))): last_hash = None if updated_hash: - last_hash = updated_hash self._context.log_warning('User: |{user}|, ' 'Switching API key set to: |{switch}|' .format(user=self.get_current_user(), switch=switch)) + + if last_hash: self._context.log_debug('API key set changed: Signing out') self._context.execute('RunPlugin(plugin://plugin.video.youtube/' 'sign/out/?confirmed=true)') + last_hash = updated_hash if updated_hash else current_set_hash + self._access_manager.update_access_token( access_token, token_expires, refresh_token, last_hash ) @@ -173,16 +177,6 @@ def get_api_keys(self, switch): 'id': ''.join((client_id, '.apps.googleusercontent.com')), 'secret': client_secret} - def _api_keys_changed(self, switch): - user_details = self._access_manager.get_current_user_details() - last_set_hash = user_details.get('last_key_hash', '') - current_set_hash = self._get_key_set_hash(switch) - if last_set_hash != current_set_hash: - self.changed = True - return current_set_hash - self.changed = False - return None - def _get_key_set_hash(self, switch, old=False): api_key, client_id, client_secret = self.get_api_keys(switch) if old and switch == 'own': From a58f31ee78dcaf00b3961f947727483f98cf4024 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 14 Dec 2023 21:59:51 +1100 Subject: [PATCH 099/141] Remove all key set setting related functions - Fixes after ba686a5 and b3b08d2 - Functionality was deprecated and should have been removed not refactored --- .../kodion/json_store/access_manager.py | 36 +++++---- .../youtube/client/__config__.py | 80 +++++-------------- 2 files changed, 42 insertions(+), 74 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index f4d152802..82eb9febf 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -131,7 +131,7 @@ def get_current_user_details(self): """ :return: current user """ - return self.get_users()[self._user] + return self.get_users()[self._user].copy() def get_current_user_id(self): """ @@ -435,14 +435,12 @@ def is_access_token_expired(self): def update_access_token(self, access_token, unix_timestamp=None, - refresh_token=None, - last_key_hash=None): + refresh_token=None): """ Updates the old access token with the new one. :param access_token: :param unix_timestamp: :param refresh_token: - :param last_key_hash: :return: """ current_user = self.get_current_user_details() @@ -454,9 +452,18 @@ def update_access_token(self, if refresh_token is not None: current_user['refresh_token'] = refresh_token - if last_key_hash is not None: - current_user['last_key_hash'] = last_key_hash + data = { + 'access_manager': { + 'users': { + self._user: current_user + } + } + } + self.save(data, update=True) + def set_last_key_hash(self, key_hash): + current_user = self.get_current_user_details() + current_user['last_key_hash'] = key_hash data = { 'access_manager': { 'users': { @@ -586,7 +593,7 @@ def set_dev_last_key_hash(self, addon_id, key_hash): def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): last_hash = self.get_dev_last_key_hash(addon_id) - current_hash = self.__calc_key_hash(api_key, client_id, client_secret) + current_hash = self.calc_key_hash(api_key, client_id, client_secret) if not last_hash and current_hash: self.set_dev_last_key_hash(addon_id, current_hash) @@ -599,16 +606,15 @@ def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): return False @staticmethod - def __calc_key_hash(api_key, client_id, client_secret): - + def calc_key_hash(key, id, secret): md5_hash = md5() try: - md5_hash.update(api_key.encode('utf-8')) - md5_hash.update(client_id.encode('utf-8')) - md5_hash.update(client_secret.encode('utf-8')) + md5_hash.update(key.encode('utf-8')) + md5_hash.update(id.encode('utf-8')) + md5_hash.update(secret.encode('utf-8')) except: - md5_hash.update(api_key) - md5_hash.update(client_id) - md5_hash.update(client_secret) + md5_hash.update(key) + md5_hash.update(id) + md5_hash.update(secret) return md5_hash.hexdigest() diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index f2946e7aa..fbe3eaf3f 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals from base64 import b64decode -from hashlib import md5 from ... import key_sets from ...kodion import Context @@ -83,54 +82,19 @@ def _on_init(self): switch = self.get_current_switch() user_details = self._access_manager.get_current_user_details() - - access_token = self._settings.user_access_token() - refresh_token = self._settings.user_refresh_token() - token_expires = self._settings.user_token_expiration() - last_hash = self._settings.api_last_hash() - + last_hash = user_details.get('last_key_hash', '') current_set_hash = self._get_key_set_hash(switch) - if current_set_hash != user_details.get('last_key_hash', ''): - self.changed = True - updated_hash = current_set_hash - else: - self.changed = False - updated_hash = None - - if access_token or refresh_token or last_hash: - self._settings.user_access_token('') - self._settings.user_refresh_token('') - self._settings.user_token_expiration(-1) - self._settings.api_last_hash('') - - if updated_hash or (access_token and refresh_token - and not (user_details.get('access_token') - and user_details.get('refresh_token'))): - if (last_hash and switch == 'own' and (last_hash == current_set_hash - or last_hash == self._get_key_set_hash('own', old=True))): - last_hash = None - - if updated_hash: - self._context.log_warning('User: |{user}|, ' - 'Switching API key set to: |{switch}|' - .format(user=self.get_current_user(), - switch=switch)) - - if last_hash: - self._context.log_debug('API key set changed: Signing out') - self._context.execute('RunPlugin(plugin://plugin.video.youtube/' - 'sign/out/?confirmed=true)') - - last_hash = updated_hash if updated_hash else current_set_hash - - self._access_manager.update_access_token( - access_token, token_expires, refresh_token, last_hash - ) - elif not updated_hash: - self._context.log_debug('User: |{user}|, ' - 'Using API key set: |{switch}|' - .format(user=self.get_current_user(), - switch=switch)) + self.changed = current_set_hash != last_hash + + self._context.log_debug('User: |{user}|, ' + 'Using API key set: |{switch}|' + .format(user=self.get_current_user(), + switch=switch)) + if self.changed: + self._context.log_debug('API key set changed: Signing out') + self._context.execute('RunPlugin(plugin://plugin.video.youtube/' + 'sign/out/?confirmed=true)') + self._access_manager.set_last_key_hash(current_set_hash) @staticmethod def get_current_switch(): @@ -173,20 +137,18 @@ def get_api_keys(self, switch): client_id = b64decode(client_id).decode('utf-8') client_secret = b64decode(client_secret).decode('utf-8') + client_id += '.apps.googleusercontent.com' return {'key': api_key, - 'id': ''.join((client_id, '.apps.googleusercontent.com')), + 'id': client_id, 'secret': client_secret} - def _get_key_set_hash(self, switch, old=False): - api_key, client_id, client_secret = self.get_api_keys(switch) - if old and switch == 'own': - client_id = client_id.replace('.apps.googleusercontent.com', '') - md5_hash = md5() - md5_hash.update(api_key.encode('utf-8')) - md5_hash.update(client_id.encode('utf-8')) - md5_hash.update(client_secret.encode('utf-8')) - - return md5_hash.hexdigest() + def _get_key_set_hash(self, switch): + key_set = self.get_api_keys(switch) + if switch == 'own': + client_id = key_set['id'].replace('.apps.googleusercontent.com', + '') + key_set['id'] = client_id + return self._access_manager.calc_key_hash(**key_set) def _strip_api_keys(self, api_key, client_id, client_secret): From 14e78890638231fa7bbb5c5db005030f4dd895f5 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 14 Dec 2023 22:28:59 +1100 Subject: [PATCH 100/141] Reduce whitespace in description - Looks worse but there is limited space on many list/widget views --- resources/lib/youtube_plugin/youtube/helper/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 331846962..2efeb40ca 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -506,11 +506,10 @@ def update_video_infos(provider, context, video_id_dict, description = strip_html_from_text(snippet['description']) if show_details: description = ''.join(( - ui.bold(channel_name, cr_after=2) if channel_name else '', + ui.bold(channel_name, cr_after=1) if channel_name else '', ui.new_line(stats, cr_after=1) if stats else '', (ui.italic(start_at, cr_after=1) if video_item.upcoming else ui.new_line(start_at, cr_after=1)) if start_at else '', - ui.new_line() if stats or start_at else '', description, )) video_item.set_studio(channel_name) From f7590a0ecdc5d5f37d615dc36cf148e2e700fef0 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 14 Dec 2023 23:11:19 +1100 Subject: [PATCH 101/141] Fix not updating channel fanart after 0889e7b --- .../lib/youtube_plugin/youtube/helper/utils.py | 2 +- resources/lib/youtube_plugin/youtube/helper/v3.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 2efeb40ca..d2ccf5439 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -543,7 +543,7 @@ def update_video_infos(provider, context, video_id_dict, # update channel mapping channel_id = snippet.get('channelId', '') - if channel_items_dict is not None: + if channel_id and channel_items_dict is not None: if channel_id not in channel_items_dict: channel_items_dict[channel_id] = [] channel_items_dict[channel_id].append(video_item) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 22e6ac0dd..e0e66f463 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -341,17 +341,20 @@ def _fetch(resource): resource['updater'](*resource['upd_args'], **resource['upd_kwargs']) for resource in resources: + if resource['defer']: + running += 1 + continue + if not resource['args'][0]: resource['complete'] = True continue running += 1 - if not resource['defer']: - # _fetch(resource) - thread = Thread(target=_fetch, args=(resource, )) - thread.daemon = True - thread.start() - resource['thread'] = thread + # _fetch(resource) + thread = Thread(target=_fetch, args=(resource, )) + thread.daemon = True + thread.start() + resource['thread'] = thread while running > 0: for resource in resources: From f50468183c3208ecf6c7380c889734e7795742e7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 15 Dec 2023 05:32:58 +1100 Subject: [PATCH 102/141] Update response/error hook implementation --- .../youtube_plugin/kodion/network/requests.py | 25 ++++++++++---- .../youtube/client/login_client.py | 33 ++++++++++--------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index bdd2981a5..34d4a8545 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -57,9 +57,12 @@ def request(self, url, method='GET', auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, # Custom event hook implementation - # See _login_json_hook and _login_error_hook in login_client.py + # See _response_hook and _error_hook in login_client.py # for example usage - response_hook=None, error_hook=None, + response_hook=None, + response_hook_kwargs=None, + error_hook=None, + error_hook_kwargs=None, error_title=None, error_info=None, raise_exc=False, **_): if timeout is None: timeout = self._timeout @@ -86,7 +89,10 @@ def request(self, url, method='GET', cert=cert, json=json,) if response_hook: - response = response_hook(response) + if response_hook_kwargs is None: + response_hook_kwargs = {} + response_hook_kwargs['response'] = response + response = response_hook(**response_hook_kwargs) else: response.raise_for_status() @@ -94,14 +100,21 @@ def request(self, url, method='GET', response_text = exc.response and exc.response.text stack_trace = format_stack() exc_tb = format_exc() + error_details = {'exc': exc} if error_hook: - error_response = error_hook(exc, response) - _title, _info, _response, _trace, _exc = error_response + if error_hook_kwargs is None: + error_hook_kwargs = {} + error_hook_kwargs['exc'] = exc + error_hook_kwargs['response'] = response + error_response = error_hook(**error_hook_kwargs) + _title, _info, _detail, _response, _trace, _exc = error_response if _title is not None: error_title = _title if _info is not None: error_info = _info + if _detail is not None: + error_details.update(_detail) if _response is not None: response = _response response_text = str(_response) @@ -117,7 +130,7 @@ def request(self, url, method='GET', error_info = str(exc) elif '{' in error_info: try: - error_info = error_info.format(exc=exc) + error_info = error_info.format(**error_details) except (AttributeError, IndexError, KeyError): error_info = str(exc) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 90205c3bb..77b4ecd8d 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -82,7 +82,8 @@ def __init__(self, config=None, language='en-US', region='', super(LoginClient, self).__init__(exc_type=LoginException) @staticmethod - def _login_json_hook(response): + def _response_hook(**kwargs): + response = kwargs['response'] try: json_data = response.json() if 'error' in json_data: @@ -95,16 +96,16 @@ def _login_json_hook(response): return json_data @staticmethod - def _login_error_hook(error, _response): - json_data = getattr(error, 'json_data', None) - if not json_data: - return None, None, None, None, LoginException + def _error_hook(**kwargs): + json_data = getattr(kwargs['exc'], 'json_data', None) + if not json_data or 'error' not in json_data: + return None, None, None, None, None, LoginException if json_data['error'] == 'authorization_pending': - return None, None, json_data, False, False + return None, None, None, json_data, False, False if (json_data['error'] == 'invalid_grant' and json_data.get('code') == '400'): - return None, None, json_data, False, InvalidGrant(json_data) - return None, None, json_data, False, LoginException(json_data) + return None, None, None, json_data, False, InvalidGrant(json_data) + return None, None, None, json_data, False, LoginException(json_data) def set_log_error(self, callback): self._log_error_callback = callback @@ -138,8 +139,8 @@ def revoke(self, refresh_token): method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, + response_hook=self._response_hook, + error_hook=self._error_hook, error_title='Logout Failed', error_info='Revoke failed: {exc}', raise_exc=True) @@ -178,8 +179,8 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, + response_hook=self._response_hook, + error_hook=self._error_hook, error_title='Login Failed', error_info=('Refresh token failed' ' {client}: {{exc}}' @@ -226,8 +227,8 @@ def request_access_token(self, code, client_id='', client_secret=''): method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, + response_hook=self._response_hook, + error_hook=self._error_hook, error_title='Login Failed: Unknown response', error_info=('Access token request failed' ' {client}: {{exc}}' @@ -262,8 +263,8 @@ def request_device_and_user_code(self, client_id=''): method='POST', data=post_data, headers=headers, - response_hook=self._login_json_hook, - error_hook=self._login_error_hook, + response_hook=self._response_hook, + error_hook=self._error_hook, error_title='Login Failed: Unknown response', error_info=('Device/user code request failed' ' {client}: {{exc}}' From 764caec393ff9f7fee777cfa9c8bf497573d9801 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:47:28 +1100 Subject: [PATCH 103/141] Standardise input to SQL storage - Input/output will be always be JSON de(serialized) and (un)pickled - Also set ensure_ascii=False --- resources/lib/youtube_plugin/kodion/abstract_provider.py | 3 +-- .../lib/youtube_plugin/kodion/json_store/json_store.py | 6 +++--- .../youtube_plugin/kodion/player/xbmc/xbmc_playlist.py | 2 +- .../lib/youtube_plugin/kodion/sql_store/data_cache.py | 6 +++--- .../youtube_plugin/kodion/sql_store/playback_history.py | 2 +- resources/lib/youtube_plugin/kodion/sql_store/storage.py | 9 +++++---- resources/lib/youtube_plugin/youtube/client/youtube.py | 6 +++--- .../lib/youtube_plugin/youtube/helper/video_info.py | 7 +++---- resources/lib/youtube_plugin/youtube/helper/yt_play.py | 3 ++- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index b346a196e..a35a8bad5 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -247,8 +247,7 @@ def _internal_search(self, context, re_match): channel_id = context.get_param('channel_id', '') self._data_cache.set_item('search_query', - json.dumps({'query': quote(query)}, - ensure_ascii=False)) + {'query': quote(query)}) if not incognito and not channel_id: try: diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 1f5a98c29..ca6baad78 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -56,7 +56,7 @@ def save(self, data, update=False, process=None): try: if not data: raise ValueError - _data = json.loads(json.dumps(data)) + _data = json.loads(json.dumps(data, ensure_ascii=False)) with open(self.filename, mode='w', encoding='utf-8') as jsonfile: jsonfile.write(to_unicode(json.dumps(_data, ensure_ascii=False, @@ -98,12 +98,12 @@ def get_data(self, process=None): try: if not self._data: raise ValueError - _data = json.loads(json.dumps(self._data)) + _data = json.loads(json.dumps(self._data, ensure_ascii=False)) return process(_data) if process is not None else _data except (TypeError, ValueError): log_error('JSONStore.get_data - invalid data:\n|{data}|'.format( data=self._data )) self.set_defaults(reset=True) - _data = json.loads(json.dumps(self._data)) + _data = json.loads(json.dumps(self._data, ensure_ascii=False)) return process(_data) if process is not None else _data diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py index 29e03de19..e6bdc14c2 100644 --- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py @@ -64,7 +64,7 @@ def get_items(self, properties=None, dumps=False): result = response['result']['items'] else: result = [] - return json.dumps(result) if dumps else result + return json.dumps(result, ensure_ascii=False) if dumps else result if 'error' in response: message = response['error']['message'] diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index 8eb571ce0..f6f7d08b3 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -26,7 +26,7 @@ def is_empty(self): return self._is_empty() def get_items(self, content_ids, seconds): - query_result = self._get_by_ids(content_ids, process=json.loads) + query_result = self._get_by_ids(content_ids) if not query_result: return {} @@ -48,7 +48,7 @@ def get_item(self, content_id, seconds): if self.get_seconds_diff(query_result[1] or current_time) > seconds: return None - return json.loads(query_result[0]) + return query_result[0] def set_item(self, content_id, item): self._set(content_id, item) @@ -63,7 +63,7 @@ def remove(self, content_id): self._remove(content_id) def update(self, content_id, item): - self._set(str(content_id), json.dumps(item)) + self._set(str(content_id), item) def _optimize_item_count(self): pass diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index 0f9a263ad..ff4763b2b 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -21,7 +21,7 @@ def is_empty(self): @staticmethod def _process_item(item): - return item.split(',') + return item.strip('"').split(',') def get_items(self, keys): query_result = self._get_by_ids(keys, process=self._process_item) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index d8a41b7d1..b41898429 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -165,7 +165,7 @@ def _set(self, item_id, item): # add 1 microsecond, required for dbapi2 now = since_epoch(datetime.now()) + 0.000001 self._open() - self._execute(True, self._set_query, values=[item_id, + self._execute(True, self._set_query, values=[str(item_id), now, self._encode(item)]) self._close() @@ -176,7 +176,7 @@ def _set_all(self, items): now = since_epoch(datetime.now()) + 0.000001 self._open() self._execute(True, self._set_query, - values=[(key, now, self._encode(json.dumps(item))) + values=[(str(key), now, self._encode(item)) for key, item in items.items()], many=True) self._close() @@ -222,12 +222,13 @@ def _decode(obj, process=None): decoded_obj = pickle.loads(obj) if process: return process(decoded_obj) - return decoded_obj + return json.loads(decoded_obj) @staticmethod def _encode(obj): return sqlite3.Binary(pickle.dumps( - obj, protocol=pickle.HIGHEST_PROTOCOL + json.dumps(obj, ensure_ascii=False), + protocol=pickle.HIGHEST_PROTOCOL )) def _get(self, item_id): diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index b6874c97b..002267b0f 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -381,7 +381,7 @@ def helper(video_id, responses): # Truncate items to keep it manageable, and cache items = items[:500] - cache.set_item(cache_items_key, json.dumps(items)) + cache.set_item(cache_items_key, items) # Build the result set items.sort( @@ -438,7 +438,7 @@ def _sort_by_date_time(item): } """ # Update cache - cache.set_item(cache_home_key, json.dumps(payload)) + cache.set_item(cache_home_key, payload) # If there are no sorted_items we fall back to default API behaviour return payload @@ -884,7 +884,7 @@ def _sort_by_date_time(item): _result['items'].sort(reverse=True, key=_sort_by_date_time) # Update cache - cache.set_item(cache_items_key, json.dumps(_result['items'])) + cache.set_item(cache_items_key, _result['items']) """ no cache, get uploads data from web """ # trim result diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 2326c5f1b..3fc2bea5e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -733,7 +733,7 @@ def _get_player_js(self): return '' js_url = self._normalize_url(js_url) - self._data_cache.set_item('player_js_url', json_dumps({'url': js_url})) + self._data_cache.set_item('player_js_url', {'url': js_url}) js_cache_key = quote(js_url) cached = self._data_cache.get_item(js_cache_key, @@ -752,7 +752,7 @@ def _get_player_js(self): return '' javascript = result.text - self._data_cache.set_item(js_cache_key, json_dumps({'js': javascript})) + self._data_cache.set_item(js_cache_key, {'js': javascript}) return javascript @staticmethod @@ -938,8 +938,7 @@ def _process_signature_cipher(self, stream_map): 'Failed to extract URL from signatureCipher' ) return None - self._data_cache.set_item(encrypted_signature, - json_dumps({'sig': signature})) + self._data_cache.set_item(encrypted_signature, {'sig': signature}) if signature: url = '{0}&{1}={2}'.format(url, query_var, signature) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index f6d4cdb76..a233a7bc0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -125,7 +125,8 @@ def play_video(provider, context): 'refresh_only': screensaver } - ui.set_property('playback_json', json.dumps(playback_json)) + ui.set_property('playback_json', json.dumps(playback_json, + ensure_ascii=False)) context.send_notification('PlaybackInit', { 'video_id': video_id, 'channel_id': playback_json.get('channel_id', ''), From 5c278282f7ac188bdabe0cccc16342754b8d3132 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:48:23 +1100 Subject: [PATCH 104/141] Add context manager to progress dialogs --- .../youtube_plugin/kodion/ui/abstract_progress_dialog.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py index 0af140254..3406a7ca4 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py @@ -19,6 +19,12 @@ def __init__(self, dialog, heading, text, total=100): self._position = 1 self.update(steps=-1) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + def get_total(self): return self._total From e9ed03f81384ce5abb87dc04561e99c7e583a45e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:52:59 +1100 Subject: [PATCH 105/141] Fix incorrect date sorting after a3af85f - Sorting was messed up due to sorting based on localised date format - No default sorting for non-live listings, will use order from API - TODO: Ensure correct order is kept when using cache --- resources/lib/youtube_plugin/kodion/items/base_item.py | 4 ++-- resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py | 4 ++-- resources/lib/youtube_plugin/youtube/provider.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 44d8a89d3..2dee09a18 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -111,7 +111,7 @@ def set_date(self, year, month, day, hour=0, minute=0, second=0): def set_date_from_datetime(self, date_time): self._date = date_time - def get_date(self, as_text=True, short=False): + def get_date(self, as_text=False, short=False): if not self._date: return '' if short: @@ -131,7 +131,7 @@ def set_dateadded(self, year, month, day, hour=0, minute=0, second=0): def set_dateadded_from_datetime(self, date_time): self._dateadded = date_time - def get_dateadded(self, as_text=True): + def get_dateadded(self, as_text=False): if not self._dateadded: return '' if as_text: diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py index 674d2f2bc..0a2715bd4 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/info_labels.py @@ -93,7 +93,7 @@ def create_from_item(base_item): info_labels = {} # 'date' = '1982-03-09' (string) - _process_datetime_value(info_labels, 'date', base_item.get_date(as_text=False)) + _process_datetime_value(info_labels, 'date', base_item.get_date()) # 'count' = 12 (integer) # Can be used to store an id for later, or for sorting purposes @@ -138,7 +138,7 @@ def create_from_item(base_item): _process_list_value(info_labels, 'artist', base_item.get_artist()) # 'dateadded' = '2014-08-11 13:08:56' (string) will be taken from 'dateadded' - _process_datetime_value(info_labels, 'dateadded', base_item.get_dateadded(as_text=False)) + _process_datetime_value(info_labels, 'dateadded', base_item.get_dateadded()) # TODO: starting with Helix this could be seconds # 'duration' = '3:18' (string) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index d0ae46420..e6d5740ad 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -493,8 +493,7 @@ def _on_channel(self, context, re_match): if not v3.handle_error(context, json_data): return False - result.extend( - v3.response_to_items(self, context, json_data, sort=lambda x: x.get_date())) + result.extend(v3.response_to_items(self, context, json_data)) return result From 860defdfc00fc73c1e9f893a30e02b71175c9043 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:05:27 +1100 Subject: [PATCH 106/141] Use same response/error hooks for Youtube.perform_v3_request - Partially fixes #545 - Remove the other handle_error methods --- .../lib/youtube_plugin/kodion/utils/player.py | 11 +- .../youtube_plugin/youtube/client/youtube.py | 408 +++++++++++++----- .../lib/youtube_plugin/youtube/helper/v3.py | 34 -- .../youtube_plugin/youtube/helper/yt_play.py | 13 +- .../youtube/helper/yt_playlist.py | 10 +- .../youtube/helper/yt_specials.py | 18 +- .../youtube/helper/yt_subscriptions.py | 6 +- .../youtube_plugin/youtube/helper/yt_video.py | 2 +- .../lib/youtube_plugin/youtube/provider.py | 13 +- resources/lib/youtube_requests.py | 121 ++++-- 10 files changed, 422 insertions(+), 214 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index 830f299c6..8c3adc136 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -348,17 +348,11 @@ def run(self): json_data = client.remove_video_from_playlist( watch_later_id, playlist_item_id ) - _ = self.provider.v3_handle_error(self.provider, - self._context, - json_data) history_playlist_id = access_manager.get_watch_history_id() if history_playlist_id and history_playlist_id != 'HL': json_data = client.add_video_to_playlist(history_playlist_id, self.video_id) - _ = self.provider.v3_handle_error(self.provider, - self._context, - json_data) # rate video if settings.get_bool('youtube.post.play.rate', False): @@ -370,10 +364,7 @@ def run(self): if do_rating: json_data = client.get_video_rating(self.video_id) - success = self.provider.v3_handle_error(self.provider, - self._context, - json_data) - if success: + if json_data: items = json_data.get('items', [{'rating': 'none'}]) rating = items[0].get('rating', 'none') if rating == 'none': diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 002267b0f..741b56d52 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -11,15 +11,15 @@ from __future__ import absolute_import, division, unicode_literals import copy -import json import re import threading import xml.etree.ElementTree as ET from .login_client import LoginClient from ..helper.video_info import VideoInfo +from ..youtube_exceptions import InvalidJSON, YouTubeException from ...kodion import Context -from ...kodion.utils import datetime_parser, to_unicode +from ...kodion.utils import datetime_parser, strip_html_from_text, to_unicode _context = Context(plugin_id='plugin.video.youtube') @@ -153,52 +153,76 @@ def get_video_streams(self, context, video_id): return video_streams - def remove_playlist(self, playlist_id): + def remove_playlist(self, playlist_id, **kwargs): params = {'id': playlist_id, 'mine': 'true'} - return self.perform_v3_request(method='DELETE', path='playlists', params=params) + return self.perform_v3_request(method='DELETE', + path='playlists', + params=params, + **kwargs) - def get_supported_languages(self, language=None): + def get_supported_languages(self, language=None, **kwargs): _language = language if not _language: _language = self._language _language = _language.replace('-', '_') params = {'part': 'snippet', 'hl': _language} - return self.perform_v3_request(method='GET', path='i18nLanguages', params=params) + return self.perform_v3_request(method='GET', + path='i18nLanguages', + params=params, + **kwargs) - def get_supported_regions(self, language=None): + def get_supported_regions(self, language=None, **kwargs): _language = language if not _language: _language = self._language _language = _language.replace('-', '_') params = {'part': 'snippet', 'hl': _language} - return self.perform_v3_request(method='GET', path='i18nRegions', params=params) - - def rename_playlist(self, playlist_id, new_title, privacy_status='private'): + return self.perform_v3_request(method='GET', + path='i18nRegions', + params=params, + **kwargs) + + def rename_playlist(self, + playlist_id, + new_title, + privacy_status='private', + **kwargs): params = {'part': 'snippet,id,status'} post_data = {'kind': 'youtube#playlist', 'id': playlist_id, 'snippet': {'title': new_title}, 'status': {'privacyStatus': privacy_status}} - return self.perform_v3_request(method='PUT', path='playlists', params=params, post_data=post_data) + return self.perform_v3_request(method='PUT', + path='playlists', + params=params, + post_data=post_data, + **kwargs) - def create_playlist(self, title, privacy_status='private'): + def create_playlist(self, title, privacy_status='private', **kwargs): params = {'part': 'snippet,status'} post_data = {'kind': 'youtube#playlist', 'snippet': {'title': title}, 'status': {'privacyStatus': privacy_status}} - return self.perform_v3_request(method='POST', path='playlists', params=params, post_data=post_data) + return self.perform_v3_request(method='POST', + path='playlists', + params=params, + post_data=post_data, + **kwargs) - def get_video_rating(self, video_id): + def get_video_rating(self, video_id, **kwargs): if isinstance(video_id, list): video_id = ','.join(video_id) params = {'id': video_id} - return self.perform_v3_request(method='GET', path='videos/getRating', params=params) + return self.perform_v3_request(method='GET', + path='videos/getRating', + params=params, + **kwargs) - def rate_video(self, video_id, rating='like'): + def rate_video(self, video_id, rating='like', **kwargs): """ Rate a video :param video_id: if of the video @@ -207,34 +231,58 @@ def rate_video(self, video_id, rating='like'): """ params = {'id': video_id, 'rating': rating} - return self.perform_v3_request(method='POST', path='videos/rate', params=params) + return self.perform_v3_request(method='POST', + path='videos/rate', + params=params, + **kwargs) - def add_video_to_playlist(self, playlist_id, video_id): + def add_video_to_playlist(self, playlist_id, video_id, **kwargs): params = {'part': 'snippet', 'mine': 'true'} post_data = {'kind': 'youtube#playlistItem', 'snippet': {'playlistId': playlist_id, 'resourceId': {'kind': 'youtube#video', 'videoId': video_id}}} - return self.perform_v3_request(method='POST', path='playlistItems', params=params, post_data=post_data) + return self.perform_v3_request(method='POST', + path='playlistItems', + params=params, + post_data=post_data, + **kwargs) # noinspection PyUnusedLocal - def remove_video_from_playlist(self, playlist_id, playlist_item_id): + def remove_video_from_playlist(self, + playlist_id, + playlist_item_id, + **kwargs): params = {'id': playlist_item_id} - return self.perform_v3_request(method='DELETE', path='playlistItems', params=params) + return self.perform_v3_request(method='DELETE', + path='playlistItems', + params=params, + **kwargs) - def unsubscribe(self, subscription_id): + def unsubscribe(self, subscription_id, **kwargs): params = {'id': subscription_id} - return self.perform_v3_request(method='DELETE', path='subscriptions', params=params) + return self.perform_v3_request(method='DELETE', + path='subscriptions', + params=params, + **kwargs) - def subscribe(self, channel_id): + def subscribe(self, channel_id, **kwargs): params = {'part': 'snippet'} post_data = {'kind': 'youtube#subscription', 'snippet': {'resourceId': {'kind': 'youtube#channel', 'channelId': channel_id}}} - return self.perform_v3_request(method='POST', path='subscriptions', params=params, post_data=post_data) - - def get_subscription(self, channel_id, order='alphabetical', page_token=''): + return self.perform_v3_request(method='POST', + path='subscriptions', + params=params, + post_data=post_data, + **kwargs) + + def get_subscription(self, + channel_id, + order='alphabetical', + page_token='', + **kwargs): """ :param channel_id: [channel-id|'mine'] @@ -252,9 +300,12 @@ def get_subscription(self, channel_id, order='alphabetical', page_token=''): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='subscriptions', params=params) + return self.perform_v3_request(method='GET', + path='subscriptions', + params=params, + **kwargs) - def get_guide_category(self, guide_category_id, page_token=''): + def get_guide_category(self, guide_category_id, page_token='', **kwargs): params = {'part': 'snippet,contentDetails,brandingSettings', 'maxResults': str(self._max_results), 'categoryId': guide_category_id, @@ -262,9 +313,12 @@ def get_guide_category(self, guide_category_id, page_token=''): 'hl': self._language} if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='channels', params=params) + return self.perform_v3_request(method='GET', + path='channels', + params=params, + **kwargs) - def get_guide_categories(self, page_token=''): + def get_guide_categories(self, page_token='', **kwargs): params = {'part': 'snippet', 'maxResults': str(self._max_results), 'regionCode': self._region, @@ -272,9 +326,12 @@ def get_guide_categories(self, page_token=''): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='guideCategories', params=params) + return self.perform_v3_request(method='GET', + path='guideCategories', + params=params, + **kwargs) - def get_popular_videos(self, page_token=''): + def get_popular_videos(self, page_token='', **kwargs): params = {'part': 'snippet,status', 'maxResults': str(self._max_results), 'regionCode': self._region, @@ -282,9 +339,12 @@ def get_popular_videos(self, page_token=''): 'chart': 'mostPopular'} if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='videos', params=params) + return self.perform_v3_request(method='GET', + path='videos', + params=params, + **kwargs) - def get_video_category(self, video_category_id, page_token=''): + def get_video_category(self, video_category_id, page_token='', **kwargs): params = {'part': 'snippet,contentDetails,status', 'maxResults': str(self._max_results), 'videoCategoryId': video_category_id, @@ -293,9 +353,12 @@ def get_video_category(self, video_category_id, page_token=''): 'hl': self._language} if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='videos', params=params) + return self.perform_v3_request(method='GET', + path='videos', + params=params, + **kwargs) - def get_video_categories(self, page_token=''): + def get_video_categories(self, page_token='', **kwargs): params = {'part': 'snippet', 'maxResults': str(self._max_results), 'regionCode': self._region, @@ -303,7 +366,10 @@ def get_video_categories(self, page_token=''): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='videoCategories', params=params) + return self.perform_v3_request(method='GET', + path='videoCategories', + params=params, + **kwargs) def _get_recommendations_for_home(self): # YouTube has deprecated this API, so use history and related items to form @@ -443,7 +509,7 @@ def _sort_by_date_time(item): # If there are no sorted_items we fall back to default API behaviour return payload - def get_activities(self, channel_id, page_token=''): + def get_activities(self, channel_id, page_token='', **kwargs): params = {'part': 'snippet,contentDetails', 'maxResults': str(self._max_results), 'regionCode': self._region, @@ -462,9 +528,12 @@ def get_activities(self, channel_id, page_token=''): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='activities', params=params) + return self.perform_v3_request(method='GET', + path='activities', + params=params, + **kwargs) - def get_channel_sections(self, channel_id): + def get_channel_sections(self, channel_id, **kwargs): params = {'part': 'snippet,contentDetails', 'regionCode': self._region, 'hl': self._language} @@ -472,9 +541,12 @@ def get_channel_sections(self, channel_id): params['mine'] = 'true' else: params['channelId'] = channel_id - return self.perform_v3_request(method='GET', path='channelSections', params=params) + return self.perform_v3_request(method='GET', + path='channelSections', + params=params, + **kwargs) - def get_playlists_of_channel(self, channel_id, page_token=''): + def get_playlists_of_channel(self, channel_id, page_token='', **kwargs): params = {'part': 'snippet', 'maxResults': str(self._max_results)} if channel_id != 'mine': @@ -484,7 +556,10 @@ def get_playlists_of_channel(self, channel_id, page_token=''): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='playlists', params=params) + return self.perform_v3_request(method='GET', + path='playlists', + params=params, + **kwargs) def get_playlist_item_id_of_video_id(self, playlist_id, video_id, page_token=''): old_max_results = self._max_results @@ -506,7 +581,11 @@ def get_playlist_item_id_of_video_id(self, playlist_id, video_id, page_token='') return None - def get_playlist_items(self, playlist_id, page_token='', max_results=None): + def get_playlist_items(self, + playlist_id, + page_token='', + max_results=None, + **kwargs): # prepare params max_results = str(self._max_results) if max_results is None else str(max_results) params = {'part': 'snippet', @@ -515,9 +594,12 @@ def get_playlist_items(self, playlist_id, page_token='', max_results=None): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='playlistItems', params=params) + return self.perform_v3_request(method='GET', + path='playlistItems', + params=params, + **kwargs) - def get_channel_by_username(self, username): + def get_channel_by_username(self, username, **kwargs): """ Returns a collection of zero or more channel resources that match the request criteria. :param username: retrieve channel_id for username @@ -529,9 +611,12 @@ def get_channel_by_username(self, username): else: params.update({'forUsername': username}) - return self.perform_v3_request(method='GET', path='channels', params=params) + return self.perform_v3_request(method='GET', + path='channels', + params=params, + **kwargs) - def get_channels(self, channel_id): + def get_channels(self, channel_id, **kwargs): """ Returns a collection of zero or more channel resources that match the request criteria. :param channel_id: list or comma-separated list of the YouTube channel ID(s) @@ -545,9 +630,12 @@ def get_channels(self, channel_id): params['id'] = channel_id else: params['mine'] = 'true' - return self.perform_v3_request(method='GET', path='channels', params=params) + return self.perform_v3_request(method='GET', + path='channels', + params=params, + **kwargs) - def get_disliked_videos(self, page_token=''): + def get_disliked_videos(self, page_token='', **kwargs): # prepare page token if not page_token: page_token = '' @@ -559,9 +647,12 @@ def get_disliked_videos(self, page_token=''): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='videos', params=params) + return self.perform_v3_request(method='GET', + path='videos', + params=params, + **kwargs) - def get_videos(self, video_id, live_details=False): + def get_videos(self, video_id, live_details=False, **kwargs): """ Returns a list of videos that match the API request parameters :param video_id: list of video ids @@ -577,19 +668,29 @@ def get_videos(self, video_id, live_details=False): params = {'part': ''.join(parts), 'id': video_id} - return self.perform_v3_request(method='GET', path='videos', params=params) + return self.perform_v3_request(method='GET', + path='videos', + params=params, + **kwargs) - def get_playlists(self, playlist_id): + def get_playlists(self, playlist_id, **kwargs): if isinstance(playlist_id, list): playlist_id = ','.join(playlist_id) params = {'part': 'snippet,contentDetails', 'id': playlist_id} - return self.perform_v3_request(method='GET', path='playlists', params=params) - - def get_live_events(self, event_type='live', order='relevance', page_token='', location=False): + return self.perform_v3_request(method='GET', + path='playlists', + params=params, + **kwargs) + + def get_live_events(self, + event_type='live', + order='relevance', + page_token='', + location=False, + **kwargs): """ - :param event_type: one of: 'live', 'completed', 'upcoming' :param order: one of: 'date', 'rating', 'relevance', 'title', 'videoCount', 'viewCount' :param page_token: @@ -619,9 +720,16 @@ def get_live_events(self, event_type='live', order='relevance', page_token='', l if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='search', params=params) + return self.perform_v3_request(method='GET', + path='search', + params=params, + **kwargs) - def get_related_videos(self, video_id, page_token='', max_results=0): + def get_related_videos(self, + video_id, + page_token='', + max_results=0, + **kwargs): # prepare page token if not page_token: page_token = '' @@ -638,9 +746,16 @@ def get_related_videos(self, video_id, page_token='', max_results=0): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='search', params=params) + return self.perform_v3_request(method='GET', + path='search', + params=params, + **kwargs) - def get_parent_comments(self, video_id, page_token='', max_results=0): + def get_parent_comments(self, + video_id, + page_token='', + max_results=0, + **kwargs): max_results = self._max_results if max_results <= 0 else max_results # prepare params @@ -652,9 +767,17 @@ def get_parent_comments(self, video_id, page_token='', max_results=0): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='commentThreads', params=params, no_login=True) - - def get_child_comments(self, parent_id, page_token='', max_results=0): + return self.perform_v3_request(method='GET', + path='commentThreads', + params=params, + no_login=True, + **kwargs) + + def get_child_comments(self, + parent_id, + page_token='', + max_results=0, + **kwargs): max_results = self._max_results if max_results <= 0 else max_results # prepare params @@ -665,9 +788,13 @@ def get_child_comments(self, parent_id, page_token='', max_results=0): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='comments', params=params, no_login=True) + return self.perform_v3_request(method='GET', + path='comments', + params=params, + no_login=True, + **kwargs) - def get_channel_videos(self, channel_id, page_token=''): + def get_channel_videos(self, channel_id, page_token='', **kwargs): """ Returns a collection of video search results for the specified channel_id """ @@ -687,10 +814,21 @@ def get_channel_videos(self, channel_id, page_token=''): if page_token: params['pageToken'] = page_token - return self.perform_v3_request(method='GET', path='search', params=params) - - def search(self, q, search_type=None, event_type='', channel_id='', - order='relevance', safe_search='moderate', page_token='', location=False): + return self.perform_v3_request(method='GET', + path='search', + params=params, + **kwargs) + + def search(self, + q, + search_type=None, + event_type='', + channel_id='', + order='relevance', + safe_search='moderate', + page_token='', + location=False, + **kwargs): """ Returns a collection of search results that match the query parameters specified in the API request. By default, a search result set identifies matching video, channel, and playlist resources, but you can also configure @@ -754,9 +892,12 @@ def search(self, q, search_type=None, event_type='', channel_id='', params['location'] = location params['locationRadius'] = _context.get_settings().get_location_radius() - return self.perform_v3_request(method='GET', path='search', params=params) + return self.perform_v3_request(method='GET', + path='search', + params=params, + **kwargs) - def get_my_subscriptions(self, page_token=None, offset=0): + def get_my_subscriptions(self, page_token=None, offset=0, **kwargs): """ modified by PureHemp, using YouTube RSS for fetching latest videos """ @@ -805,19 +946,22 @@ def _perform(_page_token, _offset, _result): if sub_page_token: params['pageToken'] = sub_page_token - sub_json_data = self.perform_v3_request(method='GET', path='subscriptions', params=params) + json_data = self.perform_v3_request(method='GET', + path='subscriptions', + params=params, + **kwargs) - if not sub_json_data: - sub_json_data = {} + if not json_data: + json_data = {} - items = sub_json_data.get('items', []) + items = json_data.get('items', []) for item in items: item = item.get('snippet', {}).get('resourceId', {}).get('channelId', '') sub_channel_ids.append(item) # get next token if exists - sub_page_token = sub_json_data.get('nextPageToken', '') + sub_page_token = json_data.get('nextPageToken', '') # terminate loop when last page if not sub_page_token: @@ -1045,19 +1189,82 @@ def _perform(_playlist_idx, _page_token, _offset, _result): return result - def perform_v3_request(self, method='GET', headers=None, path=None, - post_data=None, params=None, no_login=False): + @staticmethod + def _response_hook(**kwargs): + response = kwargs['response'] + _context.log_debug('[data] v3 response: |{0.status_code}|\n' + '\theaders: |{0.headers}|'.format(response)) + try: + json_data = response.json() + if 'error' in json_data: + raise YouTubeException('"error" in response JSON data', + json_data=json_data, + **kwargs) + except ValueError as error: + raise InvalidJSON(error, **kwargs) + response.raise_for_status() + return json_data + @staticmethod + def _error_hook(**kwargs): + exc = kwargs['exc'] + json_data = getattr(exc, 'json_data', None) + data = getattr(exc, 'pass_data', False) and json_data + exception = getattr(exc, 'raise_exc', False) and YouTubeException + + if not json_data or 'error' not in json_data: + return None, None, None, data, None, exception + + details = json_data['error'] + reason = details.get('errors', [{}])[0].get('reason', 'Unknown') + message = strip_html_from_text(details.get('message', 'Unknown error')) + + notify = getattr(exc, 'notify', True) + if notify: + ok_dialog = False + timeout = 5000 + if reason == 'accessNotConfigured': + notification = _context.localize('key.requirement.notification') + ok_dialog = True + elif reason == 'keyInvalid' and message == 'Bad Request': + notification = _context.localize('api.key.incorrect') + timeout = 7000 + elif reason in ('quotaExceeded', 'dailyLimitExceeded'): + notification = message + timeout = 7000 + else: + notification = message + + title = '{0}: {1}'.format(_context.get_name(), reason) + if ok_dialog: + _context.get_ui().on_ok(title, notification) + else: + _context.get_ui().show_notification(notification, + title, + time_ms=timeout) + + info = ('[data] v3 error: {reason}\n' + '\texc: |{exc}|\n' + '\tmessage: |{message}|') + details = {'reason': reason, 'message': message} + return '', info, details, data, False, exception + + def perform_v3_request(self, method='GET', headers=None, path=None, + post_data=None, params=None, no_login=False, + **kwargs): # params _params = {} # headers _headers = {'Host': 'www.googleapis.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.36 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64)' + ' AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/39.0.2171.36 Safari/537.36', 'Accept-Encoding': 'gzip, deflate'} # a config can decide if a token is allowed - if self._access_token and self._config.get('token-allowed', True) and not no_login: + if (not no_login and self._access_token + and self._config.get('token-allowed', True)): _headers['Authorization'] = 'Bearer %s' % self._access_token else: _params['key'] = self._config_tv['key'] @@ -1074,23 +1281,26 @@ def perform_v3_request(self, method='GET', headers=None, path=None, log_params['location'] = 'xx.xxxx,xx.xxxx' else: log_params = None - _context.log_debug('[data] v3 request: |{0}| path: |{1}| params: |{2}| post_data: |{3}|'.format(method, path, log_params, post_data)) - - result = self.request(_url, method=method, headers=_headers, json=post_data, params=_params) - if result is None: - return {} - - _context.log_debug('[data] v3 response: |{0}| headers: |{1}|'.format(result.status_code, result.headers)) - if result.headers.get('content-type', '').startswith('application/json'): - try: - return result.json() - except ValueError: - return { - 'status_code': result.status_code, - 'payload': result.text - } - return {} + _context.log_debug('[data] v3 request: |{method}|\n' + '\tpath: |{path}|\n' + '\tparams: |{params}|\n' + '\tpost_data: |{data}|\n' + '\theaders: |{headers}|'.format(method=method, + path=path, + params=log_params, + data=post_data, + headers=_headers)) + + json_data = self.request(_url, + method=method, + headers=_headers, + json=post_data, + params=_params, + response_hook=self._response_hook, + response_hook_kwargs=kwargs, + error_hook=self._error_hook) + return json_data def perform_v1_tv_request(self, method='GET', headers=None, path=None, post_data=None, params=None, no_login=False): diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index e0e66f463..295c25efa 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -24,7 +24,6 @@ from ..helper import yt_context_menu from ...kodion import KodionException from ...kodion.items import DirectoryItem, NextPageItem, VideoItem -from ...kodion.utils import strip_html_from_text def _process_list_response(provider, context, json_data): @@ -434,39 +433,6 @@ def response_to_items(provider, context, json_data, sort=None, reverse=False, pr return result -def handle_error(context, json_data): - if json_data and 'error' in json_data: - ok_dialog = False - message_timeout = 5000 - - message = strip_html_from_text(json_data['error'].get('message', '')) - log_message = strip_html_from_text(json_data['error'].get('message', '')) - reason = json_data['error']['errors'][0].get('reason', '') - title = '%s: %s' % (context.get_name(), reason) - - context.log_error('Error reason: |%s| with message: |%s|' % (reason, log_message)) - - if reason == 'accessNotConfigured': - message = context.localize('key.requirement.notification') - ok_dialog = True - - if reason == 'keyInvalid' and message == 'Bad Request': - message = context.localize('api.key.incorrect') - message_timeout = 7000 - - if reason in {'quotaExceeded', 'dailyLimitExceeded'}: - message_timeout = 7000 - - if ok_dialog: - context.get_ui().on_ok(title, message) - else: - context.get_ui().show_notification(message, title, time_ms=message_timeout) - - return False - - return True - - def _parse_kind(item): parts = item.get('kind', '').split('#') is_youtube = parts[0] == 'youtube' diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index a233a7bc0..92ad9b9b1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -246,11 +246,18 @@ def play_channel_live(provider, context): index = context.get_param('live') - 1 if index < 0: index = 0 - json_data = provider.get_client(context).search(q='', search_type='video', event_type='live', channel_id=channel_id, safe_search=False) - if not v3.handle_error(context, json_data): + json_data = provider.get_client(context).search(q='', + search_type='video', + event_type='live', + channel_id=channel_id, + safe_search=False) + if not json_data: return False - video_items = v3.response_to_items(provider, context, json_data, process_next_page=False) + video_items = v3.response_to_items(provider, + context, + json_data, + process_next_page=False) try: video_item = video_items[index] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index ffb69fdb3..b5fbacca6 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -38,7 +38,7 @@ def _process_add_video(provider, context, keymap_action=False): if playlist_id != 'HL': json_data = client.add_video_to_playlist(playlist_id=playlist_id, video_id=video_id) - if not v3.handle_error(context, json_data): + if not json_data: return False if playlist_id == watch_later_id: @@ -96,7 +96,7 @@ def _process_remove_video(provider, context): if context.get_ui().on_remove_content(video_name): json_data = provider.get_client(context).remove_video_from_playlist(playlist_id=playlist_id, playlist_item_id=video_id) - if not v3.handle_error(context, json_data): + if not json_data: return False context.get_ui().refresh_container() @@ -128,7 +128,7 @@ def _process_remove_playlist(provider, context): if context.get_ui().on_delete_content(playlist_name): json_data = provider.get_client(context).remove_playlist(playlist_id=playlist_id) - if not v3.handle_error(context, json_data): + if not json_data: return False context.get_ui().refresh_container() @@ -203,7 +203,7 @@ def _process_select_playlist(provider, context): context.localize('playlist.create')) if result and text: json_data = client.create_playlist(title=text) - if not v3.handle_error(context, json_data): + if not json_data: break playlist_id = json_data.get('id', '') @@ -236,7 +236,7 @@ def _process_rename_playlist(provider, context): default=current_playlist_name) if result and text: json_data = provider.get_client(context).rename_playlist(playlist_id=playlist_id, new_title=text) - if not v3.handle_error(context, json_data): + if not json_data: return context.get_ui().refresh_container() diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index ea2e29a1a..5a4456ba8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -33,7 +33,7 @@ def _process_related_videos(provider, context): json_data = provider.get_client(context).get_related_videos( video_id=video_id, page_token=context.get_param('page_token', '') ) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, @@ -50,7 +50,7 @@ def _process_parent_comments(provider, context): json_data = provider.get_client(context).get_parent_comments( video_id=video_id, page_token=context.get_param('page_token', '') ) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data) @@ -64,7 +64,7 @@ def _process_child_comments(provider, context): json_data = provider.get_client(context).get_child_comments( parent_id=parent_id, page_token=context.get_param('page_token', '') ) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data) @@ -74,7 +74,7 @@ def _process_recommendations(provider, context): json_data = provider.get_client(context).get_activities( channel_id='home', page_token=context.get_param('page_token', '') ) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data) @@ -84,7 +84,7 @@ def _process_popular_right_now(provider, context): json_data = provider.get_client(context).get_popular_videos( page_token=context.get_param('page_token', '') ) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data) @@ -95,14 +95,14 @@ def _process_browse_channels(provider, context): guide_id = context.get_param('guide_id', '') if guide_id: json_data = client.get_guide_category(guide_id) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data) function_cache = context.get_function_cache() json_data = function_cache.get(client.get_guide_categories, function_cache.ONE_MONTH) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data) @@ -112,7 +112,7 @@ def _process_disliked_videos(provider, context): json_data = provider.get_client(context).get_disliked_videos( page_token=context.get_param('page_token', '') ) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data) @@ -128,7 +128,7 @@ def _sort(x): page_token=context.get_param('page_token', ''), location=context.get_param('location', False), ) - if not v3.handle_error(context, json_data): + if not json_data: return False return v3.response_to_items(provider, context, json_data, sort=_sort) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index c90893947..39e81309b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -21,7 +21,7 @@ def _process_list(provider, context): page_token = context.get_param('page_token', '') # no caching json_data = provider.get_client(context).get_subscription('mine', page_token=page_token) - if not v3.handle_error(context, json_data): + if not json_data: return [] result.extend(v3.response_to_items(provider, context, json_data)) @@ -38,7 +38,7 @@ def _process_add(provider, context): if subscription_id: json_data = provider.get_client(context).subscribe(subscription_id) - if not v3.handle_error(context, json_data): + if not json_data: return False context.get_ui().show_notification( @@ -61,7 +61,7 @@ def _process_remove(provider, context): if subscription_id: json_data = provider.get_client(context).unsubscribe(subscription_id) - if not v3.handle_error(context, json_data): + if not json_data: return False context.get_ui().refresh_container() diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 63c517875..9ce0f8523 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -42,7 +42,7 @@ def _process_rate_video(provider, context, re_match): if not current_rating: client = provider.get_client(context) json_data = client.get_video_rating(video_id) - if not v3.handle_error(context, json_data): + if not json_data: return False items = json_data.get('items', []) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index e6d5740ad..4bdc07206 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -49,7 +49,6 @@ def __init__(self): self._client = None self._is_logged_in = False - self.v3_handle_error = v3.handle_error self.yt_video = yt_video def get_wizard_supported_views(self): @@ -354,7 +353,7 @@ def _on_channel_playlists(self, context, re_match): # no caching json_data = self.get_client(context).get_playlists_of_channel(channel_id, page_token) - if not v3.handle_error(context, json_data): + if not json_data: return False result.extend(v3.response_to_items(self, context, json_data)) @@ -377,7 +376,7 @@ def _on_channel_live(self, context, re_match): # no caching json_data = self.get_client(context).search(q='', search_type='video', event_type='live', channel_id=channel_id, page_token=page_token, safe_search=safe_search) - if not v3.handle_error(context, json_data): + if not json_data: return False result.extend(v3.response_to_items(self, context, json_data)) @@ -428,7 +427,7 @@ def _on_channel(self, context, re_match): json_data = function_cache.get(client.get_channel_by_username, function_cache.ONE_DAY, channel_id) - if not v3.handle_error(context, json_data): + if not json_data: return False # we correct the channel id based on the username @@ -490,7 +489,7 @@ def _on_channel(self, context, re_match): function_cache.ONE_MINUTE * 5, upload_playlist, page_token=page_token) - if not v3.handle_error(context, json_data): + if not json_data: return False result.extend(v3.response_to_items(self, context, json_data)) @@ -821,7 +820,7 @@ def _search_channel_or_playlist(self, context, id_string): elif re.match(r'[OP]L[0-9a-zA-Z_\-]{30,40}', id_string): json_data = self.get_client(context).get_playlists(id_string) - if not json_data or not v3.handle_error(context, json_data): + if not json_data: return [] result.extend(v3.response_to_items(self, context, json_data)) @@ -893,7 +892,7 @@ def on_search(self, search_text, context, re_match): page_token=page_token, channel_id=channel_id, location=location) - if not v3.handle_error(context, json_data): + if not json_data: return False result.extend(v3.response_to_items(self, context, json_data)) return result diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index b25204c74..391018b8f 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -30,17 +30,6 @@ def __get_core_components(addon_id=None): return provider, context, client -def handle_error(context, json_data): - if json_data and 'error' in json_data: - message = json_data['error'].get('message', '') - reason = json_data['error']['errors'][0].get('reason', '') - context.log_error('Error reason: |%s| with message: |%s|' % (reason, message)) - - return False - - return True - - def v3_request(method='GET', headers=None, path=None, post_data=None, params=None, addon_id=None): """ https://developers.google.com/youtube/v3/docs/ @@ -53,7 +42,14 @@ def v3_request(method='GET', headers=None, path=None, post_data=None, params=Non :type addon_id: str """ provider, context, client = __get_core_components(addon_id) - return client.perform_v3_request(method=method, headers=headers, path=path, post_data=post_data, params=params) + return client.perform_v3_request(method=method, + headers=headers, + path=path, + post_data=post_data, + params=params, + notify=False, + pass_data=True, + raise_exc=False) def _append_missing_page_token(items): @@ -76,11 +72,14 @@ def get_videos(video_id, addon_id=None): """ provider, context, client = __get_core_components(addon_id) - json_data = client.get_videos(video_id) - if not handle_error(context, json_data): + json_data = client.get_videos(video_id, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - return json_data.get('items', []) + return json_data.get('items', [{}]) def get_activities(channel_id, page_token='', all_pages=False, addon_id=None): @@ -103,11 +102,15 @@ def get_activities(channel_id, page_token='', all_pages=False, addon_id=None): items = [] def get_items(_page_token=''): - json_data = client.get_activities(channel_id, page_token=_page_token) - if not handle_error(context, json_data): + json_data = client.get_activities(channel_id, + page_token=_page_token, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - items.extend(json_data.get('items', [])) + items.extend(json_data.get('items', [{}])) error = False next_page_token = json_data.get('nextPageToken') @@ -148,11 +151,15 @@ def get_playlist_items(playlist_id, page_token='', all_pages=False, addon_id=Non items = [] def get_items(_page_token=''): - json_data = client.get_playlist_items(playlist_id, page_token=_page_token) - if not handle_error(context, json_data): + json_data = client.get_playlist_items(playlist_id, + page_token=_page_token, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - items.extend(json_data.get('items', [])) + items.extend(json_data.get('items', [{}])) error = False next_page_token = json_data.get('nextPageToken') @@ -185,11 +192,14 @@ def get_channel_id(channel_name, addon_id=None): """ provider, context, client = __get_core_components(addon_id) - json_data = client.get_channel_by_username(channel_name) - if not handle_error(context, json_data): + json_data = client.get_channel_by_username(channel_name, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - return json_data.get('items', []) + return json_data.get('items', [{}]) def get_channels(channel_id, addon_id=None): @@ -205,11 +215,14 @@ def get_channels(channel_id, addon_id=None): """ provider, context, client = __get_core_components(addon_id) - json_data = client.get_channels(channel_id) - if not handle_error(context, json_data): + json_data = client.get_channels(channel_id, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - return json_data.get('items', []) + return json_data.get('items', [{}]) def get_channel_sections(channel_id, addon_id=None): @@ -225,11 +238,14 @@ def get_channel_sections(channel_id, addon_id=None): """ provider, context, client = __get_core_components(addon_id) - json_data = client.get_channel_sections(channel_id) - if not handle_error(context, json_data): + json_data = client.get_channel_sections(channel_id, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - return json_data.get('items', []) + return json_data.get('items', [{}]) def get_playlists_of_channel(channel_id, page_token='', all_pages=False, addon_id=None): @@ -253,11 +269,15 @@ def get_playlists_of_channel(channel_id, page_token='', all_pages=False, addon_i items = [] def get_items(_page_token=''): - json_data = client.get_playlists_of_channel(channel_id, page_token=_page_token) - if not handle_error(context, json_data): + json_data = client.get_playlists_of_channel(channel_id, + page_token=_page_token, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - items.extend(json_data.get('items', [])) + items.extend(json_data.get('items', [{}])) error = False next_page_token = json_data.get('nextPageToken') @@ -290,11 +310,14 @@ def get_playlists(playlist_id, addon_id=None): """ provider, context, client = __get_core_components(addon_id) - json_data = client.get_playlists(playlist_id) - if not handle_error(context, json_data): + json_data = client.get_playlists(playlist_id, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - return json_data.get('items', []) + return json_data.get('items', [{}]) def get_related_videos(video_id, page_token='', addon_id=None): @@ -317,11 +340,15 @@ def get_related_videos(video_id, page_token='', addon_id=None): items = [] def get_items(_page_token=''): - json_data = client.get_related_videos(video_id, page_token=_page_token) - if not handle_error(context, json_data): + json_data = client.get_related_videos(video_id, + page_token=_page_token, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - items.extend([item for item in json_data.get('items', []) + items.extend([item for item in json_data.get('items', [{}]) if 'snippet' in item]) error = False @@ -369,12 +396,20 @@ def get_search(q, search_type='', event_type='', channel_id='', order='relevance items = [] def get_items(_page_token=''): - json_data = client.search(q, search_type=search_type, event_type=event_type, channel_id=channel_id, - order=order, safe_search=safe_search, page_token=_page_token) - if not handle_error(context, json_data): + json_data = client.search(q, + search_type=search_type, + event_type=event_type, + channel_id=channel_id, + order=order, + safe_search=safe_search, + page_token=_page_token, + notify=False, + pass_data=True, + raise_exc=False) + if not json_data or 'error' in json_data: return [json_data] - items.extend(json_data.get('items', [])) + items.extend(json_data.get('items', [{}])) error = False next_page_token = json_data.get('nextPageToken') From b209931f4c570f77203d6c15dd262f4e6b070a82 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:14:17 +1100 Subject: [PATCH 107/141] Add caching of playlistitems requests - Partially fixes #545 - Results will be cached for 1 hour - Also re-sorts cached values so they match the original requested order --- .../youtube/helper/resource_manager.py | 431 ++++++++++-------- .../youtube_plugin/youtube/helper/yt_play.py | 129 +++--- .../lib/youtube_plugin/youtube/provider.py | 37 +- 3 files changed, 302 insertions(+), 295 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 60e3aa4b4..8e29b9ece 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -10,180 +10,220 @@ from __future__ import absolute_import, division, unicode_literals -from ..youtube_exceptions import YouTubeException -from ...kodion.utils import strip_html_from_text - class ResourceManager(object): def __init__(self, context, client): self._context = context self._client = client - self._channel_data = {} - self._video_data = {} - self._playlist_data = {} - self._enable_channel_fanart = context.get_settings().get_bool('youtube.channel.fanart.show', True) + self._data_cache = context.get_data_cache() + self._func_cache = context.get_function_cache() + self._show_fanart = context.get_settings().get_bool( + 'youtube.channel.fanart.show', True + ) + + @staticmethod + def _list_batch(input_list, n=50): + if not isinstance(input_list, (list, tuple)): + input_list = list(input_list) + for i in range(0, len(input_list), n): + yield input_list[i:i + n] def clear(self): - self._context.get_function_cache().clear() - self._context.get_data_cache().clear() - - def _get_channel_data(self, channel_id): - return self._channel_data.get(channel_id, {}) - - def _get_video_data(self, video_id): - return self._video_data.get(video_id, {}) - - def _get_playlist_data(self, playlist_id): - return self._playlist_data.get(playlist_id, {}) - - def _update_channels(self, channel_ids): - json_data = None - updated_channel_ids = [] - function_cache = self._context.get_function_cache() - - for channel_id in channel_ids: - if channel_id == 'mine': - json_data = function_cache.get(self._client.get_channel_by_username, - function_cache.ONE_DAY, - channel_id) - items = json_data.get('items', [{'id': 'mine'}]) - - try: - channel_id = items[0]['id'] - except IndexError: - self._context.log_debug('Channel "mine" not found: %s' % json_data) - channel_id = None - - json_data = None - - if channel_id: - updated_channel_ids.append(channel_id) - - channel_ids = updated_channel_ids - - data_cache = self._context.get_data_cache() - channel_data = data_cache.get_items(channel_ids, data_cache.ONE_MONTH) - - channel_ids = set(channel_ids) - channel_ids_cached = set(channel_data) - channel_ids_to_update = channel_ids - channel_ids_cached - channel_ids_cached = channel_ids & channel_ids_cached - - result = channel_data - if channel_ids_cached: - self._context.log_debug('Found cached data for channels |%s|' % ', '.join(channel_ids_cached)) - - if channel_ids_to_update: - self._context.log_debug('No data for channels |%s| cached' % ', '.join(channel_ids_to_update)) - json_data = [ - self._client.get_channels(list_of_50) - for list_of_50 in self._list_batch(channel_ids_to_update, n=50) - ] - channel_data = { + self._func_cache.clear() + self._data_cache.clear() + + def get_channels(self, ids): + updated = [] + for channel_id in ids: + if not channel_id: + continue + + if channel_id != 'mine': + updated.append(channel_id) + continue + + data = self._func_cache.get(self._client.get_channel_by_username, + self._func_cache.ONE_DAY, + channel_id) + items = data.get('items', [{'id': 'mine'}]) + + try: + channel_id = items[0]['id'] + updated.append(channel_id) + except IndexError: + self._context.log_error('Channel not found:\n\t{data}' + .format(data=data)) + + ids = updated + result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH) + to_update = [id_ for id_ in ids if id_ not in result] + + if result: + self._context.log_debug('Found cached data for channels:\n|{ids}|' + .format(ids=list(result))) + + if to_update: + new_data = [self._client.get_channels(list_of_50) + for list_of_50 in self._list_batch(to_update, n=50)] + if not any(new_data): + new_data = None + else: + new_data = None + + if new_data: + self._context.log_debug('Got data for channels:\n|{ids}|' + .format(ids=to_update)) + new_data = { yt_item['id']: yt_item - for batch in json_data + for batch in new_data for yt_item in batch.get('items', []) if yt_item } - result.update(channel_data) - data_cache.set_items(channel_data) - self._context.log_debug('Cached data for channels |%s|' % ', '.join(channel_data)) - - if self.handle_error(json_data): - return result - return {} - - def _update_videos(self, video_ids, live_details=False, suppress_errors=False): - json_data = None - data_cache = self._context.get_data_cache() - video_data = data_cache.get_items(video_ids, data_cache.ONE_MONTH) - - video_ids = set(video_ids) - video_ids_cached = set(video_data) - video_ids_to_update = video_ids - video_ids_cached - video_ids_cached = video_ids & video_ids_cached - - result = video_data - if video_ids_cached: - self._context.log_debug('Found cached data for videos |%s|' % ', '.join(video_ids_cached)) - - if video_ids_to_update: - self._context.log_debug('No data for videos |%s| cached' % ', '.join(video_ids_to_update)) - json_data = self._client.get_videos(video_ids_to_update, live_details) - video_data = dict.fromkeys(video_ids_to_update, {}) - video_data.update({ - yt_item['id']: yt_item or {} - for yt_item in json_data.get('items', []) - }) - result.update(video_data) - data_cache.set_items(video_data) - self._context.log_debug('Cached data for videos |%s|' % ', '.join(video_data)) - - if self._context.get_settings().use_local_history(): - playback_history = self._context.get_playback_history() - played_items = playback_history.get_items(video_ids) - for video_id, play_data in played_items.items(): - result[video_id]['play_data'] = play_data + result.update(new_data) + self._data_cache.set_items(new_data) + self._context.log_debug('Cached data for channels:\n|{ids}|' + .format(ids=list(new_data))) + + # Re-sort result to match order of requested IDs + # Will only work in Python v3.7+ + if list(result) != ids: + result = { + id: result[id] + for id in ids + if id in result + } - if self.handle_error(json_data, suppress_errors) or suppress_errors: - return result - return {} + return result - @staticmethod - def _list_batch(input_list, n=50): - if not isinstance(input_list, (list, tuple)): - input_list = list(input_list) - for i in range(0, len(input_list), n): - yield input_list[i:i + n] + def get_fanarts(self, channel_ids): + if not self._show_fanart: + return {} - def get_videos(self, video_ids, live_details=False, suppress_errors=False): - list_of_50s = self._list_batch(video_ids, n=50) + result = self.get_channels(channel_ids) + banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', + 'bannerTvImageUrl', 'bannerExternalUrl'] + # transform + for key, item in result.items(): + images = item.get('brandingSettings', {}).get('image', {}) + for banner in banners: + image = images.get(banner) + if not image: + continue + result[key] = image + break + else: + # set an empty url + result[key] = '' - result = {} - for list_of_50 in list_of_50s: - result.update(self._update_videos(list_of_50, live_details, suppress_errors)) return result - def _update_playlists(self, playlists_ids): - json_data = None - data_cache = self._context.get_data_cache() - playlist_data = data_cache.get_items(playlists_ids, data_cache.ONE_MONTH) + def get_playlists(self, ids): + result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH) + to_update = [id_ for id_ in ids if id_ not in result] - playlists_ids = set(playlists_ids) - playlists_ids_cached = set(playlist_data) - playlist_ids_to_update = playlists_ids - playlists_ids_cached - playlists_ids_cached = playlists_ids & playlists_ids_cached + if result: + self._context.log_debug('Found cached data for playlists:\n|{ids}|' + .format(ids=list(result))) - result = playlist_data - if playlists_ids_cached: - self._context.log_debug('Found cached data for playlists |%s|' % ', '.join(playlists_ids_cached)) + if to_update: + new_data = [self._client.get_playlists(list_of_50) + for list_of_50 in self._list_batch(to_update, n=50)] + if not any(new_data): + new_data = None + else: + new_data = None - if playlist_ids_to_update: - self._context.log_debug('No data for playlists |%s| cached' % ', '.join(playlist_ids_to_update)) - json_data = self._client.get_playlists(playlist_ids_to_update) - playlist_data = { + if new_data: + self._context.log_debug('Got data for playlists:\n|{ids}|' + .format(ids=to_update)) + new_data = { yt_item['id']: yt_item - for yt_item in json_data.get('items', []) + for batch in new_data + for yt_item in batch.get('items', []) if yt_item } - result.update(playlist_data) - data_cache.set_items(playlist_data) - self._context.log_debug('Cached data for playlists |%s|' % ', '.join(playlist_data)) + result.update(new_data) + self._data_cache.set_items(new_data) + self._context.log_debug('Cached data for playlists:\n|{ids}|' + .format(ids=list(new_data))) + + # Re-sort result to match order of requested IDs + # Will only work in Python v3.7+ + if list(result) != ids: + result = { + id: result[id] + for id in ids + if id in result + } - if self.handle_error(json_data): - return result - return {} + return result - def get_playlists(self, playlists_ids): - list_of_50s = self._list_batch(playlists_ids, n=50) + def get_playlist_items(self, ids=None, batch_id=None): + if not ids and not batch_id: + return None + if batch_id: + ids = [batch_id[0]] + page_token = batch_id[1] + fetch_next = False + else: + page_token = None + fetch_next = True + + batch_ids = [] + to_update = [] result = {} - for list_of_50 in list_of_50s: - result.update(self._update_playlists(list_of_50)) + for playlist_id in ids: + page_token = page_token or 0 + while 1: + batch_id = (playlist_id, page_token) + batch_ids.append(batch_id) + batch = self._data_cache.get_item(batch_id, + self._data_cache.ONE_HOUR) + if not batch: + to_update.append(batch_id) + break + result[batch_id] = batch + page_token = batch.get('nextPageToken') if fetch_next else None + if page_token is None: + break + + if result: + self._context.log_debug('Found cached items for playlists:\n|{ids}|' + .format(ids=list(result))) + + new_data = {} + for playlist_id, page_token in to_update: + while 1: + batch_id = (playlist_id, page_token) + batch = self._client.get_playlist_items(*batch_id) + new_data[batch_id] = batch + page_token = batch.get('nextPageToken') if fetch_next else None + if page_token is None: + break + + if new_data: + to_update = list(new_data) + self._context.log_debug('Got items for playlists:\n|{ids}|' + .format(ids=to_update)) + self._data_cache.set_items(new_data) + result.update(new_data) + self._context.log_debug('Cached items for playlists:\n|{ids}|' + .format(ids=to_update)) + + # Re-sort result to match order of requested IDs + # Will only work in Python v3.7+ + if list(result) != batch_ids: + result = { + id: result[id] + for id in batch_ids + if id in result + } + return result def get_related_playlists(self, channel_id): - result = self._update_channels([channel_id]) + result = self.get_channels([channel_id]) # transform item = None @@ -199,65 +239,52 @@ def get_related_playlists(self, channel_id): return item.get('contentDetails', {}).get('relatedPlaylists', {}) - def get_channels(self, channel_ids): - list_of_50s = self._list_batch(channel_ids, n=50) - - result = {} - for list_of_50 in list_of_50s: - result.update(self._update_channels(list_of_50)) - return result + def get_videos(self, ids, live_details=False, suppress_errors=False): + result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH) + to_update = [id_ for id_ in ids if id_ not in result] + + if result: + self._context.log_debug('Found cached data for videos:\n|{ids}|' + .format(ids=list(result))) + + if to_update: + notify_and_raise = not suppress_errors + new_data = [self._client.get_videos(list_of_50, + live_details, + notify=notify_and_raise, + raise_exc=notify_and_raise) + for list_of_50 in self._list_batch(to_update, n=50)] + if not any(new_data): + new_data = None + else: + new_data = None - def get_fanarts(self, channel_ids): - if not self._enable_channel_fanart: - return {} + if new_data: + self._context.log_debug('Got data for videos:\n|{ids}|' + .format(ids=to_update)) + new_data = dict(dict.fromkeys(to_update, {}), **{ + yt_item['id']: yt_item or {} + for batch in new_data + for yt_item in batch.get('items', []) + }) + result.update(new_data) + self._data_cache.set_items(new_data) + self._context.log_debug('Cached data for videos:\n|{ids}|' + .format(ids=list(new_data))) + + # Re-sort result to match order of requested IDs + # Will only work in Python v3.7+ + if list(result) != ids: + result = { + id: result[id] + for id in ids + if id in result + } - result = self._update_channels(channel_ids) - banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', - 'bannerTvImageUrl', 'bannerExternalUrl'] - # transform - for key, item in result.items(): - images = item.get('brandingSettings', {}).get('image', {}) - for banner in banners: - image = images.get(banner) - if not image: - continue - result[key] = image - break - else: - # set an empty url - result[key] = '' + if self._context.get_settings().use_local_history(): + playback_history = self._context.get_playback_history() + played_items = playback_history.get_items(ids) + for video_id, play_data in played_items.items(): + result[video_id]['play_data'] = play_data return result - - def handle_error(self, json_data, suppress_errors=False): - context = self._context - if json_data and 'error' in json_data: - ok_dialog = False - message_timeout = 5000 - message = json_data['error'].get('message', '') - message = strip_html_from_text(message) - reason = json_data['error']['errors'][0].get('reason', '') - title = '%s: %s' % (context.get_name(), reason) - error_message = 'Error reason: |%s| with message: |%s|' % (reason, message) - - context.log_error(error_message) - - if reason == 'accessNotConfigured': - message = context.localize('key.requirement.notification') - ok_dialog = True - - elif reason in {'quotaExceeded', 'dailyLimitExceeded'}: - message_timeout = 7000 - - if not suppress_errors: - if ok_dialog: - context.get_ui().on_ok(title, message) - else: - context.get_ui().show_notification(message, title, - time_ms=message_timeout) - - raise YouTubeException(error_message) - - return False - - return True diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 92ad9b9b1..8274f5b58 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -146,35 +146,32 @@ def play_playlist(provider, context): if not playlist_ids: playlist_ids = [params.get('playlist_id')] - client = provider.get_client(context) + resource_manager = provider.get_resource_manager(context) ui = context.get_ui() - progress_dialog = ui.create_progress_dialog( + with ui.create_progress_dialog( context.localize('playlist.progress.updating'), context.localize('please_wait'), background=True - ) - - # start the loop and fill the list with video items - total = 0 - for playlist_id in playlist_ids: - page_token = 0 - while page_token is not None: - json_data = client.get_playlist_items(playlist_id, page_token) - if not v3.handle_error(context, json_data): - break - - if page_token == 0: - playlist_total = int(json_data.get('pageInfo', {}) - .get('totalResults', 0)) - if not playlist_total: - break - total += playlist_total - progress_dialog.set_total(total) + ) as progress_dialog: + json_data = resource_manager.get_playlist_items(playlist_ids) + + total = sum(len(chunk.get('items', [])) for chunk in json_data.values()) + progress_dialog.set_total(total) + progress_dialog.update( + steps=0, + text='{wait} {current}/{total}'.format( + wait=context.localize('please_wait'), + current=0, + total=total + ) + ) + # start the loop and fill the list with video items + for chunk in json_data.values(): result = v3.response_to_items(provider, context, - json_data, + chunk, process_next_page=False) videos.extend(result) @@ -187,51 +184,51 @@ def play_playlist(provider, context): ) ) - page_token = json_data.get('nextPageToken') or None - - # select order - order = params.get('order', '') - if not order: - order_list = ['default', 'reverse', 'shuffle'] - items = [(context.localize('playlist.play.%s' % order), order) - for order in order_list] - order = ui.on_select(context.localize('playlist.play.select'), items) - if order not in order_list: - order = 'default' - - # reverse the list - if order == 'reverse': - videos = videos[::-1] - elif order == 'shuffle': - # we have to shuffle the playlist by our self. - # The implementation of XBMC/KODI is quite weak :( - random.shuffle(videos) - - # clear the playlist - playlist = context.get_video_playlist() - playlist.clear() - - # select unshuffle - if order == 'shuffle': - playlist.unshuffle() - - # check if we have a video as starting point for the playlist - video_id = params.get('video_id', '') - # add videos to playlist - playlist_position = 0 - for idx, video in enumerate(videos): - playlist.add(video) - if video_id and not playlist_position and video_id in video.get_uri(): - playlist_position = idx - - # we use the shuffle implementation of the playlist - """ - if order == 'shuffle': - playlist.shuffle() - """ - - if progress_dialog: - progress_dialog.close() + if not videos: + return False + + # select order + order = params.get('order', '') + if not order: + order_list = ['default', 'reverse', 'shuffle'] + items = [(context.localize('playlist.play.%s' % order), order) + for order in order_list] + order = ui.on_select(context.localize('playlist.play.select'), + items) + if order not in order_list: + order = 'default' + + # reverse the list + if order == 'reverse': + videos = videos[::-1] + elif order == 'shuffle': + # we have to shuffle the playlist by our self. + # The implementation of XBMC/KODI is quite weak :( + random.shuffle(videos) + + # clear the playlist + playlist = context.get_video_playlist() + playlist.clear() + + # select unshuffle + if order == 'shuffle': + playlist.unshuffle() + + # check if we have a video as starting point for the playlist + video_id = params.get('video_id', '') + # add videos to playlist + playlist_position = 0 + for idx, video in enumerate(videos): + playlist.add(video) + if (video_id and not playlist_position + and video_id in video.get_uri()): + playlist_position = idx + + # we use the shuffle implementation of the playlist + """ + if order == 'shuffle': + playlist.shuffle() + """ if not params.get('play'): return videos diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 4bdc07206..d5950382d 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -277,45 +277,28 @@ def on_uri2addon(self, context, re_match): return False - @RegisterProviderPath('^/playlist/(?P[^/]+)/$') - def _on_playlist(self, context, re_match): - self.set_content_type(context, constants.content_type.VIDEOS) - - result = [] - - playlist_id = re_match.group('playlist_id') - page_token = context.get_param('page_token', '') - - # no caching - json_data = self.get_client(context).get_playlist_items(playlist_id=playlist_id, page_token=page_token) - if not v3.handle_error(context, json_data): - return False - result.extend(v3.response_to_items(self, context, json_data)) - - return result - """ Lists the videos of a playlist. path : '/channel/(?P[^/]+)/playlist/(?P[^/]+)/' + or + path : '/playlist/(?P[^/]+)/' channel_id : ['mine'|] playlist_id: """ - @RegisterProviderPath('^/channel/(?P[^/]+)/playlist/(?P[^/]+)/$') - def _on_channel_playlist(self, context, re_match): + @RegisterProviderPath('^(?:/channel/(?P[^/]+))?/playlist/(?P[^/]+)/$') + def _on_playlist(self, context, re_match): self.set_content_type(context, constants.content_type.VIDEOS) client = self.get_client(context) - result = [] + resource_manager = self.get_resource_manager(context) - playlist_id = re_match.group('playlist_id') - page_token = context.get_param('page_token', '') + batch_id = (re_match.group('playlist_id'), + context.get_param('page_token') or 0) - # no caching - json_data = client.get_playlist_items(playlist_id=playlist_id, page_token=page_token) - if not v3.handle_error(context, json_data): + json_data = resource_manager.get_playlist_items(batch_id=batch_id) + if not json_data: return False - result.extend(v3.response_to_items(self, context, json_data)) - + result = v3.response_to_items(self, context, json_data[batch_id]) return result """ From cb6b85a181944c1794fa1e15cdb6044b33c57a4d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:45:37 +1100 Subject: [PATCH 108/141] Remove JSON (de)serialisation for SQL storage - Partially revert 764caec which added missing JSON (de)serialisation - Data is already pickled and stored as a binary blob - Add versioning via table name - Bump to v2 to force cached data to be removed - Old table(s) will be automatically removed - Also remove row factory --- .../kodion/sql_store/data_cache.py | 5 +- .../kodion/sql_store/function_cache.py | 28 ++- .../kodion/sql_store/playback_history.py | 29 +-- .../kodion/sql_store/storage.py | 167 +++++++++--------- .../kodion/sql_store/watch_later_list.py | 6 +- .../lib/youtube_plugin/kodion/utils/player.py | 11 +- .../lib/youtube_plugin/youtube/provider.py | 8 +- 7 files changed, 112 insertions(+), 142 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index f6f7d08b3..defce41f6 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -import json from datetime import datetime from .storage import Storage @@ -45,10 +44,10 @@ def get_item(self, content_id, seconds): return None current_time = datetime.now() - if self.get_seconds_diff(query_result[1] or current_time) > seconds: + if self.get_seconds_diff(query_result[0] or current_time) > seconds: return None - return query_result[0] + return query_result[1] def set_item(self, content_id, item): self._set(content_id, item) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index 91839a64b..fa492ce8e 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -19,7 +19,8 @@ class FunctionCache(Storage): def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 - super(FunctionCache, self).__init__(filename, max_file_size_kb=max_file_size_kb) + super(FunctionCache, self).__init__(filename, + max_file_size_kb=max_file_size_kb) self._enabled = True @@ -67,10 +68,9 @@ def get_cached_only(self, func, *args, **keywords): # only return before cached data data, cache_id = self._get_cached_data(partial_func) - if data is not None: - return data[0] - - return None + if data is None: + return None + return data[1] def get(self, func, seconds, *args, **keywords): """ @@ -86,22 +86,14 @@ def get(self, func, seconds, *args, **keywords): if not self._enabled: return partial_func() - cached_data = None - cached_time = None data, cache_id = self._get_cached_data(partial_func) if data is not None: - cached_data = data[0] - cached_time = data[1] - - diff_seconds = 0 - - if cached_time is not None: - diff_seconds = self.get_seconds_diff(cached_time) - - if cached_data is None or diff_seconds > seconds: - cached_data = partial_func() - self._set(cache_id, cached_data) + cached_time, cached_data = data + if data is None or self.get_seconds_diff(cached_time) > seconds: + data = partial_func() + self._set(cache_id, data) + return data return cached_data def _optimize_item_count(self): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index ff4763b2b..4a26265d0 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -19,23 +19,14 @@ def __init__(self, filename): def is_empty(self): return self._is_empty() - @staticmethod - def _process_item(item): - return item.strip('"').split(',') - def get_items(self, keys): - query_result = self._get_by_ids(keys, process=self._process_item) + query_result = self._get_by_ids(keys) if not query_result: return {} result = { - item[0]: { - 'play_count': int(item[2][0]), - 'total_time': float(item[2][1]), - 'played_time': float(item[2][2]), - 'played_percent': int(item[2][3]), - 'last_played': str(item[1]), - } for item in query_result + item[0]: dict(item[2], last_played=item[1]) + for item in query_result } return result @@ -44,14 +35,7 @@ def get_item(self, key): if not query_result: return {} - values = query_result[0].split(',') - result = {key: { - 'play_count': int(values[0]), - 'total_time': float(values[1]), - 'played_time': float(values[2]), - 'played_percent': int(values[3]), - 'last_played': str(query_result[1]), - }} + result = {key: dict(query_result[1], last_played=query_result[0])} return result def clear(self): @@ -60,9 +44,8 @@ def clear(self): def remove(self, video_id): self._remove(video_id) - def update(self, video_id, play_count, total_time, played_time, played_percent): - item = ','.join([str(play_count), str(total_time), str(played_time), str(played_percent)]) - self._set(str(video_id), item) + def update(self, video_id, play_data): + self._set(video_id, play_data) def _optimize_item_count(self): pass diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index b41898429..833517aeb 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -import json import os import pickle import sqlite3 @@ -29,23 +28,19 @@ class Storage(object): ONE_WEEK = 7 * ONE_DAY ONE_MONTH = 4 * ONE_WEEK - _key = str('key') - _time = str('time') - _value = str('value') - _timestamp = str('timestamp') - - _table_name = 'storage' - _clear_query = 'DELETE FROM %s' % _table_name - _create_table_query = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % _table_name - _get_query = 'SELECT * FROM %s WHERE key = ?' % _table_name - _get_by_query = 'SELECT * FROM %s WHERE key in ({0})' % _table_name - _get_all_asc_query = 'SELECT * FROM %s ORDER BY time ASC LIMIT {0}' % _table_name - _get_all_desc_query = 'SELECT * FROM %s ORDER BY time DESC LIMIT {0}' % _table_name - _is_empty_query = 'SELECT EXISTS(SELECT 1 FROM %s LIMIT 1)' % _table_name - _optimize_item_query = 'SELECT key FROM %s ORDER BY time DESC LIMIT -1 OFFSET {0}' % _table_name - _remove_query = 'DELETE FROM %s WHERE key = ?' % _table_name - _remove_all_query = 'DELETE FROM %s WHERE key in ({0})' % _table_name - _set_query = 'REPLACE INTO %s (key, time, value) VALUES(?, ?, ?)' % _table_name + _table_name = 'storage_v2' + _clear_sql = 'DELETE FROM %s' % _table_name + _create_table_sql = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % _table_name + _drop_old_tables_sql = 'DELETE FROM sqlite_master WHERE type = "table" and name IS NOT "%s"' % _table_name + _get_sql = 'SELECT * FROM %s WHERE key = ?' % _table_name + _get_by_sql = 'SELECT * FROM %s WHERE key in ({0})' % _table_name + _get_all_asc_sql = 'SELECT * FROM %s ORDER BY time ASC LIMIT {0}' % _table_name + _get_all_desc_sql = 'SELECT * FROM %s ORDER BY time DESC LIMIT {0}' % _table_name + _is_empty_sql = 'SELECT EXISTS(SELECT 1 FROM %s LIMIT 1)' % _table_name + _optimize_item_sql = 'SELECT key FROM %s ORDER BY time DESC LIMIT -1 OFFSET {0}' % _table_name + _remove_sql = 'DELETE FROM %s WHERE key = ?' % _table_name + _remove_all_sql = 'DELETE FROM %s WHERE key in ({0})' % _table_name + _set_sql = 'REPLACE INTO %s (key, time, value) VALUES(?, ?, ?)' % _table_name def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._filename = filename @@ -59,7 +54,7 @@ def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._table_created = False self._needs_commit = False - sqlite3.register_converter(self._timestamp, self._convert_timestamp) + sqlite3.register_converter(str('timestamp'), self._convert_timestamp) def set_max_item_count(self, max_item_count): self._max_item_count = max_item_count @@ -68,10 +63,19 @@ def set_max_file_size_kb(self, max_file_size_kb): self._max_file_size_kb = max_file_size_kb def __del__(self): + self._close(True) + + def __enter__(self): + self._open() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): self._close() def _open(self): if self._db: + if not self._cursor: + self._cursor = self._db.cursor() return self._optimize_file_size() @@ -83,7 +87,6 @@ def _open(self): db = sqlite3.connect(self._filename, check_same_thread=False, detect_types=sqlite3.PARSE_DECLTYPES, timeout=1, isolation_level=None) - db.row_factory = sqlite3.Row cursor = db.cursor() # cursor.execute('PRAGMA journal_mode=MEMORY') cursor.execute('PRAGMA journal_mode=WAL') @@ -96,6 +99,14 @@ def _open(self): self._db = db self._cursor = cursor self._create_table() + self._drop_old_tables() + + def _drop_old_tables(self): + self._execute(True, 'PRAGMA writable_schema=1') + self._execute(True, self._drop_old_tables_sql) + self._execute(True, 'PRAGMA writable_schema=0') + self._sync() + self._execute(False, 'VACUUM') def _execute(self, needs_commit, query, values=None, many=False): if values is None: @@ -120,12 +131,13 @@ def _execute(self, needs_commit, query, values=None, many=False): time.sleep(0.1) return [] - def _close(self): - if self._db: + def _close(self, full=False): + if self._db and self._cursor: self._sync() self._db.commit() self._cursor.close() self._cursor = None + if full and self._db: self._db.close() self._db = None @@ -152,7 +164,7 @@ def _optimize_file_size(self): def _create_table(self): if self._table_created: return - self._execute(True, self._create_table_query) + self._execute(True, self._create_table_sql) self._table_created = True def _sync(self): @@ -164,22 +176,18 @@ def _sync(self): def _set(self, item_id, item): # add 1 microsecond, required for dbapi2 now = since_epoch(datetime.now()) + 0.000001 - self._open() - self._execute(True, self._set_query, values=[str(item_id), - now, - self._encode(item)]) - self._close() + with self as db: + db._execute(True, db._set_sql, + values=[str(item_id), now, db._encode(item)]) self._optimize_item_count() def _set_all(self, items): # add 1 microsecond, required for dbapi2 now = since_epoch(datetime.now()) + 0.000001 - self._open() - self._execute(True, self._set_query, - values=[(str(key), now, self._encode(item)) - for key, item in items.items()], - many=True) - self._close() + with self as db: + db._execute(True, db._set_sql, many=True, + values=[(str(item_id), now, db._encode(item)) + for item_id, item in items.items()]) self._optimize_item_count() def _optimize_item_count(self): @@ -189,32 +197,28 @@ def _optimize_item_count(self): return if self._max_item_count < 0: return - query = self._optimize_item_query.format(self._max_item_count) - self._open() - item_ids = self._execute(False, query) - key = self._key - item_ids = [item_id[key] for item_id in item_ids] - if item_ids: - self._remove_all(item_ids) - self._close() + query = self._optimize_item_sql.format(self._max_item_count) + with self as db: + item_ids = db._execute(False, query) + item_ids = [item_id[0] for item_id in item_ids] + if item_ids: + db._remove_all(item_ids) def _clear(self): - self._open() - self._execute(True, self._clear_query) - self._create_table() - self._sync() - self._execute(False, 'VACUUM') - self._close() + with self as db: + db._execute(True, db._clear_sql) + db._create_table() + db._sync() + db._execute(False, 'VACUUM') def _is_empty(self): - self._open() - result = self._execute(False, self._is_empty_query) - for item in result: - is_empty = item[0] == 0 - break - else: - is_empty = True - self._close() + with self as db: + result = db._execute(False, db._is_empty_sql) + for item in result: + is_empty = item[0] == 0 + break + else: + is_empty = True return is_empty @staticmethod @@ -222,58 +226,51 @@ def _decode(obj, process=None): decoded_obj = pickle.loads(obj) if process: return process(decoded_obj) - return json.loads(decoded_obj) + return decoded_obj @staticmethod def _encode(obj): return sqlite3.Binary(pickle.dumps( - json.dumps(obj, ensure_ascii=False), - protocol=pickle.HIGHEST_PROTOCOL + obj, protocol=pickle.HIGHEST_PROTOCOL )) - def _get(self, item_id): - self._open() - result = self._execute(False, self._get_query, [item_id]) - if result: - result = result.fetchone() - self._close() - if result: - return self._decode(result[self._value]), result[self._time] - return None + def _get(self, item_id, process=None): + with self as db: + result = db._execute(False, db._get_sql, [item_id]) + if result: + result = result.fetchone() + if not result: + return None + return result[1], self._decode(result[2], process) def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, process=None): if not item_ids: if oldest_first: - query = self._get_all_asc_query + query = self._get_all_asc_sql else: - query = self._get_all_desc_query + query = self._get_all_desc_sql query = query.format(limit) else: num_ids = len(item_ids) - query = self._get_by_query.format(('?,' * (num_ids - 1)) + '?') + query = self._get_by_sql.format(('?,' * (num_ids - 1)) + '?') item_ids = tuple(item_ids) - self._open() - result = self._execute(False, query, item_ids) - key = self._key - time = self._time - value = self._value - result = [ - (item[key], item[time], self._decode(item[value], process)) - for item in result - ] - self._close() + with self as db: + result = db._execute(False, query, item_ids) + result = [ + (item[0], item[1], db._decode(item[2], process)) + for item in result + ] return result def _remove(self, item_id): - self._open() - self._execute(True, self._remove_query, [item_id]) + with self as db: + db._execute(True, db._remove_sql, [item_id]) def _remove_all(self, item_ids): num_ids = len(item_ids) - query = self._remove_all_query.format(('?,' * (num_ids - 1)) + '?') - self._open() + query = self._remove_all_sql.format(('?,' * (num_ids - 1)) + '?') self._execute(True, query, tuple(item_ids)) @staticmethod diff --git a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index cb8e39743..658424a30 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -13,7 +13,7 @@ from datetime import datetime from .storage import Storage -from .. import items +from ..items import to_json, from_json class WatchLaterList(Storage): @@ -28,13 +28,13 @@ def _sort_item(_item): return _item[2].get_date() def get_items(self): - result = self._get_by_ids(process=items.from_json) + result = self._get_by_ids(process=from_json) return sorted(result, key=self._sort_item, reverse=False) def add(self, base_item): base_item.set_date_from_datetime(datetime.now()) - item_json_data = items.to_json(base_item) + item_json_data = to_json(base_item) self._set(base_item.get_id(), item_json_data) def remove(self, base_item): diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index 8c3adc136..f68f8ec00 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -329,11 +329,14 @@ def run(self): refresh_only = True if use_local_history: + play_data = { + 'play_count': play_count, + 'total_time': self.total_time, + 'played_time': self.current_time, + 'played_percent': self.percent_complete, + } self._context.get_playback_history().update(self.video_id, - play_count, - self.total_time, - self.current_time, - self.percent_complete) + play_data) if not refresh_only and is_logged_in: if settings.get_bool('youtube.playlist.watchlater.autoremove', diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index d5950382d..2c40c1d3f 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -473,7 +473,7 @@ def _on_channel(self, context, re_match): upload_playlist, page_token=page_token) if not json_data: - return False + return result result.extend(v3.response_to_items(self, context, json_data)) @@ -1171,11 +1171,7 @@ def on_playback_history(self, context, re_match): play_data['played_time'] = 0 play_data['played_percent'] = 0 - playback_history.update(video_id, - play_data.get('play_count', 0), - play_data.get('total_time', 0), - play_data.get('played_time', 0), - play_data.get('played_percent', 0)) + playback_history.update(video_id, play_data) context.get_ui().refresh_container() return True From d1cdfbe4df2cd29db48a267e4a561c059704f0fe Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 16 Dec 2023 07:00:44 +1100 Subject: [PATCH 109/141] Fix initial discard of subsequent pages of playlistitems --- .../youtube/helper/resource_manager.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 8e29b9ece..a685217ae 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -86,11 +86,11 @@ def get_channels(self, ids): # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ - if list(result) != ids: + if list(result) != ids[:len(result)]: result = { - id: result[id] - for id in ids - if id in result + id_: result[id_] + for id_ in ids + if id_ in result } return result @@ -118,6 +118,7 @@ def get_fanarts(self, channel_ids): return result def get_playlists(self, ids): + ids = tuple(ids) result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH) to_update = [id_ for id_ in ids if id_ not in result] @@ -149,11 +150,11 @@ def get_playlists(self, ids): # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ - if list(result) != ids: + if list(result) != ids[:len(result)]: result = { - id: result[id] - for id in ids - if id in result + id_: result[id_] + for id_ in ids + if id_ in result } return result @@ -193,13 +194,20 @@ def get_playlist_items(self, ids=None, batch_id=None): .format(ids=list(result))) new_data = {} + insert_point = 0 for playlist_id, page_token in to_update: + new_batch_ids = [] + batch_id = (playlist_id, page_token) + insert_point = batch_ids.index(batch_id, insert_point) while 1: batch_id = (playlist_id, page_token) + new_batch_ids.append(batch_id) batch = self._client.get_playlist_items(*batch_id) new_data[batch_id] = batch page_token = batch.get('nextPageToken') if fetch_next else None if page_token is None: + batch_ids[insert_point:insert_point] = new_batch_ids + insert_point += len(new_batch_ids) break if new_data: @@ -213,11 +221,11 @@ def get_playlist_items(self, ids=None, batch_id=None): # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ - if list(result) != batch_ids: + if list(result) != batch_ids[:len(result)]: result = { - id: result[id] - for id in batch_ids - if id in result + id_: result[id_] + for id_ in batch_ids + if id_ in result } return result @@ -240,6 +248,7 @@ def get_related_playlists(self, channel_id): return item.get('contentDetails', {}).get('relatedPlaylists', {}) def get_videos(self, ids, live_details=False, suppress_errors=False): + ids = tuple(ids) result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH) to_update = [id_ for id_ in ids if id_ not in result] @@ -274,11 +283,11 @@ def get_videos(self, ids, live_details=False, suppress_errors=False): # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ - if list(result) != ids: + if list(result) != ids[:len(result)]: result = { - id: result[id] - for id in ids - if id in result + id_: result[id_] + for id_ in ids + if id_ in result } if self._context.get_settings().use_local_history(): From 0c499af608cdcd5ba295f03bcef55a8002a7f9d8 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 17 Dec 2023 00:21:14 +1100 Subject: [PATCH 110/141] Minor optimisations for SQL storage - Remove unnecessary type conversions - Remove unnecessary sorting - Remove unnecessary iteration --- .../lib/youtube_plugin/kodion/items/utils.py | 2 +- .../kodion/sql_store/data_cache.py | 23 +--- .../kodion/sql_store/favorite_list.py | 6 +- .../kodion/sql_store/function_cache.py | 20 ++-- .../kodion/sql_store/playback_history.py | 24 ++-- .../kodion/sql_store/search_history.py | 5 +- .../kodion/sql_store/storage.py | 109 ++++++------------ .../kodion/sql_store/watch_later_list.py | 8 +- 8 files changed, 63 insertions(+), 134 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index 64e0966ba..a5a5971a1 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -26,7 +26,7 @@ } -def from_json(json_data): +def from_json(json_data, *_args): """ Creates a instance of the given json dump or dict. :param json_data: diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index defce41f6..e3a5a538a 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -25,29 +25,12 @@ def is_empty(self): return self._is_empty() def get_items(self, content_ids, seconds): - query_result = self._get_by_ids(content_ids) - if not query_result: - return {} - - current_time = datetime.now() - result = { - item[0]: item[2] - for item in query_result - if self.get_seconds_diff(item[1] or current_time) <= seconds - } + result = self._get_by_ids(content_ids, seconds=seconds, as_dict=True) return result def get_item(self, content_id, seconds): - content_id = str(content_id) - query_result = self._get(content_id) - if not query_result: - return None - - current_time = datetime.now() - if self.get_seconds_diff(query_result[0] or current_time) > seconds: - return None - - return query_result[1] + result = self._get(content_id, seconds=seconds) + return result def set_item(self, content_id, item): self._set(content_id, item) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py index 788ba1ff4..119cd95dd 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py @@ -22,11 +22,11 @@ def clear(self): self._clear() @staticmethod - def _sort_item(_item): - return _item[2].get_name().upper() + def _sort_item(item): + return item.get_name().upper() def get_items(self): - result = self._get_by_ids(process=from_json) + result = self._get_by_ids(process=from_json, values_only=True) return sorted(result, key=self._sort_item, reverse=False) def add(self, base_item): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index fa492ce8e..e244ba940 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -55,9 +55,9 @@ def _create_id_from_func(partial_func): md5_hash.update(str(partial_func.keywords).encode('utf-8')) return md5_hash.hexdigest() - def _get_cached_data(self, partial_func): + def _get_cached_data(self, partial_func, seconds=None): cache_id = self._create_id_from_func(partial_func) - return self._get(cache_id), cache_id + return self._get(cache_id, seconds=seconds), cache_id def get_cached_only(self, func, *args, **keywords): partial_func = partial(func, *args, **keywords) @@ -67,10 +67,8 @@ def get_cached_only(self, func, *args, **keywords): return partial_func() # only return before cached data - data, cache_id = self._get_cached_data(partial_func) - if data is None: - return None - return data[1] + data, _ = self._get_cached_data(partial_func) + return data def get(self, func, seconds, *args, **keywords): """ @@ -86,15 +84,11 @@ def get(self, func, seconds, *args, **keywords): if not self._enabled: return partial_func() - data, cache_id = self._get_cached_data(partial_func) - if data is not None: - cached_time, cached_data = data - - if data is None or self.get_seconds_diff(cached_time) > seconds: + data, cache_id = self._get_cached_data(partial_func, seconds=seconds) + if data is None: data = partial_func() self._set(cache_id, data) - return data - return cached_data + return data def _optimize_item_count(self): # override method Storage._optimize_item_count diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index 4a26265d0..a0eae31e4 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -19,23 +19,19 @@ def __init__(self, filename): def is_empty(self): return self._is_empty() - def get_items(self, keys): - query_result = self._get_by_ids(keys) - if not query_result: - return {} - - result = { - item[0]: dict(item[2], last_played=item[1]) - for item in query_result - } + def _add_last_played(self, value, item): + value['last_played'] = self._convert_timestamp(item[1]) + return value + + def get_items(self, keys=None): + result = self._get_by_ids(keys, + oldest_first=False, + process=self._add_last_played, + as_dict=True) return result def get_item(self, key): - query_result = self._get(key) - if not query_result: - return {} - - result = {key: dict(query_result[1], last_played=query_result[0])} + result = self._get(key, process=self._add_last_played) return result def clear(self): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py index bf83574ac..a0f7046ea 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -25,8 +25,9 @@ def is_empty(self): def get_items(self): result = self._get_by_ids(oldest_first=False, - limit=self._max_item_count) - return [item[2] for item in result] + limit=self._max_item_count, + values_only=True) + return result def clear(self): self._clear() diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 833517aeb..c360bb219 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -30,7 +30,7 @@ class Storage(object): _table_name = 'storage_v2' _clear_sql = 'DELETE FROM %s' % _table_name - _create_table_sql = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % _table_name + _create_table_sql = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time REAL, value BLOB)' % _table_name _drop_old_tables_sql = 'DELETE FROM sqlite_master WHERE type = "table" and name IS NOT "%s"' % _table_name _get_sql = 'SELECT * FROM %s WHERE key = ?' % _table_name _get_by_sql = 'SELECT * FROM %s WHERE key in ({0})' % _table_name @@ -54,8 +54,6 @@ def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._table_created = False self._needs_commit = False - sqlite3.register_converter(str('timestamp'), self._convert_timestamp) - def set_max_item_count(self, max_item_count): self._max_item_count = max_item_count @@ -174,16 +172,14 @@ def _sync(self): return self._execute(False, 'COMMIT') def _set(self, item_id, item): - # add 1 microsecond, required for dbapi2 - now = since_epoch(datetime.now()) + 0.000001 + now = since_epoch(datetime.now()) with self as db: db._execute(True, db._set_sql, values=[str(item_id), now, db._encode(item)]) self._optimize_item_count() def _set_all(self, items): - # add 1 microsecond, required for dbapi2 - now = since_epoch(datetime.now()) + 0.000001 + now = since_epoch(datetime.now()) with self as db: db._execute(True, db._set_sql, many=True, values=[(str(item_id), now, db._encode(item)) @@ -222,10 +218,10 @@ def _is_empty(self): return is_empty @staticmethod - def _decode(obj, process=None): + def _decode(obj, process=None, item=None): decoded_obj = pickle.loads(obj) if process: - return process(decoded_obj) + return process(decoded_obj, item) return decoded_obj @staticmethod @@ -234,17 +230,20 @@ def _encode(obj): obj, protocol=pickle.HIGHEST_PROTOCOL )) - def _get(self, item_id, process=None): + def _get(self, item_id, process=None, seconds=None): with self as db: - result = db._execute(False, db._get_sql, [item_id]) - if result: - result = result.fetchone() - if not result: + result = db._execute(False, db._get_sql, [str(item_id)]) + item = result.fetchone() if result else None + if not item: return None - return result[1], self._decode(result[2], process) + cut_off = since_epoch(datetime.now()) - seconds if seconds else 0 + if not cut_off or item[1] >= cut_off: + return self._decode(item[2], process, item) + return None def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, - process=None): + seconds=None, process=None, + as_dict=False, values_only=False): if not item_ids: if oldest_first: query = self._get_all_asc_sql @@ -258,10 +257,24 @@ def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, with self as db: result = db._execute(False, query, item_ids) - result = [ - (item[0], item[1], db._decode(item[2], process)) - for item in result - ] + cut_off = since_epoch(datetime.now()) - seconds if seconds else 0 + if as_dict: + result = { + item[0]: db._decode(item[2], process, item) + for item in result if not cut_off or item[1] >= cut_off + } + elif values_only: + result = [ + db._decode(item[2], process, item) + for item in result if not cut_off or item[1] >= cut_off + ] + else: + result = [ + (item[0], + self._convert_timestamp(item[1]), + db._decode(item[2], process, item)) + for item in result if not cut_off or item[1] >= cut_off + ] return result def _remove(self, item_id): @@ -273,60 +286,6 @@ def _remove_all(self, item_ids): query = self._remove_all_sql.format(('?,' * (num_ids - 1)) + '?') self._execute(True, query, tuple(item_ids)) - @staticmethod - def strptime(stamp, stamp_fmt): - # noinspection PyUnresolvedReferences - import _strptime - try: - time.strptime('01 01 2012', '%d %m %Y') # dummy call - except: - pass - return time.strptime(stamp, stamp_fmt) - @classmethod def _convert_timestamp(cls, val): - val = val.decode('utf-8') - if '-' in val or ':' in val: - return cls._parse_datetime_string(val) - return datetime.fromtimestamp(float(val)) - - @classmethod - def _parse_datetime_string(cls, current_stamp): - for stamp_format in ['%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S']: - try: - stamp_datetime = datetime( - *(cls.strptime(current_stamp, stamp_format)[0:6]) - ) - break - except ValueError: # current_stamp has no microseconds - continue - except TypeError: - log_error('Exception while parsing timestamp:\n' - 'current_stamp |{cs}|{cst}|\n' - 'stamp_format |{sf}|{sft}|\n{tb}' - .format(cs=current_stamp, - cst=type(current_stamp), - sf=stamp_format, - sft=type(stamp_format), - tb=print_exc())) - else: - return None - return stamp_datetime - - def get_seconds_diff(self, current_stamp): - if not current_stamp: - return 86400 # 24 hrs - - current_datetime = datetime.now() - if isinstance(current_stamp, datetime): - time_delta = current_datetime - current_stamp - return time_delta.total_seconds() - - if isinstance(current_stamp, (float, int)): - return since_epoch(current_datetime) - current_stamp - - stamp_datetime = self._parse_datetime_string(current_stamp) - if not stamp_datetime: - return 604800 # one week - time_delta = current_datetime - stamp_datetime - return time_delta.total_seconds() + return datetime.fromtimestamp(val) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index 658424a30..70a1b7f9e 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -23,13 +23,9 @@ def __init__(self, filename): def clear(self): self._clear() - @staticmethod - def _sort_item(_item): - return _item[2].get_date() - def get_items(self): - result = self._get_by_ids(process=from_json) - return sorted(result, key=self._sort_item, reverse=False) + result = self._get_by_ids(process=from_json, values_only=True) + return result def add(self, base_item): base_item.set_date_from_datetime(datetime.now()) From 14099bd9f2d10f81108822721e59b331edd6b26b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 17 Dec 2023 04:05:06 +1100 Subject: [PATCH 111/141] Update type/attr/key checking - Allow passing tuples and dict.keys instead of only lists as parameters - Other misc fixes to prevent possible exceptions --- .../youtube_plugin/kodion/abstract_provider.py | 9 +++++---- .../youtube_plugin/kodion/items/video_item.py | 3 ++- .../kodion/plugin/xbmc/xbmc_runner.py | 2 +- .../lib/youtube_plugin/kodion/utils/methods.py | 4 ++-- .../youtube/client/request_client.py | 2 +- .../lib/youtube_plugin/youtube/client/youtube.py | 16 ++++++++-------- .../youtube_plugin/youtube/helper/subtitles.py | 2 +- resources/lib/youtube_requests.py | 4 ++-- 8 files changed, 22 insertions(+), 20 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index a35a8bad5..4f543e10d 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -52,9 +52,10 @@ def __init__(self): """ for method_name in dir(self): - method = getattr(self, method_name) - if hasattr(method, 'kodion_re_path'): - self.register_path(method.kodion_re_path, method_name) + method = getattr(self, method_name, None) + path = method and getattr(method, 'kodion_re_path', None) + if path: + self.register_path(path, method_name) def get_alternative_fanart(self, context): return context.get_fanart() @@ -96,7 +97,7 @@ def navigate(self, context): re_match = re.search(key, path, re.UNICODE) if re_match is not None: method_name = self._dict_path.get(key, '') - method = getattr(self, method_name) + method = getattr(self, method_name, None) if method is not None: result = method(context, re_match) if not isinstance(result, tuple): diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 2e40a3f6a..87b3483c1 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -263,7 +263,8 @@ def get_mediatype(self): return self._mediatype def set_subtitles(self, value): - self.subtitles = value if value and isinstance(value, list) else None + if value and isinstance(value, (list, tuple)): + self.subtitles = value def set_headers(self, value): self._headers = value diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index 131451578..d23e8e48f 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -71,7 +71,7 @@ def run(self, provider, context): if isinstance(result, DirectoryItem): item_count = 1 items = [self._add_directory(result, show_fanart)] - elif isinstance(result, list): + elif isinstance(result, (list, tuple)): item_count = len(result) items = [ self._add_directory(item, show_fanart) if isinstance(item, DirectoryItem) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index de4d03f9f..2dbfc8ced 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -178,7 +178,7 @@ def _find_best_fit_video(_stream_data): def create_path(*args): comps = [] for arg in args: - if isinstance(arg, list): + if isinstance(arg, (list, tuple)): return create_path(*arg) comps.append(str(arg.strip('/').replace('\\', '/').replace('//', '/'))) @@ -193,7 +193,7 @@ def create_path(*args): def create_uri_path(*args): comps = [] for arg in args: - if isinstance(arg, list): + if isinstance(arg, (list, tuple)): return create_uri_path(*arg) comps.append(str(arg.strip('/').replace('\\', '/').replace('//', '/'))) diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 42f438214..045e35006 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -271,7 +271,7 @@ def json_traverse(json_data, path): result = json_data for keys in path: is_dict = isinstance(result, dict) - if not is_dict and not isinstance(result, list): + if not is_dict and not isinstance(result, (list, tuple)): return None if not isinstance(keys, (list, tuple)): diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 741b56d52..e484bcf54 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -213,7 +213,7 @@ def create_playlist(self, title, privacy_status='private', **kwargs): **kwargs) def get_video_rating(self, video_id, **kwargs): - if isinstance(video_id, list): + if not isinstance(video_id, str): video_id = ','.join(video_id) params = {'id': video_id} @@ -622,7 +622,7 @@ def get_channels(self, channel_id, **kwargs): :param channel_id: list or comma-separated list of the YouTube channel ID(s) :return: """ - if isinstance(channel_id, list): + if not isinstance(channel_id, str): channel_id = ','.join(channel_id) params = {'part': 'snippet,contentDetails,brandingSettings'} @@ -659,14 +659,14 @@ def get_videos(self, video_id, live_details=False, **kwargs): :param live_details: also retrieve liveStreamingDetails :return: """ - if isinstance(video_id, list): + if not isinstance(video_id, str): video_id = ','.join(video_id) - parts = ['snippet,contentDetails,status,statistics'] + parts = ['snippet', 'contentDetails', 'status', 'statistics'] if live_details: - parts.append(',liveStreamingDetails') + parts.append('liveStreamingDetails') - params = {'part': ''.join(parts), + params = {'part': ','.join(parts), 'id': video_id} return self.perform_v3_request(method='GET', path='videos', @@ -674,7 +674,7 @@ def get_videos(self, video_id, live_details=False, **kwargs): **kwargs) def get_playlists(self, playlist_id, **kwargs): - if isinstance(playlist_id, list): + if not isinstance(playlist_id, str): playlist_id = ','.join(playlist_id) params = {'part': 'snippet,contentDetails', @@ -850,7 +850,7 @@ def search(self, # prepare search type if not search_type: search_type = '' - if isinstance(search_type, list): + if not isinstance(search_type, str): search_type = ','.join(search_type) # prepare page token diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 485d538e5..94a35d90f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -284,7 +284,7 @@ def _get_language_name(track): lang_name = track.get(key, {}).get('simpleText') if not lang_name: track_name = track.get(key, {}).get('runs', [{}]) - if isinstance(track_name, list) and len(track_name) >= 1: + if isinstance(track_name, (list, tuple)) and len(track_name) >= 1: lang_name = track_name[0].get('text') if lang_name: diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index 391018b8f..81b532f88 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -53,7 +53,7 @@ def v3_request(method='GET', headers=None, path=None, post_data=None, params=Non def _append_missing_page_token(items): - if items and isinstance(items, list) and (items[-1].get('nextPageToken') is None): + if items and isinstance(items, list) and 'nextPageToken' not in items[-1]: items.append({'nextPageToken': ''}) return items @@ -481,7 +481,7 @@ def get_live(channel_id=None, user=None, url=None, addon_id=None): if matched_type == 'user': items = get_channel_id(matched_id, addon_id=addon_id) - if not items or not isinstance(items, list): + if not items or not isinstance(items, list) or 'id' not in items[0]: return None matched_id = items[0]['id'] From 5c48572d88e7d648941234fed06620eb2a86066f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 18 Dec 2023 00:13:36 +1100 Subject: [PATCH 112/141] Remove old data on optimise SQL storage by size - Also add logging for errors - Bump storage table version --- .../kodion/sql_store/data_cache.py | 2 - .../kodion/sql_store/storage.py | 76 +++++++++++-------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index e3a5a538a..06539b514 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -10,8 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -from datetime import datetime - from .storage import Storage diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index c360bb219..23f4b347a 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -28,19 +28,20 @@ class Storage(object): ONE_WEEK = 7 * ONE_DAY ONE_MONTH = 4 * ONE_WEEK - _table_name = 'storage_v2' - _clear_sql = 'DELETE FROM %s' % _table_name - _create_table_sql = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time REAL, value BLOB)' % _table_name - _drop_old_tables_sql = 'DELETE FROM sqlite_master WHERE type = "table" and name IS NOT "%s"' % _table_name - _get_sql = 'SELECT * FROM %s WHERE key = ?' % _table_name - _get_by_sql = 'SELECT * FROM %s WHERE key in ({0})' % _table_name - _get_all_asc_sql = 'SELECT * FROM %s ORDER BY time ASC LIMIT {0}' % _table_name - _get_all_desc_sql = 'SELECT * FROM %s ORDER BY time DESC LIMIT {0}' % _table_name - _is_empty_sql = 'SELECT EXISTS(SELECT 1 FROM %s LIMIT 1)' % _table_name - _optimize_item_sql = 'SELECT key FROM %s ORDER BY time DESC LIMIT -1 OFFSET {0}' % _table_name - _remove_sql = 'DELETE FROM %s WHERE key = ?' % _table_name - _remove_all_sql = 'DELETE FROM %s WHERE key in ({0})' % _table_name - _set_sql = 'REPLACE INTO %s (key, time, value) VALUES(?, ?, ?)' % _table_name + _table_name = 'storage_v3' + _clear_sql = 'DELETE FROM {table}'.format(table=_table_name) + _create_table_sql = 'CREATE TABLE IF NOT EXISTS {table} (key TEXT PRIMARY KEY, time REAL, value BLOB, size INTEGER)'.format(table=_table_name) + _drop_old_tables_sql = 'DELETE FROM sqlite_master WHERE type = "table" and name IS NOT "{table}"'.format(table=_table_name) + _get_sql = 'SELECT * FROM {table} WHERE key = ?'.format(table=_table_name) + _get_by_sql = 'SELECT * FROM {table} WHERE key in ({{0}})'.format(table=_table_name) + _get_all_asc_sql = 'SELECT * FROM {table} ORDER BY time ASC LIMIT {{0}}'.format(table=_table_name) + _get_all_desc_sql = 'SELECT * FROM {table} ORDER BY time DESC LIMIT {{0}}'.format(table=_table_name) + _is_empty_sql = 'SELECT EXISTS(SELECT 1 FROM {table} LIMIT 1)'.format(table=_table_name) + _optimize_item_sql = 'SELECT key FROM {table} ORDER BY time DESC LIMIT -1 OFFSET {{0}}'.format(table=_table_name) + _prune_sql = 'DELETE FROM {table} WHERE ROWID IN (SELECT ROWID FROM {table} WHERE (SELECT SUM(size) FROM {table} AS _ WHERE time<={table}.time) <= {{0}})'.format(table=_table_name) + _remove_sql = 'DELETE FROM {table} WHERE key = ?'.format(table=_table_name) + _remove_all_sql = 'DELETE FROM {table} WHERE key in ({{0}})'.format(table=_table_name) + _set_sql = 'REPLACE INTO {table} (key, time, value, size) VALUES(?, ?, ?, ?)'.format(table=_table_name) def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._filename = filename @@ -52,6 +53,7 @@ def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._max_file_size_kb = max_file_size_kb self._table_created = False + self._table_updated = False self._needs_commit = False def set_max_item_count(self, max_item_count): @@ -76,14 +78,11 @@ def _open(self): self._cursor = self._db.cursor() return - self._optimize_file_size() - path = os.path.dirname(self._filename) if not os.path.exists(path): os.makedirs(path) db = sqlite3.connect(self._filename, check_same_thread=False, - detect_types=sqlite3.PARSE_DECLTYPES, timeout=1, isolation_level=None) cursor = db.cursor() # cursor.execute('PRAGMA journal_mode=MEMORY') @@ -91,20 +90,26 @@ def _open(self): cursor.execute('PRAGMA busy_timeout=20000') cursor.execute('PRAGMA read_uncommitted=TRUE') cursor.execute('PRAGMA temp_store=MEMORY') + cursor.execute('PRAGMA page_size=4096') # cursor.execute('PRAGMA synchronous=OFF') cursor.execute('PRAGMA synchronous=NORMAL') - cursor.arraysize = 100 + cursor.arraysize = 50 self._db = db self._cursor = cursor + self._create_table() self._drop_old_tables() + self._optimize_file_size() def _drop_old_tables(self): + if self._table_updated: + return self._execute(True, 'PRAGMA writable_schema=1') self._execute(True, self._drop_old_tables_sql) self._execute(True, 'PRAGMA writable_schema=0') self._sync() self._execute(False, 'VACUUM') + self._table_updated = True def _execute(self, needs_commit, query, values=None, many=False): if values is None: @@ -124,18 +129,23 @@ def _execute(self, needs_commit, query, values=None, many=False): return self._cursor.executemany(query, values) return self._cursor.execute(query, values) except TypeError: + log_error('SQLStorage._execute - |{0}|'.format(print_exc())) return [] except: + log_error('SQLStorage._execute - |{0}|'.format(print_exc())) time.sleep(0.1) return [] def _close(self, full=False): - if self._db and self._cursor: + if not self._db: + return + + if self._cursor: self._sync() self._db.commit() self._cursor.close() self._cursor = None - if full and self._db: + if full: self._db.close() self._db = None @@ -144,20 +154,19 @@ def _optimize_file_size(self): if self._max_file_size_kb <= 0: return - # do nothing - only if this folder exists - path = os.path.dirname(self._filename) - if not os.path.exists(path): + # do nothing - only if this db exists + if not os.path.exists(self._filename): return - if not os.path.exists(self._filename): + file_size_kb = (os.path.getsize(self._filename) // 1024) + if file_size_kb <= self._max_file_size_kb: return - try: - file_size_kb = (os.path.getsize(self._filename) // 1024) - if file_size_kb >= self._max_file_size_kb: - os.remove(self._filename) - except OSError: - pass + prune_size = 1024 * int(file_size_kb - self._max_file_size_kb / 2) + query = self._prune_sql.format(prune_size) + self._execute(True, query) + self._sync() + self._execute(False, 'VACUUM') def _create_table(self): if self._table_created: @@ -175,14 +184,14 @@ def _set(self, item_id, item): now = since_epoch(datetime.now()) with self as db: db._execute(True, db._set_sql, - values=[str(item_id), now, db._encode(item)]) + values=[str(item_id), now, *db._encode(item)]) self._optimize_item_count() def _set_all(self, items): now = since_epoch(datetime.now()) with self as db: db._execute(True, db._set_sql, many=True, - values=[(str(item_id), now, db._encode(item)) + values=[(str(item_id), now, *db._encode(item)) for item_id, item in items.items()]) self._optimize_item_count() @@ -226,9 +235,12 @@ def _decode(obj, process=None, item=None): @staticmethod def _encode(obj): - return sqlite3.Binary(pickle.dumps( + blob = sqlite3.Binary(pickle.dumps( obj, protocol=pickle.HIGHEST_PROTOCOL )) + size = getattr(blob, 'nbytes', None) or blob.itemsize * len(blob) + return blob, size + def _get(self, item_id, process=None, seconds=None): with self as db: From ae79b262aa5912a8fd147ec1c4804775b87cb551 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 21 Dec 2023 02:27:20 +1100 Subject: [PATCH 113/141] Misc tidy ups --- resources/lib/youtube_authentication.py | 4 +- .../kodion/constants/const_content_types.py | 1 + .../kodion/constants/const_paths.py | 1 + .../kodion/constants/const_settings.py | 1 + .../kodion/context/xbmc/xbmc_context.py | 4 +- resources/lib/youtube_plugin/kodion/debug.py | 4 +- .../youtube_plugin/kodion/items/__init__.py | 12 ++--- .../youtube_plugin/kodion/items/base_item.py | 20 ++++----- .../kodion/network/http_server.py | 6 +-- .../youtube_plugin/kodion/network/requests.py | 2 +- .../kodion/plugin/xbmc/xbmc_runner.py | 12 +++-- .../settings/xbmc/xbmc_plugin_settings.py | 44 +++++++++---------- .../kodion/sql_store/storage.py | 2 +- .../kodion/ui/abstract_progress_dialog.py | 2 +- .../youtube/client/login_client.py | 4 +- .../youtube_plugin/youtube/client/youtube.py | 4 +- .../youtube_plugin/youtube/helper/utils.py | 4 +- .../lib/youtube_plugin/youtube/helper/v3.py | 4 +- .../youtube/helper/video_info.py | 12 ++--- .../youtube_plugin/youtube/helper/yt_play.py | 8 ++-- .../lib/youtube_plugin/youtube/provider.py | 6 +-- 21 files changed, 82 insertions(+), 75 deletions(-) diff --git a/resources/lib/youtube_authentication.py b/resources/lib/youtube_authentication.py index e29822074..fe463b867 100644 --- a/resources/lib/youtube_authentication.py +++ b/resources/lib/youtube_authentication.py @@ -103,8 +103,8 @@ def sign_in(addon_id): try: signed_in = youtube_authentication.sign_in(addon_id='plugin.video.example') # refreshes access tokens if already signed in - except youtube_authentication.LoginException as e: - error_message = e.get_message() + except youtube_authentication.LoginException as exc: + error_message = exc.get_message() # handle error signed_in = False diff --git a/resources/lib/youtube_plugin/kodion/constants/const_content_types.py b/resources/lib/youtube_plugin/kodion/constants/const_content_types.py index 5990ee1f6..6436882bb 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_content_types.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_content_types.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals + FILES = 'files' SONGS = 'songs' ARTISTS = 'artists' diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index 8903eb34d..aad8c4752 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals + SEARCH = 'kodion/search' FAVORITES = 'kodion/favorites' WATCH_LATER = 'kodion/watch_later' diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 16ddff484..9f232632b 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals + THUMB_SIZE = 'kodion.thumbnail.size' # (int) SHOW_FANART = 'kodion.fanart.show' # (bool) SAFE_SEARCH = 'kodion.safe.search' # (int) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 6ba3d588d..8472372a9 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -334,8 +334,8 @@ def get_language(self): language = language.split('-') language = '%s-%s' % (language[0].lower(), language[1].upper()) return language - except Exception, ex: - self.log_error('Failed to get system language (%s)', ex.__str__()) + except Exception as exc: + self.log_error('Failed to get system language (%s)', exc.__str__()) return 'en-US' ''' diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index 4c7b9aa9b..896a7a3b1 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -63,7 +63,7 @@ def runtime(context, addon_version, elapsed, single_file=True): class Profiler(object): """Class used to profile a block of code""" - __slots__ = ('__weakref__', '_enabled', '_profiler', '_reuse', 'name', ) + __slots__ = ('__weakref__', '_enabled', '_profiler', '_reuse', 'name',) from cProfile import Profile as _Profile from pstats import Stats as _Stats @@ -120,7 +120,7 @@ def __enter__(self): if not self._profiler: self._create_profiler() - def __exit__(self, exc_type=None, exc_value=None, traceback=None): + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): if not self._enabled: return diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index fff783f35..343c7acba 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -12,18 +12,18 @@ from .utils import to_json, from_json, to_jsons -from .uri_item import UriItem -from .base_item import BaseItem from .audio_item import AudioItem +from .base_item import BaseItem from .directory_item import DirectoryItem -from .watch_later_item import WatchLaterItem from .favorites_item import FavoritesItem -from .search_item import SearchItem +from .image_item import ImageItem from .new_search_item import NewSearchItem -from .search_history_item import SearchHistoryItem from .next_page_item import NextPageItem +from .search_history_item import SearchHistoryItem +from .search_item import SearchItem +from .uri_item import UriItem from .video_item import VideoItem -from .image_item import ImageItem +from .watch_later_item import WatchLaterItem __all__ = ('AudioItem', diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 2dee09a18..b648fc807 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -10,8 +10,8 @@ from __future__ import absolute_import, division, unicode_literals -import hashlib -import datetime +from datetime import date, datetime +from hashlib import md5 from ..compatibility import unescape @@ -58,7 +58,7 @@ def get_id(self): Returns a unique id of the item. :return: unique id of the item. """ - md5_hash = hashlib.md5() + md5_hash = md5() md5_hash.update(self._name.encode('utf-8')) md5_hash.update(self._uri.encode('utf-8')) return md5_hash.hexdigest() @@ -106,7 +106,7 @@ def replace_context_menu(self): return self._replace_context_menu def set_date(self, year, month, day, hour=0, minute=0, second=0): - self._date = datetime.datetime(year, month, day, hour, minute, second) + self._date = datetime(year, month, day, hour, minute, second) def set_date_from_datetime(self, date_time): self._date = date_time @@ -121,12 +121,12 @@ def get_date(self, as_text=False, short=False): return self._date def set_dateadded(self, year, month, day, hour=0, minute=0, second=0): - self._dateadded = datetime.datetime(year, - month, - day, - hour, - minute, - second) + self._dateadded = datetime(year, + month, + day, + hour, + minute, + second) def set_dateadded_from_datetime(self, date_time): self._dateadded = date_time diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 48836c372..18d4a041e 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -533,11 +533,11 @@ def get_http_server(address=None, port=None): server = BaseHTTPServer.HTTPServer((address, port), YouTubeProxyRequestHandler) return server - except socket_error as e: + except socket_error as exc: log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|' - .format(address=address, port=port, response=str(e))) + .format(address=address, port=port, response=str(exc))) xbmcgui.Dialog().notification(_addon_name, - str(e), + str(exc), _addon_icon, time=5000, sound=False) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 34d4a8545..939a07b3c 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -49,7 +49,7 @@ def __init__(self, exc_type=RequestException): def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self._session.close() def request(self, url, method='GET', diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index d23e8e48f..4b5cf9f19 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -10,6 +10,8 @@ from __future__ import absolute_import, division, unicode_literals +from traceback import format_exc + from ..abstract_provider_runner import AbstractProviderRunner from ...compatibility import xbmcgui, xbmcplugin from ...exceptions import KodionException @@ -46,10 +48,12 @@ def run(self, provider, context): try: results = provider.navigate(context) - except KodionException as ex: - if provider.handle_exception(context, ex): - context.log_error(ex.__str__()) - xbmcgui.Dialog().ok("Exception in ContentProvider", ex.__str__()) + except KodionException as exc: + if provider.handle_exception(context, exc): + context.log_error('XbmcRunner.run - {exc}:\n{details}'.format( + exc=exc, details=format_exc() + )) + xbmcgui.Dialog().ok("Error in ContentProvider", exc.__str__()) xbmcplugin.endOfDirectory(self.handle, succeeded=False) return False diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index 577d72bb2..682208539 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -73,12 +73,12 @@ def get_bool(self, setting, default=None, echo=None): error = False try: value = bool(self._get_bool(self._type(), setting)) - except (AttributeError, TypeError) as ex: - error = ex + except (AttributeError, TypeError) as exc: + error = exc value = self.get_string(setting, echo=False) value = AbstractSettings.VALUE_FROM_STR.get(value.lower(), default) - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc value = default if self._echo and echo is not False: @@ -95,8 +95,8 @@ def set_bool(self, setting, value, echo=None): error = not self._set_bool(self._type(), setting, value) if not error: self._cache[setting] = value - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc if self._echo and echo is not False: log_debug('Set |{setting}|: {value} (bool, {status})'.format( @@ -115,16 +115,16 @@ def get_int(self, setting, default=-1, process=None, echo=None): value = int(self._get_int(self._type(), setting)) if process: value = process(value) - except (AttributeError, TypeError, ValueError) as ex: - error = ex + except (AttributeError, TypeError, ValueError) as exc: + error = exc value = self.get_string(setting, echo=False) try: value = int(value) - except (TypeError, ValueError) as ex: - error = ex + except (TypeError, ValueError) as exc: + error = exc value = default - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc value = default if self._echo and echo is not False: @@ -141,8 +141,8 @@ def set_int(self, setting, value, echo=None): error = not self._set_int(self._type(), setting, value) if not error: self._cache[setting] = value - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc if self._echo and echo is not False: log_debug('Set |{setting}|: {value} (int, {status})'.format( @@ -159,8 +159,8 @@ def get_string(self, setting, default='', echo=None): error = False try: value = self._get_str(self._type(), setting) or default - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc value = default if self._echo and echo is not False: @@ -177,8 +177,8 @@ def set_string(self, setting, value, echo=None): error = not self._set_str(self._type(), setting, value) if not error: self._cache[setting] = value - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc if self._echo and echo is not False: log_debug('Set |{setting}|: "{value}" (str, {status})'.format( @@ -197,8 +197,8 @@ def get_string_list(self, setting, default=None, echo=None): value = self._get_str_list(self._type(), setting) if not value: value = [] if default is None else default - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc value = default if self._echo and echo is not False: @@ -215,8 +215,8 @@ def set_string_list(self, setting, value, echo=None): error = not self._set_str_list(self._type(), setting, value) if not error: self._cache[setting] = value - except RuntimeError as ex: - error = ex + except RuntimeError as exc: + error = exc if self._echo and echo is not False: log_debug('Set |{setting}|: "{value}" (str list, {status})'.format( diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 23f4b347a..f8a0ced86 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -69,7 +69,7 @@ def __enter__(self): self._open() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self._close() def _open(self): diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py index 3406a7ca4..dd6eb24f3 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py @@ -22,7 +22,7 @@ def __init__(self, dialog, heading, text, total=100): def __enter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self.close() def get_total(self): diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 77b4ecd8d..a1f00d46d 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -90,8 +90,8 @@ def _response_hook(**kwargs): raise YouTubeException('"error" in response JSON data', json_data=json_data, response=response) - except ValueError as error: - raise InvalidJSON(error, response=response) + except ValueError as exc: + raise InvalidJSON(exc, response=response) response.raise_for_status() return json_data diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index e484bcf54..0d9f65cce 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1200,8 +1200,8 @@ def _response_hook(**kwargs): raise YouTubeException('"error" in response JSON data', json_data=json_data, **kwargs) - except ValueError as error: - raise InvalidJSON(error, **kwargs) + except ValueError as exc: + raise InvalidJSON(exc, **kwargs) response.raise_for_status() return json_data diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index d2ccf5439..dc979a27e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -269,7 +269,7 @@ def update_playlist_infos(provider, context, playlist_id_dict, playlist_item.set_image(image) channel_id = snippet['channelId'] - # if the path directs to a playlist of our own, we correct the channel id to 'mine' + # if the path directs to a playlist of our own, set channel id to 'mine' if path == '/channel/mine/playlists/': channel_id = 'mine' channel_name = snippet.get('channelTitle', '') @@ -611,7 +611,7 @@ def update_video_infos(provider, context, video_id_dict, context_menu, context ) - # got to [CHANNEL], only if we are not directly in the channel provide a jump to the channel + # got to [CHANNEL] only if we are not directly in the channel if (channel_id and channel_name and create_path('channel', channel_id) != path): video_item.set_channel_id(channel_id) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 295c25efa..d92c2c2de 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -48,7 +48,6 @@ def _process_list_response(provider, context, json_data): use_play_data = not incognito and settings.use_local_history() for yt_item in yt_items: - is_youtube, kind = _parse_kind(yt_item) if not is_youtube or not kind: context.log_debug('v3 response: Item discarded, is_youtube=False') @@ -278,7 +277,8 @@ def _process_list_response(provider, context, json_data): else: raise KodionException("Unknown kind '%s'" % kind) - # this will also update the channel_id_dict with the correct channel id for each video. + # this will also update the channel_id_dict with the correct channel_id + # for each video. channel_items_dict = {} running = 0 diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 3fc2bea5e..c24d34546 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -10,10 +10,10 @@ from __future__ import absolute_import, division, unicode_literals +import json import random import re -import traceback -from json import dumps as json_dumps, loads as json_loads +from traceback import format_exc from .ratebypass import ratebypass from .signature.cipher import Cipher @@ -706,7 +706,7 @@ def _get_player_config(page): found = re.search(r'ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;', page.text) if found: - return json_loads(found.group(1)) + return json.loads(found.group(1)) return None def _get_player_js(self): @@ -930,9 +930,9 @@ def _process_signature_cipher(self, stream_map): if not signature: try: signature = self._cipher.get_signature(encrypted_signature) - except Exception as error: + except Exception as exc: self._context.log_debug('{0}: {1}\n{2}'.format( - error, encrypted_signature, traceback.format_exc() + exc, encrypted_signature, format_exc() )) self._context.log_error( 'Failed to extract URL from signatureCipher' @@ -1566,7 +1566,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): def _stream_sort(stream): if not stream: - return (1, ) + return (1,) return ( - stream['height'], diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 8274f5b58..484d6857e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -12,7 +12,7 @@ import json import random -import traceback +from traceback import format_exc from ..helper import utils, v3 from ..youtube_exceptions import YouTubeException @@ -46,9 +46,9 @@ def play_video(provider, context): try: video_streams = client.get_video_streams(context, video_id) - except YouTubeException as e: - ui.show_notification(message=e.get_message()) - context.log_error(traceback.print_exc()) + except YouTubeException as exc: + ui.show_notification(message=exc.get_message()) + context.log_error(format_exc()) return False if not video_streams: diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 2c40c1d3f..0041370fe 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -217,11 +217,11 @@ def get_client(self, context): access_manager.update_dev_access_token(dev_id, access_token, expires_in) else: access_manager.update_access_token(access_token, expires_in) - except (InvalidGrant, LoginException) as ex: - self.handle_exception(context, ex) + except (InvalidGrant, LoginException) as exc: + self.handle_exception(context, exc) access_tokens = ['', ''] # reset access_token - if isinstance(ex, InvalidGrant): + if isinstance(exc, InvalidGrant): if dev_id: access_manager.update_dev_access_token(dev_id, access_token='', refresh_token='') else: From da1b91a0220ccf44cc7fa06a438353dec91a88ba Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 21 Dec 2023 03:27:27 +1100 Subject: [PATCH 114/141] Further updates to SQL storage - Versioning for each type of storage - Table creation and update to occur once per class or if db is deleted - Use connection context managers to auto-commit/rollback - Transactions created manually where required - Restore Python 2 compatibility --- .../kodion/sql_store/data_cache.py | 14 +- .../kodion/sql_store/favorite_list.py | 8 +- .../kodion/sql_store/function_cache.py | 12 +- .../kodion/sql_store/playback_history.py | 16 +- .../kodion/sql_store/search_history.py | 8 +- .../kodion/sql_store/storage.py | 427 +++++++++++------- .../kodion/sql_store/watch_later_list.py | 8 +- 7 files changed, 310 insertions(+), 183 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index 06539b514..e0271f939 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -14,6 +14,11 @@ class DataCache(Storage): + _table_name = 'storage_v2' + _table_created = False + _table_updated = False + _sql = {} + def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 super(DataCache, self).__init__(filename, @@ -34,10 +39,7 @@ def set_item(self, content_id, item): self._set(content_id, item) def set_items(self, items): - self._set_all(items) - - def clear(self): - self._clear() + self._set_many(items) def remove(self, content_id): self._remove(content_id) @@ -45,5 +47,5 @@ def remove(self, content_id): def update(self, content_id, item): self._set(str(content_id), item) - def _optimize_item_count(self): - pass + def _optimize_item_count(self, limit=-1, defer=False): + return False diff --git a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py index 119cd95dd..44e72d220 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py @@ -15,12 +15,14 @@ class FavoriteList(Storage): + _table_name = 'storage_v2' + _table_created = False + _table_updated = False + _sql = {} + def __init__(self, filename): super(FavoriteList, self).__init__(filename) - def clear(self): - self._clear() - @staticmethod def _sort_item(item): return item.get_name().upper() diff --git a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index e244ba940..17e6a9682 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -17,6 +17,11 @@ class FunctionCache(Storage): + _table_name = 'storage_v2' + _table_created = False + _table_updated = False + _sql = {} + def __init__(self, filename, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 super(FunctionCache, self).__init__(filename, @@ -24,9 +29,6 @@ def __init__(self, filename, max_file_size_mb=5): self._enabled = True - def clear(self): - self._clear() - def enabled(self): """ Enables the caching @@ -90,7 +92,7 @@ def get(self, func, seconds, *args, **keywords): self._set(cache_id, data) return data - def _optimize_item_count(self): + def _optimize_item_count(self, limit=-1, defer=False): # override method Storage._optimize_item_count # for function cache do not optimize by item count, use database size. - pass + return False diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index a0eae31e4..56874ba97 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -13,6 +13,11 @@ class PlaybackHistory(Storage): + _table_name = 'storage_v2' + _table_created = False + _table_updated = False + _sql = {} + def __init__(self, filename): super(PlaybackHistory, self).__init__(filename) @@ -34,17 +39,14 @@ def get_item(self, key): result = self._get(key, process=self._add_last_played) return result - def clear(self): - self._clear() - def remove(self, video_id): self._remove(video_id) def update(self, video_id, play_data): self._set(video_id, play_data) - def _optimize_item_count(self): - pass + def _optimize_item_count(self, limit=-1, defer=False): + return False - def _optimize_file_size(self): - pass + def _optimize_file_size(self, limit=-1, defer=False): + return False diff --git a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py index a0f7046ea..b53110b32 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -16,6 +16,11 @@ class SearchHistory(Storage): + _table_name = 'storage_v2' + _table_created = False + _table_updated = False + _sql = {} + def __init__(self, filename, max_item_count=10): super(SearchHistory, self).__init__(filename, max_item_count=max_item_count) @@ -29,9 +34,6 @@ def get_items(self): values_only=True) return result - def clear(self): - self._clear() - @staticmethod def _make_id(search_text): md5_hash = md5() diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index f8a0ced86..0d990b709 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -15,7 +15,7 @@ import sqlite3 import time from datetime import datetime -from traceback import print_exc +from traceback import format_exc from ..logger import log_error from ..utils.datetime_parser import since_epoch @@ -28,20 +28,107 @@ class Storage(object): ONE_WEEK = 7 * ONE_DAY ONE_MONTH = 4 * ONE_WEEK - _table_name = 'storage_v3' - _clear_sql = 'DELETE FROM {table}'.format(table=_table_name) - _create_table_sql = 'CREATE TABLE IF NOT EXISTS {table} (key TEXT PRIMARY KEY, time REAL, value BLOB, size INTEGER)'.format(table=_table_name) - _drop_old_tables_sql = 'DELETE FROM sqlite_master WHERE type = "table" and name IS NOT "{table}"'.format(table=_table_name) - _get_sql = 'SELECT * FROM {table} WHERE key = ?'.format(table=_table_name) - _get_by_sql = 'SELECT * FROM {table} WHERE key in ({{0}})'.format(table=_table_name) - _get_all_asc_sql = 'SELECT * FROM {table} ORDER BY time ASC LIMIT {{0}}'.format(table=_table_name) - _get_all_desc_sql = 'SELECT * FROM {table} ORDER BY time DESC LIMIT {{0}}'.format(table=_table_name) - _is_empty_sql = 'SELECT EXISTS(SELECT 1 FROM {table} LIMIT 1)'.format(table=_table_name) - _optimize_item_sql = 'SELECT key FROM {table} ORDER BY time DESC LIMIT -1 OFFSET {{0}}'.format(table=_table_name) - _prune_sql = 'DELETE FROM {table} WHERE ROWID IN (SELECT ROWID FROM {table} WHERE (SELECT SUM(size) FROM {table} AS _ WHERE time<={table}.time) <= {{0}})'.format(table=_table_name) - _remove_sql = 'DELETE FROM {table} WHERE key = ?'.format(table=_table_name) - _remove_all_sql = 'DELETE FROM {table} WHERE key in ({{0}})'.format(table=_table_name) - _set_sql = 'REPLACE INTO {table} (key, time, value, size) VALUES(?, ?, ?, ?)'.format(table=_table_name) + _table_name = 'storage_v2' + _table_created = False + _table_updated = False + + _sql = { + 'clear': ( + 'DELETE' + ' FROM {table};' + ), + 'create_table': ( + 'CREATE TABLE' + ' IF NOT EXISTS {table} (' + ' key TEXT PRIMARY KEY,' + ' timestamp REAL,' + ' value BLOB,' + ' size INTEGER' + ' );' + ), + 'drop_old_table': ( + 'DELETE' + ' FROM sqlite_master' + ' WHERE type = "table"' + ' and name IS NOT "{table}";' + ), + 'get': ( + 'SELECT *' + ' FROM {table}' + ' WHERE key = ?;' + ), + 'get_by_key': ( + 'SELECT *' + ' FROM {table}' + ' WHERE key in ({{0}});' + ), + 'get_many': ( + 'SELECT *' + ' FROM {table}' + ' ORDER BY timestamp' + ' LIMIT {{0}};' + ), + 'get_many_desc': ( + 'SELECT *' + ' FROM {table}' + ' ORDER BY timestamp DESC' + ' LIMIT {{0}};' + ), + 'has_old_table': ( + 'SELECT EXISTS (' + ' SELECT 1' + ' FROM sqlite_master' + ' WHERE type = "table"' + ' and name IS NOT "{table}"' + ');' + ), + 'is_empty': ( + 'SELECT EXISTS (' + ' SELECT 1' + ' FROM {table}' + ');' + ), + 'prune_by_count': ( + 'DELETE' + ' FROM {table}' + ' WHERE rowid IN (' + ' SELECT rowid' + ' FROM {table}' + ' ORDER BY timestamp DESC' + ' LIMIT {{0}}' + ' OFFSET {{1}}' + ' );' + ), + 'prune_by_size': ( + 'DELETE' + ' FROM {table}' + ' WHERE rowid IN (' + ' SELECT rowid' + ' FROM {table}' + ' WHERE (' + ' SELECT SUM(size)' + ' FROM {table} AS _' + ' WHERE timestamp<={table}.timestamp' + ' ) <= {{0}}' + ' );' + ), + 'remove': ( + 'DELETE' + ' FROM {table}' + ' WHERE key = ?;' + ), + 'remove_by_key': ( + 'DELETE' + ' FROM {table}' + ' WHERE key in ({{0}});' + ), + 'set': ( + 'REPLACE' + ' INTO {table}' + ' (key, timestamp, value, size)' + ' VALUES(?, ?, ?, ?);' + ), + } def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._filename = filename @@ -52,9 +139,12 @@ def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._max_item_count = max_item_count self._max_file_size_kb = max_file_size_kb - self._table_created = False - self._table_updated = False - self._needs_commit = False + if not self._sql: + statements = { + name: sql.format(table=self._table_name) + for name, sql in Storage._sql.items() + } + self.__class__._sql.update(statements) def set_max_item_count(self, max_item_count): self._max_item_count = max_item_count @@ -63,61 +153,88 @@ def set_max_file_size_kb(self, max_file_size_kb): self._max_file_size_kb = max_file_size_kb def __del__(self): - self._close(True) + self._close() def __enter__(self): - self._open() - return self + if not self._db or not self._cursor: + self._open() + return self._db, self._cursor def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self._close() def _open(self): - if self._db: - if not self._cursor: - self._cursor = self._db.cursor() - return - - path = os.path.dirname(self._filename) - if not os.path.exists(path): - os.makedirs(path) + if not os.path.exists(self._filename): + os.makedirs(os.path.dirname(self._filename), exist_ok=True) + self.__class__._table_created = False + self.__class__._table_updated = True + + try: + db = sqlite3.connect(self._filename, + check_same_thread=False, + timeout=1, + isolation_level=None) + except sqlite3.OperationalError as exc: + log_error('SQLStorage._execute - {exc}:\n{details}'.format( + exc=exc, details=format_exc() + )) + return False - db = sqlite3.connect(self._filename, check_same_thread=False, - timeout=1, isolation_level=None) cursor = db.cursor() - # cursor.execute('PRAGMA journal_mode=MEMORY') - cursor.execute('PRAGMA journal_mode=WAL') - cursor.execute('PRAGMA busy_timeout=20000') - cursor.execute('PRAGMA read_uncommitted=TRUE') - cursor.execute('PRAGMA temp_store=MEMORY') - cursor.execute('PRAGMA page_size=4096') - # cursor.execute('PRAGMA synchronous=OFF') - cursor.execute('PRAGMA synchronous=NORMAL') - cursor.arraysize = 50 + cursor.arraysize = 100 + + sql_script = [ + 'PRAGMA journal_mode = WAL;', + 'PRAGMA busy_timeout = 20000;', + 'PRAGMA read_uncommitted = TRUE;', + 'PRAGMA temp_store = MEMORY;', + 'PRAGMA page_size = 4096;', + 'PRAGMA synchronous = NORMAL;', + 'PRAGMA mmap_size = 10485760;', + # 'PRAGMA locking_mode = EXCLUSIVE;' + 'PRAGMA cache_size = 500;' + ] + statements = [] + + if not self._table_created: + statements.append(self._sql['create_table']) + + if not self._table_updated: + for result in cursor.execute(self._sql['has_old_table']): + if result[0] == 1: + statements.extend(( + 'PRAGMA writable_schema = 1;', + self._sql['drop_old_table'], + 'PRAGMA writable_schema = 0;', + )) + break + + if statements: + transaction_begin = len(sql_script) + 1 + sql_script.extend(('BEGIN;', 'COMMIT;', 'VACUUM;')) + sql_script[transaction_begin:transaction_begin] = statements + cursor.executescript('\n'.join(sql_script)) + + self.__class__._table_created = True + self.__class__._table_updated = True self._db = db self._cursor = cursor - self._create_table() - self._drop_old_tables() - self._optimize_file_size() + def _close(self): + if self._cursor: + self._execute(self._cursor, 'PRAGMA optimize') + self._cursor.close() + self._cursor = None + if self._db: + # Not needed if using self._db as a context manager + # self._db.commit() + self._db.close() + self._db = None - def _drop_old_tables(self): - if self._table_updated: - return - self._execute(True, 'PRAGMA writable_schema=1') - self._execute(True, self._drop_old_tables_sql) - self._execute(True, 'PRAGMA writable_schema=0') - self._sync() - self._execute(False, 'VACUUM') - self._table_updated = True - - def _execute(self, needs_commit, query, values=None, many=False): + @staticmethod + def _execute(cursor, query, values=None, many=False): if values is None: values = () - if not self._needs_commit and needs_commit: - self._needs_commit = True - self._cursor.execute('BEGIN') - """ Tests revealed that sqlite has problems to release the database in time This happens no so often, but just to be sure, we try at least 3 times @@ -126,99 +243,95 @@ def _execute(self, needs_commit, query, values=None, many=False): for _ in range(3): try: if many: - return self._cursor.executemany(query, values) - return self._cursor.execute(query, values) - except TypeError: - log_error('SQLStorage._execute - |{0}|'.format(print_exc())) - return [] - except: - log_error('SQLStorage._execute - |{0}|'.format(print_exc())) + return cursor.executemany(query, values) + return cursor.execute(query, values) + except sqlite3.OperationalError as exc: + log_error('SQLStorage._execute - {exc}:\n{details}'.format( + exc=exc, details=format_exc() + )) time.sleep(0.1) + except sqlite3.Error as exc: + log_error('SQLStorage._execute - {exc}:\n{details}'.format( + exc=exc, details=format_exc() + )) + return [] return [] - def _close(self, full=False): - if not self._db: - return - - if self._cursor: - self._sync() - self._db.commit() - self._cursor.close() - self._cursor = None - if full: - self._db.close() - self._db = None - - def _optimize_file_size(self): - # do nothing - only we have given a size + def _optimize_file_size(self, defer=False): + # do nothing - optimize only if max size limit has been set if self._max_file_size_kb <= 0: - return - - # do nothing - only if this db exists - if not os.path.exists(self._filename): - return + return False - file_size_kb = (os.path.getsize(self._filename) // 1024) - if file_size_kb <= self._max_file_size_kb: - return + try: + file_size_kb = (os.path.getsize(self._filename) // 1024) + if file_size_kb <= self._max_file_size_kb: + return False + except OSError: + return False prune_size = 1024 * int(file_size_kb - self._max_file_size_kb / 2) - query = self._prune_sql.format(prune_size) - self._execute(True, query) - self._sync() - self._execute(False, 'VACUUM') - - def _create_table(self): - if self._table_created: - return - self._execute(True, self._create_table_sql) - self._table_created = True - - def _sync(self): - if not self._needs_commit: - return None - self._needs_commit = False - return self._execute(False, 'COMMIT') + query = self._sql['prune_by_size'].format(prune_size) + if defer: + return query + with self as (db, cursor), db: + self._execute(cursor, query) + self._execute(cursor, 'VACUUM') + return True + + def _optimize_item_count(self, limit=-1, defer=False): + # do nothing - optimize only if max item limit has been set + if self._max_item_count < 0: + return False - def _set(self, item_id, item): - now = since_epoch(datetime.now()) - with self as db: - db._execute(True, db._set_sql, - values=[str(item_id), now, *db._encode(item)]) - self._optimize_item_count() + # clear db if max item count has been set to 0 + if not self._max_item_count: + if not self._is_empty(): + return self.clear(defer) + return False + + query = self._sql['prune_by_count'].format( + limit, self._max_item_count + ) + if defer: + return query + with self as (db, cursor), db: + self._execute(cursor, query) + self._execute(cursor, 'VACUUM') + return True - def _set_all(self, items): + def _set(self, item_id, item): + values = self._encode(item_id, item) + optimize_query = self._optimize_item_count(1, defer=True) + with self as (db, cursor), db: + if optimize_query: + self._execute(cursor, 'BEGIN') + self._execute(cursor, optimize_query) + self._execute(cursor, self._sql['set'], values=values) + + def _set_many(self, items): now = since_epoch(datetime.now()) - with self as db: - db._execute(True, db._set_sql, many=True, - values=[(str(item_id), now, *db._encode(item)) - for item_id, item in items.items()]) - self._optimize_item_count() + values = [self._encode(*item, timestamp=now) + for item in items.items()] + optimize_query = self._optimize_item_count(len(items), defer=True) + with self as (db, cursor), db: + self._execute(cursor, 'BEGIN') + if optimize_query: + self._execute(cursor, optimize_query) + self._execute(cursor, self._sql['set'], many=True, values=values) + self._optimize_file_size() - def _optimize_item_count(self): - if not self._max_item_count: - if not self._is_empty(): - self._clear() - return - if self._max_item_count < 0: - return - query = self._optimize_item_sql.format(self._max_item_count) - with self as db: - item_ids = db._execute(False, query) - item_ids = [item_id[0] for item_id in item_ids] - if item_ids: - db._remove_all(item_ids) - - def _clear(self): - with self as db: - db._execute(True, db._clear_sql) - db._create_table() - db._sync() - db._execute(False, 'VACUUM') + def clear(self, defer=False): + query = self._sql['clear'] + if defer: + return query + with self as (db, cursor), db: + self._execute(cursor, query) + self._execute(cursor, 'VACUUM') + return True def _is_empty(self): - with self as db: - result = db._execute(False, db._is_empty_sql) + with self as (db, cursor), db: + result = self._execute(cursor, self._sql['is_empty']) for item in result: is_empty = item[0] == 0 break @@ -234,17 +347,17 @@ def _decode(obj, process=None, item=None): return decoded_obj @staticmethod - def _encode(obj): + def _encode(key, obj, timestamp=None): + timestamp = timestamp or since_epoch(datetime.now()) blob = sqlite3.Binary(pickle.dumps( obj, protocol=pickle.HIGHEST_PROTOCOL )) size = getattr(blob, 'nbytes', None) or blob.itemsize * len(blob) - return blob, size - + return str(key), timestamp, blob, size def _get(self, item_id, process=None, seconds=None): - with self as db: - result = db._execute(False, db._get_sql, [str(item_id)]) + with self as (db, cursor), db: + result = self._execute(cursor, self._sql['get'], [str(item_id)]) item = result.fetchone() if result else None if not item: return None @@ -258,45 +371,47 @@ def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, as_dict=False, values_only=False): if not item_ids: if oldest_first: - query = self._get_all_asc_sql + query = self._sql['get_many'] else: - query = self._get_all_desc_sql + query = self._sql['get_many_desc'] query = query.format(limit) else: num_ids = len(item_ids) - query = self._get_by_sql.format(('?,' * (num_ids - 1)) + '?') + query = self._sql['get_by_key'].format(('?,' * (num_ids - 1)) + '?') item_ids = tuple(item_ids) - with self as db: - result = db._execute(False, query, item_ids) + with self as (db, cursor), db: + result = self._execute(cursor, query, item_ids) cut_off = since_epoch(datetime.now()) - seconds if seconds else 0 if as_dict: result = { - item[0]: db._decode(item[2], process, item) + item[0]: self._decode(item[2], process, item) for item in result if not cut_off or item[1] >= cut_off } elif values_only: result = [ - db._decode(item[2], process, item) + self._decode(item[2], process, item) for item in result if not cut_off or item[1] >= cut_off ] else: result = [ (item[0], self._convert_timestamp(item[1]), - db._decode(item[2], process, item)) + self._decode(item[2], process, item)) for item in result if not cut_off or item[1] >= cut_off ] return result def _remove(self, item_id): - with self as db: - db._execute(True, db._remove_sql, [item_id]) + with self as (db, cursor), db: + self._execute(cursor, self._sql['remove'], [item_id]) - def _remove_all(self, item_ids): + def _remove_many(self, item_ids): num_ids = len(item_ids) - query = self._remove_all_sql.format(('?,' * (num_ids - 1)) + '?') - self._execute(True, query, tuple(item_ids)) + query = self._sql['remove_by_key'].format(('?,' * (num_ids - 1)) + '?') + with self as (db, cursor), db: + self._execute(cursor, query, tuple(item_ids)) + self._execute(cursor, 'VACUUM') @classmethod def _convert_timestamp(cls, val): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index 70a1b7f9e..1618a5570 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -17,12 +17,14 @@ class WatchLaterList(Storage): + _table_name = 'storage_v2' + _table_created = False + _table_updated = False + _sql = {} + def __init__(self, filename): super(WatchLaterList, self).__init__(filename) - def clear(self): - self._clear() - def get_items(self): result = self._get_by_ids(process=from_json, values_only=True) return result From 8b1a57626ab611c9d52f17d6a0311d257981645e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 21 Dec 2023 03:57:31 +1100 Subject: [PATCH 115/141] Update ResourceManager and _process_list_response - Defer cache store until all data has been fetched/updated - Improve deferred thread creation sequence --- .../youtube/helper/resource_manager.py | 49 +++--- .../lib/youtube_plugin/youtube/helper/v3.py | 146 +++++++++++------- 2 files changed, 122 insertions(+), 73 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index a685217ae..ffcbb7cf9 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -20,6 +20,7 @@ def __init__(self, context, client): self._show_fanart = context.get_settings().get_bool( 'youtube.channel.fanart.show', True ) + self.new_data = {} @staticmethod def _list_batch(input_list, n=50): @@ -32,7 +33,7 @@ def clear(self): self._func_cache.clear() self._data_cache.clear() - def get_channels(self, ids): + def get_channels(self, ids, defer_cache=False): updated = [] for channel_id in ids: if not channel_id: @@ -80,9 +81,7 @@ def get_channels(self, ids): if yt_item } result.update(new_data) - self._data_cache.set_items(new_data) - self._context.log_debug('Cached data for channels:\n|{ids}|' - .format(ids=list(new_data))) + self.cache_data(new_data, defer=defer_cache) # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ @@ -95,11 +94,11 @@ def get_channels(self, ids): return result - def get_fanarts(self, channel_ids): + def get_fanarts(self, channel_ids, defer_cache=False): if not self._show_fanart: return {} - result = self.get_channels(channel_ids) + result = self.get_channels(channel_ids, defer_cache=defer_cache) banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', 'bannerTvImageUrl', 'bannerExternalUrl'] # transform @@ -117,7 +116,7 @@ def get_fanarts(self, channel_ids): return result - def get_playlists(self, ids): + def get_playlists(self, ids, defer_cache=False): ids = tuple(ids) result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH) to_update = [id_ for id_ in ids if id_ not in result] @@ -144,9 +143,7 @@ def get_playlists(self, ids): if yt_item } result.update(new_data) - self._data_cache.set_items(new_data) - self._context.log_debug('Cached data for playlists:\n|{ids}|' - .format(ids=list(new_data))) + self.cache_data(new_data, defer=defer_cache) # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ @@ -159,7 +156,7 @@ def get_playlists(self, ids): return result - def get_playlist_items(self, ids=None, batch_id=None): + def get_playlist_items(self, ids=None, batch_id=None, defer_cache=False): if not ids and not batch_id: return None @@ -214,10 +211,8 @@ def get_playlist_items(self, ids=None, batch_id=None): to_update = list(new_data) self._context.log_debug('Got items for playlists:\n|{ids}|' .format(ids=to_update)) - self._data_cache.set_items(new_data) result.update(new_data) - self._context.log_debug('Cached items for playlists:\n|{ids}|' - .format(ids=to_update)) + self.cache_data(new_data, defer=defer_cache) # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ @@ -230,8 +225,8 @@ def get_playlist_items(self, ids=None, batch_id=None): return result - def get_related_playlists(self, channel_id): - result = self.get_channels([channel_id]) + def get_related_playlists(self, channel_id, defer_cache=False): + result = self.get_channels([channel_id], defer_cache=defer_cache) # transform item = None @@ -247,7 +242,11 @@ def get_related_playlists(self, channel_id): return item.get('contentDetails', {}).get('relatedPlaylists', {}) - def get_videos(self, ids, live_details=False, suppress_errors=False): + def get_videos(self, + ids, + live_details=False, + suppress_errors=False, + defer_cache=False): ids = tuple(ids) result = self._data_cache.get_items(ids, self._data_cache.ONE_MONTH) to_update = [id_ for id_ in ids if id_ not in result] @@ -277,9 +276,7 @@ def get_videos(self, ids, live_details=False, suppress_errors=False): for yt_item in batch.get('items', []) }) result.update(new_data) - self._data_cache.set_items(new_data) - self._context.log_debug('Cached data for videos:\n|{ids}|' - .format(ids=list(new_data))) + self.cache_data(new_data, defer=defer_cache) # Re-sort result to match order of requested IDs # Will only work in Python v3.7+ @@ -297,3 +294,15 @@ def get_videos(self, ids, live_details=False, suppress_errors=False): result[video_id]['play_data'] = play_data return result + + def cache_data(self, data=None, defer=False): + if defer: + if data: + self.new_data.update(data) + return + + data = data or self.new_data + if data: + self._data_cache.set_items(data) + self._context.log_debug('Cached data for items:\n|{ids}|' + .format(ids=list(data))) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index d92c2c2de..447db2d4b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -281,102 +281,142 @@ def _process_list_response(provider, context, json_data): # for each video. channel_items_dict = {} - running = 0 resource_manager = provider.get_resource_manager(context) - resources = [ - { + resources = { + 1: { 'fetcher': resource_manager.get_videos, - 'args': (video_id_dict.keys(), ), - 'kwargs': {'live_details': True, 'suppress_errors': True}, + 'args': (video_id_dict,), + 'kwargs': { + 'live_details': True, + 'suppress_errors': True, + 'defer_cache': True, + }, 'thread': None, 'updater': update_video_infos, - 'upd_args': (provider, context, video_id_dict, playlist_item_id_dict, channel_items_dict), - 'upd_kwargs': {'data': None, 'live_details': True, 'use_play_data': use_play_data}, + 'upd_args': ( + provider, + context, + video_id_dict, + playlist_item_id_dict, + channel_items_dict, + ), + 'upd_kwargs': { + 'data': None, + 'live_details': True, + 'use_play_data': use_play_data + }, 'complete': False, 'defer': False, }, - { + 2: { 'fetcher': resource_manager.get_playlists, - 'args': (playlist_id_dict.keys(), ), - 'kwargs': {}, + 'args': (playlist_id_dict,), + 'kwargs': {'defer_cache': True}, 'thread': None, 'updater': update_playlist_infos, - 'upd_args': (provider, context, playlist_id_dict, channel_items_dict), + 'upd_args': ( + provider, + context, + playlist_id_dict, + channel_items_dict, + ), 'upd_kwargs': {'data': None}, 'complete': False, 'defer': False, }, - { + 3: { 'fetcher': resource_manager.get_channels, - 'args': (channel_id_dict.keys(), ), - 'kwargs': {}, + 'args': (channel_id_dict,), + 'kwargs': {'defer_cache': True}, 'thread': None, 'updater': update_channel_infos, - 'upd_args': (provider, context, channel_id_dict, subscription_id_dict, channel_items_dict), + 'upd_args': ( + provider, + context, + channel_id_dict, + subscription_id_dict, + channel_items_dict, + ), 'upd_kwargs': {'data': None}, 'complete': False, 'defer': False, }, - { + 4: { 'fetcher': resource_manager.get_fanarts, - 'args': (channel_items_dict.keys(), ), - 'kwargs': {}, + 'args': (channel_items_dict,), + 'kwargs': {'defer_cache': True}, 'thread': None, 'updater': update_fanarts, - 'upd_args': (provider, context, channel_items_dict), + 'upd_args': ( + provider, + context, + channel_items_dict, + ), 'upd_kwargs': {'data': None}, 'complete': False, 'defer': True, }, - ] + 5: { + 'fetcher': resource_manager.cache_data, + 'args': (), + 'kwargs': {}, + 'thread': None, + 'updater': None, + 'upd_args': (), + 'upd_kwargs': {}, + 'complete': False, + 'defer': 4, + }, + } def _fetch(resource): data = resource['fetcher']( *resource['args'], **resource['kwargs'] ) - if not data: + if not data or not resource['updater']: return resource['upd_kwargs']['data'] = data resource['updater'](*resource['upd_args'], **resource['upd_kwargs']) - for resource in resources: - if resource['defer']: - running += 1 + remaining = len(resources) + deferred = sum(1 for resource in resources.values() if resource['defer']) + iterator = iter(resources.values()) + while remaining: + try: + resource = next(iterator) + except StopIteration: + iterator = iter(resources.values()) + resource = next(iterator) + + if resource['complete']: continue - if not resource['args'][0]: + defer = resource['defer'] + if defer: + if remaining > deferred: + continue + if defer in resources and not resources[defer]['complete']: + continue + resource['defer'] = False + + args = resource['args'] + if args and not args[0]: resource['complete'] = True + remaining -= 1 continue - running += 1 - # _fetch(resource) - thread = Thread(target=_fetch, args=(resource, )) - thread.daemon = True - thread.start() - resource['thread'] = thread - - while running > 0: - for resource in resources: - if resource['complete']: - continue - - thread = resource['thread'] - if thread: - thread.join(30) - if not thread.is_alive(): - resource['thread'] = None - resource['complete'] = True - running -= 1 - elif resource['defer']: - resource['defer'] = False - # _fetch(resource) - thread = Thread(target=_fetch, args=(resource, )) - thread.daemon = True - thread.start() - resource['thread'] = thread - else: - running -= 1 + thread = resource['thread'] + if thread: + thread.join(1) + if not thread.is_alive(): + resource['thread'] = None resource['complete'] = True + remaining -= 1 + else: + thread = Thread(target=_fetch, args=(resource,)) + thread.daemon = True + thread.start() + resource['thread'] = thread return result From f8812410c22d4d3a6a36c7098311bf2181b2ee1f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 21 Dec 2023 04:19:20 +1100 Subject: [PATCH 116/141] Create Session as class variable in BaseRequestsClass - Create once, close pools when not needed --- .../lib/youtube_plugin/kodion/network/requests.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 939a07b3c..ced557ab2 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -25,7 +25,7 @@ class BaseRequestsClass(object): - http_adapter = HTTPAdapter( + _http_adapter = HTTPAdapter( pool_maxsize=10, pool_block=True, max_retries=Retry( @@ -36,15 +36,17 @@ class BaseRequestsClass(object): ) ) + _session = Session() + _session.mount('https://', _http_adapter) + atexit.register(_session.close) + def __init__(self, exc_type=RequestException): self._verify = _settings.verify_ssl() self._timeout = _settings.get_timeout() self._default_exc = exc_type - self._session = Session() - self._session.verify = self._verify - self._session.mount('https://', self.http_adapter) - atexit.register(self._session.close) + def __del__(self): + self._session.close() def __enter__(self): return self From 658892745c3022829cc8701cecd2a20aded8adec Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 21 Dec 2023 04:21:10 +1100 Subject: [PATCH 117/141] Set _is_action in DirectoryItem.__init__ --- resources/lib/youtube_plugin/kodion/items/directory_item.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/directory_item.py b/resources/lib/youtube_plugin/kodion/items/directory_item.py index 2738c0b03..8b26ae4e5 100644 --- a/resources/lib/youtube_plugin/kodion/items/directory_item.py +++ b/resources/lib/youtube_plugin/kodion/items/directory_item.py @@ -14,10 +14,10 @@ class DirectoryItem(BaseItem): - def __init__(self, name, uri, image='', fanart=''): + def __init__(self, name, uri, image='', fanart='', action=False): super(DirectoryItem, self).__init__(name, uri, image, fanart) self._plot = self.get_name() - self._is_action = False + self._is_action = action self._channel_subscription_id = None self._channel_id = None From 421ec800ffe766af848bc9adc858940b8a7ef927 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 21 Dec 2023 05:56:11 +1100 Subject: [PATCH 118/141] Add local Watch Later and History lists - Make items.utils.to_json a method of BaseItem - Also fix to allow handling of date/datetime objects - Will only work in Python 3.7+ - TODO: fix date parsing for older versions of Python - TODO: update context menu items - TODO: fix serialisation prior to updating items --- .../resource.language.en_au/strings.po | 8 + .../resource.language.en_gb/strings.po | 8 + .../resource.language.en_nz/strings.po | 8 + .../resource.language.en_us/strings.po | 8 + .../kodion/abstract_provider.py | 145 ++++++--- .../kodion/constants/const_paths.py | 1 + .../kodion/context/abstract_context.py | 3 +- .../kodion/context/xbmc/xbmc_context.py | 6 +- .../youtube_plugin/kodion/items/__init__.py | 7 +- .../youtube_plugin/kodion/items/base_item.py | 15 + .../lib/youtube_plugin/kodion/items/utils.py | 59 ++-- .../kodion/sql_store/favorite_list.py | 11 +- .../kodion/sql_store/watch_later_list.py | 15 +- .../lib/youtube_plugin/youtube/provider.py | 276 ++++++++++++------ 14 files changed, 373 insertions(+), 197 deletions(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index de411ad0c..3002b35d3 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -1372,3 +1372,11 @@ msgstr "" msgctxt "#30768" msgid "Disable high framerate video at maximum video quality" msgstr "" + +msgctxt "#30769" +msgid "Clear Watch Later list" +msgstr "" + +msgctxt "#30770" +msgid "Are you sure you want to clear your Watch Later list?" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 831ef1a2b..fb9fd6986 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1372,3 +1372,11 @@ msgstr "" msgctxt "#30768" msgid "Disable high framerate video at maximum video quality" msgstr "" + +msgctxt "#30769" +msgid "Clear Watch Later list" +msgstr "" + +msgctxt "#30770" +msgid "Are you sure you want to clear your Watch Later list?" +msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index 15c435e76..34c126c5b 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -1368,3 +1368,11 @@ msgstr "" msgctxt "#30768" msgid "Disable high framerate video at maximum video quality" msgstr "" + +msgctxt "#30769" +msgid "Clear Watch Later list" +msgstr "" + +msgctxt "#30770" +msgid "Are you sure you want to clear your Watch Later list?" +msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index e33ce18a0..82767d56b 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -1373,3 +1373,11 @@ msgstr "" msgctxt "#30768" msgid "Disable high framerate video at maximum video quality" msgstr "" + +msgctxt "#30769" +msgid "Clear Watch Later list" +msgstr "" + +msgctxt "#30770" +msgid "Are you sure you want to clear your Watch Later list?" +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 4f543e10d..34b7c6322 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -import json import re from . import constants @@ -20,8 +19,6 @@ DirectoryItem, NewSearchItem, SearchHistoryItem, - from_json, - to_jsons, ) from .utils import to_unicode @@ -38,12 +35,33 @@ def __init__(self): # register some default paths self.register_path(r'^/$', '_internal_root') - self.register_path(r''.join(['^/', constants.paths.WATCH_LATER, '/(?Padd|remove|list)/?$']), - '_internal_watch_later') - self.register_path(r''.join(['^/', constants.paths.FAVORITES, '/(?Padd|remove|list)/?$']), '_internal_favorite') - self.register_path(r''.join(['^/', constants.paths.SEARCH, '/(?Pinput|query|list|remove|clear|rename)/?$']), - '_internal_search') - self.register_path(r'(?P.*\/)extrafanart\/([\?#].+)?$', '_internal_on_extra_fanart') + + self.register_path(r''.join([ + '^/', + constants.paths.WATCH_LATER, + '/(?Padd|clear|list|remove)/?$' + ]), '_internal_watch_later') + + self.register_path(r''.join([ + '^/', + constants.paths.FAVORITES, + '/(?Padd|clear|list|remove)/?$' + ]), '_internal_favorite') + + self.register_path(r''.join([ + '^/', + constants.paths.SEARCH, + '/(?Pinput|query|list|remove|clear|rename)/?$' + ]), '_internal_search') + + self.register_path(r''.join([ + '^/', + constants.paths.HISTORY, + '/$' + ]), 'on_playback_history') + + self.register_path(r'(?P.*\/)extrafanart\/([\?#].+)?$', + '_internal_on_extra_fanart') """ Test each method of this class for the appended attribute '_re_match' by the @@ -122,70 +140,107 @@ def _internal_on_extra_fanart(self, context, re_match): new_context = context.clone(new_path=path) return self.on_extra_fanart(new_context, re_match) + def on_playback_history(self, context, re_match): + raise NotImplementedError() + def on_search(self, search_text, context, re_match): raise NotImplementedError() def on_root(self, context, re_match): raise NotImplementedError() - def on_watch_later(self, context, re_match): - pass - def _internal_root(self, context, re_match): return self.on_root(context, re_match) @staticmethod def _internal_favorite(context, re_match): params = context.get_params() - command = re_match.group('command') - if command == 'add': - fav_item = from_json(params['item']) - context.get_favorite_list().add(fav_item) - return None - if command == 'remove': - fav_item = from_json(params['item']) - context.get_favorite_list().remove(fav_item) - context.get_ui().refresh_container() - return None + if not command: + return False + if command == 'list': - directory_items = context.get_favorite_list().get_items() + items = context.get_favorite_list().get_items() - for directory_item in directory_items: - context_menu = [(context.localize('watch_later.remove'), - 'RunPlugin(%s)' % context.create_uri([constants.paths.FAVORITES, 'remove'], - params={'item': to_jsons(directory_item)}))] - directory_item.set_context_menu(context_menu) + for item in items: + context_menu = [( + context.localize('favorites.remove'), + 'RunPlugin(%s)' % context.create_uri( + [constants.paths.FAVORITES, 'remove'], + params={'item_id': item.get_id()} + ) + )] + item.set_context_menu(context_menu) - return directory_items - return None + return items - def _internal_watch_later(self, context, re_match): - self.on_watch_later(context, re_match) + video_id = params.get('video_id') + if not video_id: + return False - params = context.get_params() - - command = re_match.group('command') if command == 'add': - item = from_json(params['item']) - context.get_watch_later_list().add(item) - return None + item = params.get('item') + if item: + context.get_favorite_list().add(video_id, item) + return True + if command == 'remove': - item = from_json(params['item']) - context.get_watch_later_list().remove(item) + context.get_favorite_list().remove(video_id) context.get_ui().refresh_container() - return None + return True + + return False + + def _internal_watch_later(self, context, re_match): + params = context.get_params() + command = re_match.group('command') + if not command: + return False + if command == 'list': video_items = context.get_watch_later_list().get_items() for video_item in video_items: - context_menu = [(context.localize('watch_later.remove'), - 'RunPlugin(%s)' % context.create_uri([constants.paths.WATCH_LATER, 'remove'], - params={'item': to_jsons(video_item)}))] + context_menu = [( + context.localize('watch_later.remove'), + 'RunPlugin(%s)' % context.create_uri( + [constants.paths.WATCH_LATER, 'remove'], + params={'item_id': video_item.get_id()} + ) + ), ( + context.localize('watch_later.clear'), + 'RunPlugin(%s)' % context.create_uri( + [constants.paths.WATCH_LATER, 'clear'] + ) + )] video_item.set_context_menu(context_menu) return video_items - return None + + if (command == 'clear' and context.get_ui().on_yes_no_input( + context.get_name(), + context.localize('watch_later.clear.confirm') + )): + context.get_watch_later_list().clear() + context.get_ui().refresh_container() + return True + + video_id = params.get('video_id') + if not video_id: + return False + + if command == 'add': + item = params.get('item') + if item: + context.get_watch_later_list().add(video_id, item) + return True + + if command == 'remove': + context.get_watch_later_list().remove(video_id) + context.get_ui().refresh_container() + return True + + return False @property def data_cache(self): diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index aad8c4752..b0ba36f27 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -14,3 +14,4 @@ SEARCH = 'kodion/search' FAVORITES = 'kodion/favorites' WATCH_LATER = 'kodion/watch_later' +HISTORY = 'kodion/playback_history' diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 576384437..1f3851bc5 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -64,7 +64,7 @@ class AbstractContext(object): } _STRING_PARAMS = { 'api_key', - 'action', # deprecated + 'action', 'addon_id', 'channel_id', 'channel_name', @@ -72,6 +72,7 @@ class AbstractContext(object): 'client_secret', 'event_type', 'item', + 'item_id', 'next_page_token', 'page_token', 'parent_id', diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 8472372a9..027ee84ab 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -48,8 +48,6 @@ class XbmcContext(AbstractContext): 'cache.function': 30557, 'cancel': 30615, 'channels': 30500, - 'clear_history': 30609, - 'clear_history_confirmation': 30610, 'client.id.incorrect': 30649, 'client.ip': 30700, 'client.ip.failed': 30701, @@ -88,6 +86,8 @@ class XbmcContext(AbstractContext): 'go_to_channel': 30502, 'highlights': 30104, 'history': 30509, + 'history.clear': 30609, + 'history.clear.confirm': 30610, 'history.list.remove': 30572, 'history.list.remove.confirm': 30573, 'history.list.set': 30571, @@ -230,6 +230,8 @@ class XbmcContext(AbstractContext): 'watch_later': 30107, 'watch_later.add': 30107, 'watch_later.added_to': 30713, + 'watch_later.clear': 30769, + 'watch_later.clear.confirm': 30770, 'watch_later.list.remove': 30568, 'watch_later.list.remove.confirm': 30569, 'watch_later.list.set': 30567, diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index 343c7acba..5cba8f9e2 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -10,8 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -from .utils import to_json, from_json, to_jsons - from .audio_item import AudioItem from .base_item import BaseItem from .directory_item import DirectoryItem @@ -22,6 +20,7 @@ from .search_history_item import SearchHistoryItem from .search_item import SearchItem from .uri_item import UriItem +from .utils import from_json from .video_item import VideoItem from .watch_later_item import WatchLaterItem @@ -38,6 +37,4 @@ 'UriItem', 'VideoItem', 'WatchLaterItem', - 'from_json', - 'to_json', - 'to_jsons') + 'from_json',) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index b648fc807..9e71bb88a 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals +import json from datetime import date, datetime from hashlib import md5 @@ -53,6 +54,20 @@ def __str__(self): obj_str = "------------------------------\n'%s'\nURI: %s\nImage: %s\n------------------------------" % (name, uri, image) return obj_str + def to_dict(self): + return {'type': self.__class__.__name__, 'data': self.__dict__} + + def dumps(self): + def _encoder(obj): + if isinstance(obj, (date, datetime)): + return { + '__class__': obj.__class__.__name__, + '__isoformat__': obj.isoformat(), + } + return json.JSONEncoder().default(obj) + + return json.dumps(self.to_dict(), ensure_ascii=False, default=_encoder) + def get_id(self): """ Returns a unique id of the item. diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index a5a5971a1..7c985b61b 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -11,6 +11,7 @@ from __future__ import absolute_import, division, unicode_literals import json +from datetime import date, datetime from .audio_item import AudioItem from .directory_item import DirectoryItem @@ -26,51 +27,33 @@ } +def _decoder(obj): + date_in_isoformat = obj.get('__isoformat__') + if not date_in_isoformat: + return obj + + if obj['__class__'] == 'date': + return date.fromisoformat(date_in_isoformat) + return datetime.fromisoformat(date_in_isoformat) + + def from_json(json_data, *_args): """ - Creates a instance of the given json dump or dict. + Creates an instance of the given json dump or dict. :param json_data: :return: """ - - def _from_json(_json_data): - item_type = _json_data.get('type') - if not item_type or item_type not in _ITEM_TYPES: - return _json_data - - item = _ITEM_TYPES[item_type]() - - data = _json_data.get('data', {}) - for key, value in data.items(): - if hasattr(item, key): - setattr(item, key, value) - - return item - if isinstance(json_data, str): - json_data = json.loads(json_data) - return _from_json(json_data) - - -def to_jsons(base_item): - return json.dumps(to_json(base_item)) - - -def to_json(base_item): - """ - Convert the given @base_item to json - :param base_item: - :return: json string - """ + json_data = json.loads(json_data, object_hook=_decoder) - def _to_json(obj): - if isinstance(obj, dict): - return obj.__dict__ + item_type = json_data.get('type') + if not item_type or item_type not in _ITEM_TYPES: + return json_data - for name, item_type in _ITEM_TYPES.items(): - if isinstance(obj, item_type): - return {'type': name, 'data': obj.__dict__} + item = _ITEM_TYPES[item_type](name='', uri='') - return obj.__dict__ + for key, value in json_data.get('data', {}).items(): + if hasattr(item, key): + setattr(item, key, value) - return _to_json(base_item) + return item diff --git a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py index 44e72d220..fc84cbae0 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py @@ -11,7 +11,7 @@ from __future__ import absolute_import, division, unicode_literals from .storage import Storage -from ..items import from_json, to_json +from ..items import from_json class FavoriteList(Storage): @@ -31,9 +31,8 @@ def get_items(self): result = self._get_by_ids(process=from_json, values_only=True) return sorted(result, key=self._sort_item, reverse=False) - def add(self, base_item): - item_json_data = to_json(base_item) - self._set(base_item.get_id(), item_json_data) + def add(self, item_id, item): + self._set(item_id, item) - def remove(self, base_item): - self._remove(base_item.get_id()) + def remove(self, item_id): + self._remove(item_id) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index 1618a5570..39274f029 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -10,10 +10,8 @@ from __future__ import absolute_import, division, unicode_literals -from datetime import datetime - from .storage import Storage -from ..items import to_json, from_json +from ..items import from_json class WatchLaterList(Storage): @@ -29,11 +27,8 @@ def get_items(self): result = self._get_by_ids(process=from_json, values_only=True) return result - def add(self, base_item): - base_item.set_date_from_datetime(datetime.now()) - - item_json_data = to_json(base_item) - self._set(base_item.get_id(), item_json_data) + def add(self, video_id, item): + self._set(video_id, item) - def remove(self, base_item): - self._remove(base_item.get_id()) + def remove(self, video_id): + self._remove(video_id) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 0041370fe..a405a2ca5 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1140,15 +1140,74 @@ def show_client_ip(self, context, re_match): context.get_ui().show_notification(context.localize('httpd.not.running')) # noinspection PyUnusedLocal - @RegisterProviderPath('^/playback_history/$') def on_playback_history(self, context, re_match): params = context.get_params() - video_id = params.get('video_id') action = params.get('action') - if not video_id or not action: - return True + if not action: + return False + playback_history = context.get_playback_history() - play_data = playback_history.get_items([video_id]).get(video_id) + + if action == 'list': + play_data = playback_history.get_items() + if not play_data: + return True + json_data = self.get_client(context).get_videos(play_data) + if not json_data: + return True + items = v3.response_to_items(self, context, json_data) + + for item in items: + context_menu = [( + context.localize('remove'), + 'RunPlugin({0})'.format(context.create_uri( + [constants.paths.HISTORY], + params={'action': 'remove', + 'video_id': item.video_id} + )) + ), ( + context.localize('mark.unwatched'), + 'RunPlugin({0})'.format(context.create_uri( + [constants.paths.HISTORY], + params={'action': 'mark_unwatched', + 'video_id': item.video_id} + )) + ), ( + context.localize('mark.watched'), + 'RunPlugin({0})'.format(context.create_uri( + [constants.paths.HISTORY], + params={'action': 'mark_watched', + 'video_id': item.video_id} + )) + ), ( + context.localize('history.clear'), + 'RunPlugin({0})'.format(context.create_uri( + [constants.paths.HISTORY], + params={'action': 'clear'} + )) + )] + item.set_context_menu(context_menu) + + return items + + if (action == 'clear' and context.get_ui().on_yes_no_input( + context.get_name(), + context.localize('history.clear.confirm') + )): + playback_history.clear() + context.get_ui().refresh_container() + return True + + video_id = params.get('video_id') + if not video_id: + return False + + if action == 'remove': + playback_history.remove(video_id) + context.get_ui().refresh_container() + return True + + play_data = playback_history.get_item(video_id) if not play_data: play_data = { 'play_count': 0, @@ -1190,6 +1249,7 @@ def on_root(self, context, re_match): ui = context.get_ui() _ = self.get_client(context) # required for self.is_logged_in() + logged_in = self.is_logged_in() self.set_content_type(context, constants.content_type.FILES) @@ -1277,95 +1337,131 @@ def on_root(self, context, re_match): my_location_item.set_fanart(self.get_fanart(context)) result.append(my_location_item) - # subscriptions - if self.is_logged_in(): - # my channel - if settings.get_bool('youtube.folder.my_channel.show', True): - my_channel_item = DirectoryItem(localize('my_channel'), - create_uri(['channel', 'mine']), - image=create_path('media', 'channel.png')) - my_channel_item.set_fanart(self.get_fanart(context)) - result.append(my_channel_item) - - # watch later - watch_later_playlist_id = access_manager.get_watch_later_id() - if settings.get_bool('youtube.folder.watch_later.show', True) and watch_later_playlist_id: - watch_later_item = DirectoryItem(localize('watch_later'), - create_uri(['channel', 'mine', 'playlist', watch_later_playlist_id]), - create_path('media', 'watch_later.png')) - watch_later_item.set_fanart(self.get_fanart(context)) + # my channel + if logged_in and settings.get_bool('youtube.folder.my_channel.show', True): + my_channel_item = DirectoryItem( + localize('my_channel'), + create_uri(['channel', 'mine']), + image=create_path('media', 'channel.png'), + fanart=self.get_fanart(context) + ) + result.append(my_channel_item) + + # watch later + if settings.get_bool('youtube.folder.watch_later.show', True): + playlist_id = logged_in and access_manager.get_watch_later_id() + if playlist_id and playlist_id != 'HL': + watch_later_item = DirectoryItem( + localize('watch_later'), + create_uri(['channel', 'mine', 'playlist', playlist_id]), + image=create_path('media', 'watch_later.png'), + fanart=self.get_fanart(context) + ) context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, context, watch_later_playlist_id) + yt_context_menu.append_play_all_from_playlist(context_menu, + context, + playlist_id) watch_later_item.set_context_menu(context_menu) result.append(watch_later_item) + else: + watch_history_item = DirectoryItem( + localize('watch_later'), + create_uri([constants.paths.WATCH_LATER, 'list']), + image=create_path('media', 'watch_later.png'), + fanart=self.get_fanart(context) + ) + result.append(watch_history_item) + + # liked videos + if logged_in and settings.get_bool('youtube.folder.liked_videos.show', True): + resource_manager = self.get_resource_manager(context) + playlists = resource_manager.get_related_playlists(channel_id='mine') + if 'likes' in playlists: + liked_videos_item = DirectoryItem( + localize('video.liked'), + create_uri(['channel', 'mine', 'playlist', playlists['likes']]), + image=create_path('media', 'likes.png'), + fanart=self.get_fanart(context) + ) + context_menu = [] + yt_context_menu.append_play_all_from_playlist(context_menu, context, playlists['likes']) + liked_videos_item.set_context_menu(context_menu) + result.append(liked_videos_item) + + # disliked videos + if logged_in and settings.get_bool('youtube.folder.disliked_videos.show', True): + disliked_videos_item = DirectoryItem( + localize('video.disliked'), + create_uri(['special', 'disliked_videos']), + image=create_path('media', 'dislikes.png'), + fanart=self.get_fanart(context) + ) + result.append(disliked_videos_item) + + # history + if settings.get_bool('youtube.folder.history.show', False): + playlist_id = logged_in and access_manager.get_watch_history_id() + if playlist_id and playlist_id != 'HL': + watch_history_item = DirectoryItem( + localize('history'), + create_uri(['channel', 'mine', 'playlist', playlist_id]), + image=create_path('media', 'history.png'), + fanart=self.get_fanart(context) + ) + context_menu = [] + yt_context_menu.append_play_all_from_playlist(context_menu, + context, + playlist_id) + watch_history_item.set_context_menu(context_menu) + result.append(watch_history_item) + elif settings.use_local_history(): + watch_history_item = DirectoryItem( + localize('history'), + create_uri([constants.paths.HISTORY], params={'action': 'list'}), + image=create_path('media', 'history.png'), + fanart=self.get_fanart(context) + ) + result.append(watch_history_item) + + # (my) playlists + if logged_in and settings.get_bool('youtube.folder.playlists.show', True): + playlists_item = DirectoryItem( + localize('playlists'), + create_uri(['channel', 'mine', 'playlists']), + image=create_path('media', 'playlist.png'), + fanart=self.get_fanart(context) + ) + result.append(playlists_item) + + # saved playlists + if logged_in and settings.get_bool('youtube.folder.saved.playlists.show', True): + playlists_item = DirectoryItem( + localize('saved.playlists'), + create_uri(['special', 'saved_playlists']), + image=create_path('media', 'playlist.png'), + fanart=self.get_fanart(context) + ) + result.append(playlists_item) - # liked videos - if settings.get_bool('youtube.folder.liked_videos.show', True): - resource_manager = self.get_resource_manager(context) - playlists = resource_manager.get_related_playlists(channel_id='mine') - if 'likes' in playlists: - liked_videos_item = DirectoryItem(localize('video.liked'), - create_uri(['channel', 'mine', 'playlist', playlists['likes']]), - create_path('media', 'likes.png')) - liked_videos_item.set_fanart(self.get_fanart(context)) - context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, context, playlists['likes']) - liked_videos_item.set_context_menu(context_menu) - result.append(liked_videos_item) - - # disliked videos - if settings.get_bool('youtube.folder.disliked_videos.show', True): - disliked_videos_item = DirectoryItem(localize('video.disliked'), - create_uri(['special', 'disliked_videos']), - create_path('media', 'dislikes.png')) - disliked_videos_item.set_fanart(self.get_fanart(context)) - result.append(disliked_videos_item) - - # history - if settings.get_bool('youtube.folder.history.show', False): - watch_history_playlist_id = access_manager.get_watch_history_id() - if watch_history_playlist_id != 'HL': - watch_history_item = DirectoryItem(localize('history'), - create_uri(['channel', 'mine', 'playlist', watch_history_playlist_id]), - create_path('media', 'history.png')) - watch_history_item.set_fanart(self.get_fanart(context)) - context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, context, watch_history_playlist_id) - watch_history_item.set_context_menu(context_menu) - - result.append(watch_history_item) - - # (my) playlists - if settings.get_bool('youtube.folder.playlists.show', True): - playlists_item = DirectoryItem(localize('playlists'), - create_uri(['channel', 'mine', 'playlists']), - create_path('media', 'playlist.png')) - playlists_item.set_fanart(self.get_fanart(context)) - result.append(playlists_item) - - # saved playlists - if settings.get_bool('youtube.folder.saved.playlists.show', True): - playlists_item = DirectoryItem(localize('saved.playlists'), - create_uri(['special', 'saved_playlists']), - create_path('media', 'playlist.png')) - playlists_item.set_fanart(self.get_fanart(context)) - result.append(playlists_item) - - # subscriptions - if settings.get_bool('youtube.folder.subscriptions.show', True): - subscriptions_item = DirectoryItem(localize('subscriptions'), - create_uri(['subscriptions', 'list']), - image=create_path('media', 'channels.png')) - subscriptions_item.set_fanart(self.get_fanart(context)) - result.append(subscriptions_item) - - # browse channels - if settings.get_bool('youtube.folder.browse_channels.show', True): - browse_channels_item = DirectoryItem(localize('browse_channels'), - create_uri(['special', 'browse_channels']), - image=create_path('media', 'browse_channels.png')) - browse_channels_item.set_fanart(self.get_fanart(context)) - result.append(browse_channels_item) + # subscriptions + if logged_in and settings.get_bool('youtube.folder.subscriptions.show', True): + subscriptions_item = DirectoryItem( + localize('subscriptions'), + create_uri(['subscriptions', 'list']), + image=create_path('media', 'channels.png'), + fanart=self.get_fanart(context) + ) + result.append(subscriptions_item) + + # browse channels + if logged_in and settings.get_bool('youtube.folder.browse_channels.show', True): + browse_channels_item = DirectoryItem( + localize('browse_channels'), + create_uri(['special', 'browse_channels']), + image=create_path('media', 'browse_channels.png'), + fanart=self.get_fanart(context) + ) + result.append(browse_channels_item) # completed live events if settings.get_bool('youtube.folder.completed.live.show', True): From d19b11c66acac471b64c42769442aa12eaf747bf Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 09:06:49 +1100 Subject: [PATCH 119/141] Initial consolidation of context menu methods - Context menu items are evaluated at the time that they are created - This creates problems when the underlying item data changes - The context menu item can then contain out of date data - e.g. updated channel fanart or video play data - TODO: register menu items, but don't call them until ListItem is added to UI --- .../kodion/abstract_provider.py | 31 +- .../kodion/context/xbmc/xbmc_context.py | 7 +- .../youtube_plugin/kodion/items/__init__.py | 4 +- .../youtube_plugin/kodion/items/menu_items.py | 491 ++++++++++++++++++ .../kodion/items/search_history_item.py | 12 +- .../youtube_plugin/youtube/helper/utils.py | 266 +++++----- .../lib/youtube_plugin/youtube/helper/v3.py | 10 +- .../youtube/helper/yt_context_menu.py | 216 -------- .../lib/youtube_plugin/youtube/provider.py | 79 ++- 9 files changed, 695 insertions(+), 421 deletions(-) create mode 100644 resources/lib/youtube_plugin/kodion/items/menu_items.py delete mode 100644 resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 34b7c6322..57c5fdffa 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -19,6 +19,7 @@ DirectoryItem, NewSearchItem, SearchHistoryItem, + menu_items ) from .utils import to_unicode @@ -163,13 +164,11 @@ def _internal_favorite(context, re_match): items = context.get_favorite_list().get_items() for item in items: - context_menu = [( - context.localize('favorites.remove'), - 'RunPlugin(%s)' % context.create_uri( - [constants.paths.FAVORITES, 'remove'], - params={'item_id': item.get_id()} - ) - )] + context_menu = [ + menu_items.favorites_remove( + context, item.video_id + ), + ] item.set_context_menu(context_menu) return items @@ -201,18 +200,14 @@ def _internal_watch_later(self, context, re_match): video_items = context.get_watch_later_list().get_items() for video_item in video_items: - context_menu = [( - context.localize('watch_later.remove'), - 'RunPlugin(%s)' % context.create_uri( - [constants.paths.WATCH_LATER, 'remove'], - params={'item_id': video_item.get_id()} - ) - ), ( - context.localize('watch_later.clear'), - 'RunPlugin(%s)' % context.create_uri( - [constants.paths.WATCH_LATER, 'clear'] + context_menu = [ + menu_items.watch_later_local_remove( + context, video_item.video_id + ), + menu_items.watch_later_local_clear( + context ) - )] + ] video_item.set_context_menu(context_menu) return video_items diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 027ee84ab..20b69bf80 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -92,6 +92,10 @@ class XbmcContext(AbstractContext): 'history.list.remove.confirm': 30573, 'history.list.set': 30571, 'history.list.set.confirm': 30574, + 'history.mark.unwatched': 30669, + 'history.mark.watched': 30670, + 'history.remove': 30108, + 'history.reset.resume_point': 30674, 'httpd.not.running': 30699, 'inputstreamhelper.is_installed': 30625, 'isa.enable.confirm': 30579, @@ -102,8 +106,6 @@ class XbmcContext(AbstractContext): 'live': 30539, 'live.completed': 30647, 'live.upcoming': 30646, - 'mark.unwatched': 30669, - 'mark.watched': 30670, 'must_be_signed_in': 30616, 'my_channel': 30507, 'my_location': 30654, @@ -142,7 +144,6 @@ class XbmcContext(AbstractContext): 'renamed': 30667, 'requires.krypton': 30624, 'reset.access_manager.confirm': 30581, - 'reset.resume_point': 30674, 'retry': 30612, 'saved.playlists': 30611, 'search': 30102, diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index 5cba8f9e2..948f32655 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals +from . import menu_items from .audio_item import AudioItem from .base_item import BaseItem from .directory_item import DirectoryItem @@ -37,4 +38,5 @@ 'UriItem', 'VideoItem', 'WatchLaterItem', - 'from_json',) + 'from_json', + 'menu_items',) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py new file mode 100644 index 000000000..ba51775bd --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -0,0 +1,491 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2018 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +from ..constants import paths + + +def more_for_video(context, video_id, logged_in=False, refresh_container=False): + return ( + context.localize('video.more'), + 'RunPlugin({0})'.format(context.create_uri( + ['video', 'more'], + { + 'video_id': video_id, + 'logged_in': logged_in, + 'refresh_container': refresh_container, + } + )) + ) + + +def content_from_description(context, video_id): + return ( + context.localize('video.description.links'), + 'RunPlugin({0})'.format(context.create_uri( + ['special', 'description_links'], + { + 'video_id': video_id, + } + )) + ) + + +def play_with(context): + return ( + context.localize('video.play.with'), + 'Action(SwitchPlayer)' + ) + + +def queue_video(context): + return ( + context.localize('video.queue'), + 'Action(Queue)' + ) + + +def play_all_from_playlist(context, playlist_id, video_id=''): + if video_id: + return ( + context.localize('playlist.play.from_here'), + 'RunPlugin({0})'.format(context.create_uri( + ['play'], + { + 'playlist_id': playlist_id, + 'video_id': video_id, + 'play': True, + } + )) + ) + return ( + context.localize('playlist.play.all'), + 'RunPlugin({0})'.format(context.create_uri( + ['play'], + { + 'playlist_id': playlist_id, + 'play': True, + } + )) + ) + + +def add_video_to_playlist(context, video_id): + return ( + context.localize('video.add_to_playlist'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'select', 'playlist'], + { + 'video_id': video_id, + } + )) + ) + + +def remove_video_from_playlist(context, playlist_id, video_id, video_name): + return ( + context.localize('remove'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'remove', 'video'], + { + 'playlist_id': playlist_id, + 'video_id': video_id, + 'video_name': video_name, + } + )) + ) + + +def rename_playlist(context, playlist_id, playlist_name): + return ( + context.localize('rename'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'rename', 'playlist'], + { + 'playlist_id': playlist_id, + 'playlist_name': playlist_name + } + )) + ) + + +def delete_playlist(context, playlist_id, playlist_name): + return ( + context.localize('delete'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'remove', 'playlist'], + { + 'playlist_id': playlist_id, + 'playlist_name': playlist_name + } + )) + ) + + +def remove_as_watchlater(context, playlist_id, playlist_name): + return ( + context.localize('watch_later.list.remove'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'remove', 'watchlater'], + { + 'playlist_id': playlist_id, + 'playlist_name': playlist_name + } + )) + ) + + +def set_as_watchlater(context, playlist_id, playlist_name): + return ( + context.localize('watch_later.list.set'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'set', 'watchlater'], + { + 'playlist_id': playlist_id, + 'playlist_name': playlist_name + } + )) + ) + + +def remove_as_history(context, playlist_id, playlist_name): + return ( + context.localize('history.list.remove'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'remove', 'history'], + { + 'playlist_id': playlist_id, + 'playlist_name': playlist_name + } + )) + ) + + +def set_as_history(context, playlist_id, playlist_name): + return ( + context.localize('history.list.set'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'set', 'history'], + { + 'playlist_id': playlist_id, + 'playlist_name': playlist_name + } + )) + ) + + +def remove_my_subscriptions_filter(context, channel_name): + return ( + context.localize('my_subscriptions.filter.remove'), + 'RunPlugin({0})'.format(context.create_uri( + ['my_subscriptions', 'filter'], + { + 'channel_name': channel_name, + 'action': 'remove' + } + )) + ) + + +def add_my_subscriptions_filter(context, channel_name): + return ( + context.localize('my_subscriptions.filter.add'), + 'RunPlugin({0})'.format(context.create_uri( + ['my_subscriptions', 'filter'], + { + 'channel_name': channel_name, + 'action': 'add', + } + )) + ) + + +def rate_video(context, video_id, refresh_container=False): + return ( + context.localize('video.rate'), + 'RunPlugin({0})'.format(context.create_uri( + ['video', 'rate'], + { + 'video_id': video_id, + 'refresh_container': refresh_container, + } + )) + ) + + +def watch_later_add(context, playlist_id, video_id): + return ( + context.localize('watch_later.add'), + 'RunPlugin({0})'.format(context.create_uri( + ['playlist', 'add', 'video'], + { + 'playlist_id': playlist_id, + 'video_id': video_id, + } + )) + ) + + +def watch_later_local_add(context, item): + return ( + context.localize('watch_later.add'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.WATCH_LATER, 'add'], + { + 'video_id': item.video_id, + 'item': item.dumps(), + } + )) + ) + + +def watch_later_local_remove(context, video_id): + return ( + context.localize('watch_later.remove'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.WATCH_LATER, 'remove'], + { + 'video_id': video_id, + } + )) + ) + + +def watch_later_local_clear(context): + return ( + context.localize('watch_later.clear'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.WATCH_LATER, 'clear'] + )) + ) + + +def go_to_channel(context, channel_id, channel_name): + return ( + context.localize('go_to_channel') % context.get_ui().bold(channel_name), + 'Container.Update({0})'.format(context.create_uri( + ['channel', channel_id] + )) + ) + + +def related_videos(context, video_id): + return ( + context.localize('related_videos'), + 'Container.Update({0})'.format(context.create_uri( + ['special', 'related_videos'], + { + 'video_id': video_id, + } + )) + ) + + +def refresh(context): + return ( + context.localize('refresh'), + 'Container.Refresh' + ) + + +def subscribe_to_channel(context, channel_id, channel_name=''): + if not channel_name: + return ( + context.localize('subscribe'), + 'RunPlugin({0})'.format(context.create_uri( + ['subscriptions', 'add'], + { + 'subscription_id': channel_id, + } + )) + ) + return ( + context.localize('subscribe_to') % context.get_ui().bold(channel_name), + 'RunPlugin({0})'.format(context.create_uri( + ['subscriptions', 'add'], + { + 'subscription_id': channel_id, + } + )) + ) + + +def unsubscribe_from_channel(context, channel_id): + return ( + context.localize('unsubscribe'), + 'RunPlugin({0})'.format(context.create_uri( + ['subscriptions', 'remove'], + { + 'subscription_id': channel_id, + } + )) + ) + + +def play_with_subtitles(context, video_id): + return ( + context.localize('video.play.with_subtitles'), + 'RunPlugin({0})'.format(context.create_uri( + ['play'], + { + 'video_id': video_id, + 'prompt_for_subtitles': True, + } + )) + ) + + +def play_audio_only(context, video_id): + return ( + context.localize('video.play.audio_only'), + 'RunPlugin({0})'.format(context.create_uri( + ['play'], + { + 'video_id': video_id, + 'audio_only': True, + } + )) + ) + + +def play_ask_for_quality(context, video_id): + return ( + context.localize('video.play.ask_for_quality'), + 'RunPlugin({0})'.format(context.create_uri( + ['play'], + { + 'video_id': video_id, + 'ask_for_quality': True, + } + )) + ) + + +def history_remove(context, video_id): + return ( + context.localize('history.remove'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.HISTORY], + { + 'action': 'remove', + 'video_id': video_id + } + )) + ) + + +def history_clear(context): + return ( + context.localize('history.clear'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.HISTORY], + { + 'action': 'clear' + } + )) + ) + + +def history_mark_watched(context, video_id): + return ( + context.localize('history.mark.watched'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.HISTORY], + { + 'video_id': video_id, + 'action': 'mark_watched', + } + )) + ) + + +def history_mark_unwatched(context, video_id): + return ( + context.localize('history.mark.unwatched'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.HISTORY], + { + 'video_id': video_id, + 'action': 'mark_unwatched', + } + )) + ) + + +def history_reset_resume(context, video_id): + return ( + context.localize('history.reset.resume_point'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.HISTORY], + { + 'video_id': video_id, + 'action': 'reset_resume', + } + )) + ) + + +def favorites_add(context, item): + return ( + context.localize('favorites.add'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.FAVORITES, 'add'], + { + 'video_id': item.video_id, + 'item': item.dumps(), + } + )) + ) + + +def favorites_remove(context, video_id): + return ( + context.localize('favorites.remove'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.FAVORITES, 'remove'], + { + 'vide_id': video_id, + } + )) + ) + + +def search_remove(context, query): + return ( + context.localize('search.remove'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.SEARCH, 'remove'], + { + 'q': query, + } + )) + ) + + +def search_rename(context, query): + return ( + context.localize('search.rename'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.SEARCH, 'rename'], + { + 'q': query, + } + )) + ) + + +def search_clear(context): + return ( + context.localize('search.clear'), + 'RunPlugin({0})'.format(context.create_uri( + [paths.SEARCH, 'clear'] + )) + ) diff --git a/resources/lib/youtube_plugin/kodion/items/search_history_item.py b/resources/lib/youtube_plugin/kodion/items/search_history_item.py index e53d020aa..b2aeab883 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_history_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_history_item.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals +from . import menu_items from .directory_item import DirectoryItem from ..constants.const_paths import SEARCH @@ -29,10 +30,9 @@ def __init__(self, context, query, image=None, fanart=None, location=False): else: self.set_fanart(context.get_fanart()) - context_menu = [(context.localize('search.remove'), - 'RunPlugin(%s)' % context.create_uri([SEARCH, 'remove'], params={'q': query})), - (context.localize('search.rename'), - 'RunPlugin(%s)' % context.create_uri([SEARCH, 'rename'], params={'q': query})), - (context.localize('search.clear'), - 'RunPlugin(%s)' % context.create_uri([SEARCH, 'clear']))] + context_menu = [ + menu_items.search_remove(context, query), + menu_items.search_rename(context, query), + menu_items.search_clear(context), + ] self.set_context_menu(context_menu) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index dc979a27e..311d3719b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -14,8 +14,7 @@ import time from math import log10 -from ..helper import yt_context_menu -from ...kodion.items import DirectoryItem +from ...kodion.items import DirectoryItem, menu_items from ...kodion.utils import ( create_path, datetime_parser, @@ -36,8 +35,6 @@ 'viewCount': 'lightblue', } -__RE_HISTORY_MATCH = re.compile(r'^/special/watch_history_tv/$') - __RE_PLAYLIST_MATCH = re.compile( r'^(/channel/(?P[^/]+))/playlist/(?P[^/]+)/$' ) @@ -164,18 +161,25 @@ def update_channel_infos(provider, context, channel_id_dict, if subscription_id_dict is None: subscription_id_dict = {} - filter_list = [] + settings = context.get_settings() logged_in = provider.is_logged_in() path = context.get_path() + + filter_list = None if path == '/subscriptions/list/': - filter_string = context.get_settings().get_string( - 'youtube.filter.my_subscriptions_filtered.list', '' - ) - filter_string = filter_string.replace(', ', ',') - filter_list = filter_string.split(',') - filter_list = [x.lower() for x in filter_list] + in_subscription_list = True + if settings.get_bool('youtube.folder.my_subscriptions_filtered.show', + False): + filter_string = settings.get_string( + 'youtube.filter.my_subscriptions_filtered.list', '' + ) + filter_string = filter_string.replace(', ', ',') + filter_list = filter_string.split(',') + filter_list = [x.lower() for x in filter_list] + else: + in_subscription_list = False - thumb_size = context.get_settings().use_thumbnail_size + thumb_size = settings.use_thumbnail_size banners = [ 'bannerTvMediumImageUrl', 'bannerTvLowImageUrl', @@ -197,30 +201,39 @@ def update_channel_infos(provider, context, channel_id_dict, # - update context menu context_menu = [] + # -- unsubscribe from channel subscription_id = subscription_id_dict.get(channel_id, '') if subscription_id: channel_item.set_channel_subscription_id(subscription_id) - yt_context_menu.append_unsubscribe_from_channel( - context_menu, context, subscription_id + context_menu.append( + menu_items.unsubscribe_from_channel( + context, subscription_id + ) ) + # -- subscribe to the channel - if logged_in and path != '/subscriptions/list/': - yt_context_menu.append_subscribe_to_channel( - context_menu, context, channel_id + if logged_in and not in_subscription_list: + context_menu.append( + menu_items.subscribe_to_channel( + context, channel_id + ) ) - if path == '/subscriptions/list/': + # add/remove from filter list + if in_subscription_list and filter_list is not None: channel = title.lower().replace(',', '') - if channel in filter_list: - yt_context_menu.append_remove_my_subscriptions_filter( - context_menu, context, title - ) - else: - yt_context_menu.append_add_my_subscriptions_filter( - context_menu, context, title + context_menu.append( + menu_items.remove_my_subscriptions_filter( + context, title + ) if channel in filter_list else + menu_items.add_my_subscriptions_filter( + context, title ) - channel_item.set_context_menu(context_menu) + ) + + if context_menu: + channel_item.set_context_menu(context_menu) fanart_images = yt_item.get('brandingSettings', {}).get('image', {}) for banner in banners: @@ -273,49 +286,49 @@ def update_playlist_infos(provider, context, playlist_id_dict, if path == '/channel/mine/playlists/': channel_id = 'mine' channel_name = snippet.get('channelTitle', '') - context_menu = [] + # play all videos of the playlist - yt_context_menu.append_play_all_from_playlist( - context_menu, context, playlist_id - ) + context_menu = [ + menu_items.play_all_from_playlist( + context, playlist_id + ) + ] if logged_in: if channel_id != 'mine': # subscribe to the channel via the playlist item - yt_context_menu.append_subscribe_to_channel( - context_menu, context, channel_id, channel_name + context_menu.append( + menu_items.subscribe_to_channel( + context, channel_id, channel_name + ) ) else: - # remove my playlist - yt_context_menu.append_delete_playlist( - context_menu, context, playlist_id, title - ) - - # rename playlist - yt_context_menu.append_rename_playlist( - context_menu, context, playlist_id, title - ) - - # remove as my custom watch later playlist - if playlist_id == custom_watch_later_id: - yt_context_menu.append_remove_as_watchlater( - context_menu, context, playlist_id, title - ) - # set as my custom watch later playlist - else: - yt_context_menu.append_set_as_watchlater( - context_menu, context, playlist_id, title - ) - # remove as custom history playlist - if playlist_id == custom_history_id: - yt_context_menu.append_remove_as_history( - context_menu, context, playlist_id, title - ) - # set as custom history playlist - else: - yt_context_menu.append_set_as_history( - context_menu, context, playlist_id, title - ) + context_menu.extend(( + # remove my playlist + menu_items.delete_playlist( + context, playlist_id, title + ), + # rename playlist + menu_items.rename_playlist( + context, playlist_id, title + ), + # remove as my custom watch later playlist + menu_items.remove_as_watchlater( + context, playlist_id, title + ) if playlist_id == custom_watch_later_id else + # set as my custom watch later playlist + menu_items.set_as_watchlater( + context, playlist_id, title + ), + # remove as custom history playlist + menu_items.remove_as_history( + context, playlist_id, title + ) if playlist_id == custom_history_id else + # set as custom history playlist + menu_items.set_as_history( + context, playlist_id, title + ), + )) if context_menu: playlist_item.set_context_menu(context_menu) @@ -538,25 +551,22 @@ def update_video_infos(provider, context, video_id_dict, image = ''.join([image, '?ct=', thumb_stamp]) video_item.set_image(image) - # set fanart - video_item.set_fanart(provider.get_fanart(context)) - # update channel mapping channel_id = snippet.get('channelId', '') + video_item.set_subscription_id(channel_id) if channel_id and channel_items_dict is not None: if channel_id not in channel_items_dict: channel_items_dict[channel_id] = [] channel_items_dict[channel_id].append(video_item) - context_menu = [] + context_menu = [ + # Refresh + menu_items.refresh(context), + # Queue Video + menu_items.queue_video(context), + ] replace_context_menu = False - # Refresh - yt_context_menu.append_refresh(context_menu, context) - - # Queue Video - yt_context_menu.append_queue_video(context_menu, context) - """ Play all videos of the playlist. @@ -570,22 +580,26 @@ def update_video_infos(provider, context, video_id_dict, playlist_id = playlist_match.group('playlist_id') playlist_channel_id = playlist_match.group('channel_id') - yt_context_menu.append_play_all_from_playlist( - context_menu, context, playlist_id, video_id - ) - yt_context_menu.append_play_all_from_playlist( - context_menu, context, playlist_id - ) + context_menu.extend(( + menu_items.play_all_from_playlist( + context, playlist_id, video_id + ), + menu_items.play_all_from_playlist( + context, playlist_id + ) + )) # 'play with...' (external player) if alternate_player: - yt_context_menu.append_play_with(context_menu, context) + context_menu.append(menu_items.play_with(context)) if logged_in: # add 'Watch Later' only if we are not in my 'Watch Later' list - if wl_playlist_id and wl_playlist_id != playlist_id: - yt_context_menu.append_watch_later( - context_menu, context, wl_playlist_id, video_id + if wl_playlist_id and playlist_id and wl_playlist_id != playlist_id: + context_menu.append( + menu_items.watch_later_add( + context, wl_playlist_id, video_id + ) ) # provide 'remove' for videos in my playlists @@ -596,72 +610,74 @@ def update_video_infos(provider, context, video_id_dict, playlist_item_id = playlist_item_id_dict[video_id] video_item.set_playlist_id(playlist_id) video_item.set_playlist_item_id(playlist_item_id) - context_menu.append(( - context.localize('remove'), - 'RunPlugin(%s)' % context.create_uri( - ['playlist', 'remove', 'video'], - {'playlist_id': playlist_id, - 'video_id': playlist_item_id, - 'video_name': video_item.get_name()} + context_menu.append( + menu_items.remove_video_from_playlist( + context, playlist_id, video_id, video_item.get_name() ) - )) - - if __RE_HISTORY_MATCH.match(path): - yt_context_menu.append_clear_watch_history( - context_menu, context ) + else: + context_menu.append( + menu_items.watch_later_local_add( + context, video_item + ) + ) # got to [CHANNEL] only if we are not directly in the channel if (channel_id and channel_name and create_path('channel', channel_id) != path): video_item.set_channel_id(channel_id) - yt_context_menu.append_go_to_channel( - context_menu, context, channel_id, channel_name + context_menu.append( + menu_items.go_to_channel( + context, channel_id, channel_name + ) ) if logged_in: # subscribe to the channel of the video - video_item.set_subscription_id(channel_id) - yt_context_menu.append_subscribe_to_channel( - context_menu, context, channel_id, channel_name + context_menu.append( + menu_items.subscribe_to_channel( + context, channel_id, channel_name + ) ) if not video_item.live and play_data: - if not play_data.get('play_count'): - yt_context_menu.append_mark_watched( - context_menu, context, video_id - ) - else: - yt_context_menu.append_mark_unwatched( - context_menu, context, video_id + context_menu.append( + menu_items.history_mark_unwatched( + context, video_id + ) if play_data.get('play_count') else + menu_items.history_mark_watched( + context, video_id ) + ) if (play_data.get('played_percent', 0) > 0 or play_data.get('played_time', 0) > 0): - yt_context_menu.append_reset_resume_point( - context_menu, context, video_id + context_menu.append( + menu_items.history_reset_resume( + context, video_id + ) ) # more... refresh_container = (path.startswith('/channel/mine/playlist/LL') or path == '/special/disliked_videos/') - yt_context_menu.append_more_for_video( - context_menu, context, video_id, - is_logged_in=logged_in, - refresh_container=refresh_container - ) - - if not video_item.live: - yt_context_menu.append_play_with_subtitles( - context_menu, context, video_id - ) - yt_context_menu.append_play_audio_only( - context_menu, context, video_id - ) - - yt_context_menu.append_play_ask_for_quality( - context_menu, context, video_id - ) + context_menu.extend(( + menu_items.more_for_video( + context, + video_id, + logged_in=logged_in, + refresh_container=refresh_container, + ), + menu_items.play_with_subtitles( + context, video_id + ), + menu_items.play_audio_only( + context, video_id + ), + menu_items.play_ask_for_quality( + context, video_id + ), + )) if context_menu: video_item.set_context_menu( diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 447db2d4b..345a85ca7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -21,9 +21,8 @@ update_playlist_infos, update_video_infos, ) -from ..helper import yt_context_menu from ...kodion import KodionException -from ...kodion.items import DirectoryItem, NextPageItem, VideoItem +from ...kodion.items import DirectoryItem, NextPageItem, VideoItem, menu_items def _process_list_response(provider, context, json_data): @@ -87,8 +86,11 @@ def _process_list_response(provider, context, json_data): # if logged in => provide subscribing to the channel if provider.is_logged_in(): - context_menu = [] - yt_context_menu.append_subscribe_to_channel(context_menu, context, channel_id) + context_menu = [ + menu_items.subscribe_to_channel( + context, channel_id + ), + ] channel_item.set_context_menu(context_menu) result.append(channel_item) channel_id_dict[channel_id] = channel_item diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py b/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py deleted file mode 100644 index 3159ebc49..000000000 --- a/resources/lib/youtube_plugin/youtube/helper/yt_context_menu.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from ... import kodion - - -def append_more_for_video(context_menu, context, video_id, is_logged_in=False, refresh_container=False): - _is_logged_in = '0' - if is_logged_in: - _is_logged_in = '1' - - _refresh_container = '0' - if refresh_container: - _refresh_container = '1' - - context_menu.append((context.localize('video.more'), - 'RunPlugin(%s)' % context.create_uri(['video', 'more'], - {'video_id': video_id, - 'logged_in': _is_logged_in, - 'refresh_container': _refresh_container}))) - - -def append_content_from_description(context_menu, context, video_id): - context_menu.append((context.localize('video.description.links'), - 'Container.Update(%s)' % context.create_uri(['special', 'description_links'], - {'video_id': video_id}))) - - -def append_play_with(context_menu, context): - context_menu.append((context.localize('video.play.with'), 'Action(SwitchPlayer)')) - - -def append_queue_video(context_menu, context): - context_menu.append((context.localize('video.queue'), 'Action(Queue)')) - - -def append_play_all_from_playlist(context_menu, context, playlist_id, video_id=''): - if video_id: - context_menu.append((context.localize('playlist.play.from_here'), - 'RunPlugin(%s)' % context.create_uri(['play'], - {'playlist_id': playlist_id, - 'video_id': video_id, - 'play': '1'}))) - else: - context_menu.append((context.localize('playlist.play.all'), - 'RunPlugin(%s)' % context.create_uri(['play'], - {'playlist_id': playlist_id, - 'play': '1'}))) - - -def append_add_video_to_playlist(context_menu, context, video_id): - context_menu.append((context.localize('video.add_to_playlist'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'select', 'playlist'], - {'video_id': video_id}))) - - -def append_rename_playlist(context_menu, context, playlist_id, playlist_name): - context_menu.append((context.localize('rename'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'rename', 'playlist'], - {'playlist_id': playlist_id, - 'playlist_name': playlist_name}))) - - -def append_delete_playlist(context_menu, context, playlist_id, playlist_name): - context_menu.append((context.localize('delete'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'remove', 'playlist'], - {'playlist_id': playlist_id, - 'playlist_name': playlist_name}))) - - -def append_remove_as_watchlater(context_menu, context, playlist_id, playlist_name): - context_menu.append((context.localize('watch_later.list.remove'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'remove', 'watchlater'], - {'playlist_id': playlist_id, - 'playlist_name': playlist_name}))) - - -def append_set_as_watchlater(context_menu, context, playlist_id, playlist_name): - context_menu.append((context.localize('watch_later.list.set'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'set', 'watchlater'], - {'playlist_id': playlist_id, - 'playlist_name': playlist_name}))) - - -def append_remove_as_history(context_menu, context, playlist_id, playlist_name): - context_menu.append((context.localize('history.list.remove'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'remove', 'history'], - {'playlist_id': playlist_id, - 'playlist_name': playlist_name}))) - - -def append_set_as_history(context_menu, context, playlist_id, playlist_name): - context_menu.append((context.localize('history.list.set'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'set', 'history'], - {'playlist_id': playlist_id, - 'playlist_name': playlist_name}))) - - -def append_remove_my_subscriptions_filter(context_menu, context, channel_name): - if context.get_settings().get_bool('youtube.folder.my_subscriptions_filtered.show', False): - context_menu.append((context.localize('my_subscriptions.filter.remove'), - 'RunPlugin(%s)' % context.create_uri(['my_subscriptions', 'filter'], - {'channel_name': channel_name, - 'action': 'remove'}))) - - -def append_add_my_subscriptions_filter(context_menu, context, channel_name): - if context.get_settings().get_bool('youtube.folder.my_subscriptions_filtered.show', False): - context_menu.append((context.localize('my_subscriptions.filter.add'), - 'RunPlugin(%s)' % context.create_uri(['my_subscriptions', 'filter'], - {'channel_name': channel_name, - 'action': 'add'}))) - - -def append_rate_video(context_menu, context, video_id, refresh_container=False): - refresh_container = '1' if refresh_container else '0' - context_menu.append((context.localize('video.rate'), - 'RunPlugin(%s)' % context.create_uri(['video', 'rate'], - {'video_id': video_id, - 'refresh_container': refresh_container}))) - - -def append_watch_later(context_menu, context, playlist_id, video_id): - playlist_path = kodion.utils.create_path('channel', 'mine', 'playlist', playlist_id) - if playlist_id and playlist_path != context.get_path(): - context_menu.append((context.localize('watch_later'), - 'RunPlugin(%s)' % context.create_uri(['playlist', 'add', 'video'], - {'playlist_id': playlist_id, 'video_id': video_id}))) - - -def append_go_to_channel(context_menu, context, channel_id, channel_name): - text = context.localize('go_to_channel') % context.get_ui().bold(channel_name) - context_menu.append((text, 'Container.Update(%s)' % context.create_uri(['channel', channel_id]))) - - -def append_related_videos(context_menu, context, video_id): - context_menu.append((context.localize('related_videos'), - 'Container.Update(%s)' % context.create_uri(['special', 'related_videos'], - {'video_id': video_id}))) - - -def append_clear_watch_history(context_menu, context): - context_menu.append((context.localize('clear_history'), - 'Container.Update(%s)' % context.create_uri(['history', 'clear']))) - - -def append_refresh(context_menu, context): - context_menu.append((context.localize('refresh'), 'Container.Refresh')) - - -def append_subscribe_to_channel(context_menu, context, channel_id, channel_name=''): - if channel_name: - text = context.localize('subscribe_to') % context.get_ui().bold(channel_name) - context_menu.append( - (text, 'RunPlugin(%s)' % context.create_uri(['subscriptions', 'add'], {'subscription_id': channel_id}))) - else: - context_menu.append((context.localize('subscribe'), - 'RunPlugin(%s)' % context.create_uri(['subscriptions', 'add'], - {'subscription_id': channel_id}))) - - -def append_unsubscribe_from_channel(context_menu, context, channel_id): - context_menu.append((context.localize('unsubscribe'), - 'RunPlugin(%s)' % context.create_uri(['subscriptions', 'remove'], - {'subscription_id': channel_id}))) - - -def append_mark_watched(context_menu, context, video_id): - context_menu.append((context.localize('mark.watched'), - 'RunPlugin(%s)' % context.create_uri(['playback_history'], - {'video_id': video_id, - 'action': 'mark_watched'}))) - - -def append_mark_unwatched(context_menu, context, video_id): - context_menu.append((context.localize('mark.unwatched'), - 'RunPlugin(%s)' % context.create_uri(['playback_history'], - {'video_id': video_id, - 'action': 'mark_unwatched'}))) - - -def append_reset_resume_point(context_menu, context, video_id): - context_menu.append((context.localize('reset.resume_point'), - 'RunPlugin(%s)' % context.create_uri(['playback_history'], - {'video_id': video_id, - 'action': 'reset_resume'}))) - - -def append_play_with_subtitles(context_menu, context, video_id): - context_menu.append((context.localize('video.play.with_subtitles'), - 'RunPlugin(%s)' % context.create_uri(['play'], - {'video_id': video_id, - 'prompt_for_subtitles': '1'}))) - - -def append_play_audio_only(context_menu, context, video_id): - context_menu.append((context.localize('video.play.audio_only'), - 'RunPlugin(%s)' % context.create_uri(['play'], - {'video_id': video_id, - 'audio_only': '1'}))) - - -def append_play_ask_for_quality(context_menu, context, video_id): - context_menu.append((context.localize('video.play.ask_for_quality'), - 'RunPlugin(%s)' % context.create_uri(['play'], - {'video_id': video_id, - 'ask_for_quality': '1'}))) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index a405a2ca5..d406c3739 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -23,7 +23,6 @@ UrlResolver, UrlToItemConverter, v3, - yt_context_menu, yt_login, yt_old_actions, yt_play, @@ -36,7 +35,7 @@ from .youtube_exceptions import InvalidGrant, LoginException from ..kodion import (AbstractProvider, RegisterProviderPath, constants) from ..kodion.compatibility import xbmcaddon, xbmcvfs -from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem +from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem, menu_items from ..kodion.network import get_client_ip_address, is_httpd_live from ..kodion.utils import find_video_id, strip_html_from_text @@ -653,14 +652,6 @@ def _on_yt_specials(self, context, re_match): category = re_match.group('category') return yt_specials.process(category, self, context) - # noinspection PyUnusedLocal - @RegisterProviderPath('^/history/clear/$') - def _on_yt_clear_history(self, context, re_match): - if context.get_ui().on_yes_no_input(context.get_name(), context.localize('clear_history_confirmation')): - json_data = self.get_client(context).clear_watch_history() - if 'error' not in json_data: - context.get_ui().show_notification(context.localize('succeeded')) - @RegisterProviderPath('^/users/(?P[^/]+)/$') def _on_users(self, context, re_match): action = re_match.group('action') @@ -1158,34 +1149,21 @@ def on_playback_history(self, context, re_match): items = v3.response_to_items(self, context, json_data) for item in items: - context_menu = [( - context.localize('remove'), - 'RunPlugin({0})'.format(context.create_uri( - [constants.paths.HISTORY], - params={'action': 'remove', - 'video_id': item.video_id} - )) - ), ( - context.localize('mark.unwatched'), - 'RunPlugin({0})'.format(context.create_uri( - [constants.paths.HISTORY], - params={'action': 'mark_unwatched', - 'video_id': item.video_id} - )) - ), ( - context.localize('mark.watched'), - 'RunPlugin({0})'.format(context.create_uri( - [constants.paths.HISTORY], - params={'action': 'mark_watched', - 'video_id': item.video_id} - )) - ), ( - context.localize('history.clear'), - 'RunPlugin({0})'.format(context.create_uri( - [constants.paths.HISTORY], - params={'action': 'clear'} - )) - )] + video_id = item.video_id + context_menu = [ + menu_items.history_remove( + context, video_id + ), + menu_items.history_mark_unwatched( + context, video_id + ) if play_data[video_id]['play_count'] else + menu_items.history_mark_watched( + context, video_id + ), + menu_items.history_clear( + context + ), + ] item.set_context_menu(context_menu) return items @@ -1357,10 +1335,11 @@ def on_root(self, context, re_match): image=create_path('media', 'watch_later.png'), fanart=self.get_fanart(context) ) - context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, - context, - playlist_id) + context_menu = [ + menu_items.play_all_from_playlist( + context, playlist_id + ) + ] watch_later_item.set_context_menu(context_menu) result.append(watch_later_item) else: @@ -1383,8 +1362,11 @@ def on_root(self, context, re_match): image=create_path('media', 'likes.png'), fanart=self.get_fanart(context) ) - context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, context, playlists['likes']) + context_menu = [ + menu_items.play_all_from_playlist( + context, playlists['likes'] + ) + ] liked_videos_item.set_context_menu(context_menu) result.append(liked_videos_item) @@ -1408,10 +1390,11 @@ def on_root(self, context, re_match): image=create_path('media', 'history.png'), fanart=self.get_fanart(context) ) - context_menu = [] - yt_context_menu.append_play_all_from_playlist(context_menu, - context, - playlist_id) + context_menu = [ + menu_items.play_all_from_playlist( + context, playlist_id + ) + ] watch_history_item.set_context_menu(context_menu) result.append(watch_history_item) elif settings.use_local_history(): From 45ce733a59f84e5825488e1bd2e9c4a02953cfc7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 09:14:05 +1100 Subject: [PATCH 120/141] Provide stack trace in error log messages --- .../lib/youtube_plugin/kodion/network/requests.py | 5 ++--- .../kodion/plugin/xbmc/xbmc_runner.py | 4 ++-- .../lib/youtube_plugin/kodion/sql_store/storage.py | 8 ++++---- .../lib/youtube_plugin/youtube/helper/video_info.py | 13 +++++++------ .../lib/youtube_plugin/youtube/helper/yt_play.py | 6 ++++-- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index ced557ab2..a8bc4a7b9 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -10,7 +10,7 @@ from __future__ import absolute_import, division, unicode_literals import atexit -from traceback import format_exc, format_stack +from traceback import format_stack from requests import Session from requests.adapters import HTTPAdapter, Retry @@ -101,7 +101,6 @@ def request(self, url, method='GET', except (RequestException, self._default_exc) as exc: response_text = exc.response and exc.response.text stack_trace = format_stack() - exc_tb = format_exc() error_details = {'exc': exc} if error_hook: @@ -147,7 +146,7 @@ def request(self, url, method='GET', ) log_error('\n'.join([part for part in [ - error_title, error_info, response_text, stack_trace, exc_tb + error_title, error_info, response_text, stack_trace ] if part])) if raise_exc: diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index 4b5cf9f19..3d5282dc9 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -10,7 +10,7 @@ from __future__ import absolute_import, division, unicode_literals -from traceback import format_exc +from traceback import format_stack from ..abstract_provider_runner import AbstractProviderRunner from ...compatibility import xbmcgui, xbmcplugin @@ -51,7 +51,7 @@ def run(self, provider, context): except KodionException as exc: if provider.handle_exception(context, exc): context.log_error('XbmcRunner.run - {exc}:\n{details}'.format( - exc=exc, details=format_exc() + exc=exc, details=''.join(format_stack()) )) xbmcgui.Dialog().ok("Error in ContentProvider", exc.__str__()) xbmcplugin.endOfDirectory(self.handle, succeeded=False) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 0d990b709..46c7c2ba9 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -15,7 +15,7 @@ import sqlite3 import time from datetime import datetime -from traceback import format_exc +from traceback import format_stack from ..logger import log_error from ..utils.datetime_parser import since_epoch @@ -176,7 +176,7 @@ def _open(self): isolation_level=None) except sqlite3.OperationalError as exc: log_error('SQLStorage._execute - {exc}:\n{details}'.format( - exc=exc, details=format_exc() + exc=exc, details=''.join(format_stack()) )) return False @@ -247,12 +247,12 @@ def _execute(cursor, query, values=None, many=False): return cursor.execute(query, values) except sqlite3.OperationalError as exc: log_error('SQLStorage._execute - {exc}:\n{details}'.format( - exc=exc, details=format_exc() + exc=exc, details=''.join(format_stack()) )) time.sleep(0.1) except sqlite3.Error as exc: log_error('SQLStorage._execute - {exc}:\n{details}'.format( - exc=exc, details=format_exc() + exc=exc, details=''.join(format_stack()) )) return [] return [] diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index c24d34546..b494e20e0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -13,7 +13,7 @@ import json import random import re -from traceback import format_exc +from traceback import format_stack from .ratebypass import ratebypass from .signature.cipher import Cipher @@ -931,12 +931,13 @@ def _process_signature_cipher(self, stream_map): try: signature = self._cipher.get_signature(encrypted_signature) except Exception as exc: - self._context.log_debug('{0}: {1}\n{2}'.format( - exc, encrypted_signature, format_exc() + self._context.log_error('VideoInfo._process_signature_cipher - ' + 'failed to extract URL from |{sig}|\n' + '{exc}:\n{details}'.format( + sig=encrypted_signature, + exc=exc, + details=''.join(format_stack()) )) - self._context.log_error( - 'Failed to extract URL from signatureCipher' - ) return None self._data_cache.set_item(encrypted_signature, {'sig': signature}) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 484d6857e..16963ce3f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -12,7 +12,7 @@ import json import random -from traceback import format_exc +from traceback import format_stack from ..helper import utils, v3 from ..youtube_exceptions import YouTubeException @@ -47,8 +47,10 @@ def play_video(provider, context): try: video_streams = client.get_video_streams(context, video_id) except YouTubeException as exc: + context.log_error('yt_play.play_video - {exc}:\n{details}'.format( + exc=exc, details=''.join(format_stack()) + )) ui.show_notification(message=exc.get_message()) - context.log_error(format_exc()) return False if not video_streams: From 61e0a859321ff1a43415370e99cc9e4fc0a8892a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 09:19:18 +1100 Subject: [PATCH 121/141] Allow for proper inheritance of request exceptions - Also tidy up imports after 860defd --- .../youtube_plugin/kodion/network/requests.py | 11 ++++++--- .../youtube/client/login_client.py | 23 +++++++++---------- .../youtube/client/request_client.py | 9 +++++++- .../youtube/helper/yt_playlist.py | 1 - .../youtube_plugin/youtube/helper/yt_video.py | 1 - 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index a8bc4a7b9..c5585b664 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -40,10 +40,15 @@ class BaseRequestsClass(object): _session.mount('https://', _http_adapter) atexit.register(_session.close) - def __init__(self, exc_type=RequestException): + def __init__(self, exc_type=None): self._verify = _settings.verify_ssl() self._timeout = _settings.get_timeout() - self._default_exc = exc_type + if isinstance(exc_type, tuple): + self._default_exc = (RequestException,) + exc_type + elif exc_type: + self._default_exc = (RequestException, exc_type) + else: + self._default_exc = RequestException def __del__(self): self._session.close() @@ -98,7 +103,7 @@ def request(self, url, method='GET', else: response.raise_for_status() - except (RequestException, self._default_exc) as exc: + except self._default_exc as exc: response_text = exc.response and exc.response.text stack_trace = format_stack() error_details = {'exc': exc} diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index a1f00d46d..f6a70d0ea 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -23,7 +23,6 @@ InvalidGrant, InvalidJSON, LoginException, - YouTubeException, ) from ...kodion.compatibility import parse_qsl from ...kodion.logger import log_debug @@ -87,9 +86,9 @@ def _response_hook(**kwargs): try: json_data = response.json() if 'error' in json_data: - raise YouTubeException('"error" in response JSON data', - json_data=json_data, - response=response) + raise LoginException('"error" in response JSON data', + json_data=json_data, + response=response) except ValueError as exc: raise InvalidJSON(exc, response=response) response.raise_for_status() @@ -139,8 +138,8 @@ def revoke(self, refresh_token): method='POST', data=post_data, headers=headers, - response_hook=self._response_hook, - error_hook=self._error_hook, + response_hook=LoginClient._response_hook, + error_hook=LoginClient._error_hook, error_title='Logout Failed', error_info='Revoke failed: {exc}', raise_exc=True) @@ -179,8 +178,8 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): method='POST', data=post_data, headers=headers, - response_hook=self._response_hook, - error_hook=self._error_hook, + response_hook=LoginClient._response_hook, + error_hook=LoginClient._error_hook, error_title='Login Failed', error_info=('Refresh token failed' ' {client}: {{exc}}' @@ -227,8 +226,8 @@ def request_access_token(self, code, client_id='', client_secret=''): method='POST', data=post_data, headers=headers, - response_hook=self._response_hook, - error_hook=self._error_hook, + response_hook=LoginClient._response_hook, + error_hook=LoginClient._error_hook, error_title='Login Failed: Unknown response', error_info=('Access token request failed' ' {client}: {{exc}}' @@ -263,8 +262,8 @@ def request_device_and_user_code(self, client_id=''): method='POST', data=post_data, headers=headers, - response_hook=self._response_hook, - error_hook=self._error_hook, + response_hook=LoginClient._response_hook, + error_hook=LoginClient._error_hook, error_title='Login Failed: Unknown response', error_info=('Device/user code request failed' ' {client}: {{exc}}' diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 045e35006..3fd740386 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -258,8 +258,15 @@ class YouTubeRequestClient(BaseRequestsClass): }, } - def __init__(self, exc_type=YouTubeException): + def __init__(self, exc_type=None): + if isinstance(exc_type, tuple): + exc_type = (YouTubeException,) + exc_type + elif exc_type: + exc_type = (YouTubeException, exc_type) + else: + exc_type = YouTubeException super(YouTubeRequestClient, self).__init__(exc_type=exc_type) + self._access_token = None self.video_id = None diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index b5fbacca6..936d859d8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -from ..helper import v3 from ...kodion import KodionException from ...kodion.utils import find_video_id diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 9ce0f8523..aa6086e92 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -from ..helper import v3 from ...kodion import KodionException from ...kodion.utils import find_video_id From 6805638e448862e02f85b45bc0277d857e87963f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 09:42:25 +1100 Subject: [PATCH 122/141] Create new Context without default plugin_id value --- resources/lib/youtube_authentication.py | 18 ++++++++---------- .../kodion/constants/__init__.py | 10 +++++++++- .../kodion/context/xbmc/xbmc_context.py | 14 +++++++++----- resources/lib/youtube_plugin/kodion/runner.py | 2 +- resources/lib/youtube_plugin/kodion/service.py | 2 +- .../youtube/client/__config__.py | 2 +- .../youtube_plugin/youtube/client/youtube.py | 2 +- resources/lib/youtube_registration.py | 2 +- resources/lib/youtube_requests.py | 4 ++-- resources/lib/youtube_resolver.py | 4 ++-- 10 files changed, 35 insertions(+), 25 deletions(-) diff --git a/resources/lib/youtube_authentication.py b/resources/lib/youtube_authentication.py index fe463b867..6c7f0739c 100644 --- a/resources/lib/youtube_authentication.py +++ b/resources/lib/youtube_authentication.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals from youtube_plugin.youtube.provider import Provider +from youtube_plugin.kodion.constants import ADDON_ID from youtube_plugin.kodion.context import Context from youtube_plugin.youtube.helper import yt_login @@ -27,8 +28,7 @@ def __add_new_developer(addon_id): :param addon_id: id of the add-on being added :return: """ - params = {'addon_id': addon_id} - context = Context(params=params, plugin_id='plugin.video.youtube') + context = Context(params={'addon_id': addon_id}) access_manager = context.get_access_manager() developers = access_manager.get_developers() @@ -45,14 +45,13 @@ def __auth(addon_id, mode=SIGN_IN): :param mode: SIGN_IN or SIGN_OUT :return: addon provider, context and client """ - if not addon_id or addon_id == 'plugin.video.youtube': - context = Context(plugin_id='plugin.video.youtube') + if not addon_id or addon_id == ADDON_ID: + context = Context() context.log_error('Developer authentication: |%s| Invalid addon_id' % addon_id) return __add_new_developer(addon_id) - params = {'addon_id': addon_id} provider = Provider() - context = Context(params=params, plugin_id='plugin.video.youtube') + context = Context(params={'addon_id': addon_id}) _ = provider.get_client(context=context) # NOQA logged_in = provider.is_logged_in() @@ -156,12 +155,11 @@ def reset_access_tokens(addon_id): :param addon_id: id of the add-on having it's access tokens reset :return: """ - if not addon_id or addon_id == 'plugin.video.youtube': - context = Context(plugin_id='plugin.video.youtube') + if not addon_id or addon_id == ADDON_ID: + context = Context() context.log_error('Developer reset access tokens: |%s| Invalid addon_id' % addon_id) return - params = {'addon_id': addon_id} - context = Context(params=params, plugin_id='plugin.video.youtube') + context = Context(params={'addon_id': addon_id}) access_manager = context.get_access_manager() access_manager.update_dev_access_token(addon_id, access_token='', refresh_token='') diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 91e90d688..9a297be63 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -16,4 +16,12 @@ from . import const_paths as paths -__all__ = ['setting', 'sort_method', 'content_type', 'paths'] +ADDON_ID = 'plugin.video.youtube' + +__all__ = ( + 'ADDON_ID', + 'content_type', + 'paths', + 'setting', + 'sort_method', +) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 20b69bf80..d84843a6d 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -26,6 +26,7 @@ xbmcplugin, xbmcvfs, ) +from ...constants import ADDON_ID from ...player.xbmc.xbmc_player import XbmcPlayer from ...player.xbmc.xbmc_playlist import XbmcPlaylist from ...settings.xbmc.xbmc_plugin_settings import XbmcPluginSettings @@ -255,7 +256,7 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override if plugin_id: self._addon = xbmcaddon.Addon(id=plugin_id) else: - self._addon = xbmcaddon.Addon(id='plugin.video.youtube') + self._addon = xbmcaddon.Addon(id=ADDON_ID) """ I don't know what xbmc/kodi is doing with a simple uri, but we have to extract the information from the @@ -286,7 +287,7 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override self._video_player = None self._audio_player = None self._plugin_handle = int(sys.argv[1]) if num_args > 1 else -1 - self._plugin_id = plugin_id or self._addon.getAddonInfo('id') + self._plugin_id = plugin_id or ADDON_ID self._plugin_name = plugin_name or self._addon.getAddonInfo('name') self._version = self._addon.getAddonInfo('version') self._native_path = xbmcvfs.translatePath(self._addon.getAddonInfo('path')) @@ -431,8 +432,11 @@ def clone(self, new_path=None, new_params=None): if not new_params: new_params = self.get_params() - new_context = XbmcContext(path=new_path, params=new_params, plugin_name=self._plugin_name, - plugin_id=self._plugin_id, override=False) + new_context = XbmcContext(path=new_path, + params=new_params, + plugin_name=self._plugin_name, + plugin_id=self._plugin_id, + override=False) new_context._function_cache = self._function_cache new_context._search_history = self._search_history new_context._favorite_list = self._favorite_list @@ -488,7 +492,7 @@ def send_notification(self, method, data): data = json.dumps(data) self.log_debug('send_notification: |%s| -> |%s|' % (method, data)) data = '\\"[\\"%s\\"]\\"' % quote(data) - self.execute('NotifyAll(plugin.video.youtube,%s,%s)' % (method, data)) + self.execute('NotifyAll({0},{1},{2})'.format(ADDON_ID, method, data)) def use_inputstream_adaptive(self): if self._settings.use_isa(): diff --git a/resources/lib/youtube_plugin/kodion/runner.py b/resources/lib/youtube_plugin/kodion/runner.py index d03d44d69..458fe9e97 100644 --- a/resources/lib/youtube_plugin/kodion/runner.py +++ b/resources/lib/youtube_plugin/kodion/runner.py @@ -30,7 +30,7 @@ def run(provider, context=None): start_time = timeit.default_timer() if not context: - context = Context(plugin_id='plugin.video.youtube') + context = Context() context.log_debug('Starting Kodion framework by bromix...') python_version = 'Unknown version of Python' diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index e50d59d71..609ab63d6 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -51,7 +51,7 @@ def run(): ping_timestamp = None first_run = True - context = Context(plugin_id='plugin.video.youtube') + context = Context() context.log_debug('YouTube service initialization...') diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index fbe3eaf3f..545becf4e 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -192,7 +192,7 @@ def _strip_api_keys(self, api_key, client_id, client_secret): return return_key, return_id, return_secret -_api_check = APICheck(Context(plugin_id='plugin.video.youtube')) +_api_check = APICheck(Context()) keys_changed = _api_check.changed current_user = _api_check.get_current_user() diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 0d9f65cce..272840e84 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -22,7 +22,7 @@ from ...kodion.utils import datetime_parser, strip_html_from_text, to_unicode -_context = Context(plugin_id='plugin.video.youtube') +_context = Context() class YouTube(LoginClient): diff --git a/resources/lib/youtube_registration.py b/resources/lib/youtube_registration.py index 548be86bb..a5585082d 100644 --- a/resources/lib/youtube_registration.py +++ b/resources/lib/youtube_registration.py @@ -42,7 +42,7 @@ def register_api_keys(addon_id, api_key, client_id, client_secret): :param client_secret: YouTube Data v3 Client secret """ - context = Context(plugin_id='plugin.video.youtube') + context = Context() if not addon_id or addon_id == 'plugin.video.youtube': context.log_error('Register API Keys: |%s| Invalid addon_id' % addon_id) diff --git a/resources/lib/youtube_requests.py b/resources/lib/youtube_requests.py index 81b532f88..822a04c38 100644 --- a/resources/lib/youtube_requests.py +++ b/resources/lib/youtube_requests.py @@ -22,9 +22,9 @@ def __get_core_components(addon_id=None): """ provider = Provider() if addon_id is not None: - context = Context(params={'addon_id': addon_id}, plugin_id='plugin.video.youtube') + context = Context(params={'addon_id': addon_id}) else: - context = Context(plugin_id='plugin.video.youtube') + context = Context() client = provider.get_client(context=context) return provider, context, client diff --git a/resources/lib/youtube_resolver.py b/resources/lib/youtube_resolver.py index 53c771a3e..82429150f 100644 --- a/resources/lib/youtube_resolver.py +++ b/resources/lib/youtube_resolver.py @@ -18,9 +18,9 @@ def _get_core_components(addon_id=None): provider = Provider() if addon_id is not None: - context = Context(params={'addon_id': addon_id}, plugin_id='plugin.video.youtube') + context = Context(params={'addon_id': addon_id}) else: - context = Context(plugin_id='plugin.video.youtube') + context = Context() client = provider.get_client(context=context) return provider, context, client From 613d51e0490d52d2ff870dbb6857db4a6dbda780 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:33:20 +1100 Subject: [PATCH 123/141] Youtube class now requires Context as a parameter - Reuse existing, rather than creating a new Context at the module level - Remove log method callbacks --- .../youtube/client/login_client.py | 11 --- .../youtube_plugin/youtube/client/youtube.py | 94 +++++++++++-------- .../lib/youtube_plugin/youtube/provider.py | 5 +- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index f6a70d0ea..76964ea66 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -76,8 +76,6 @@ def __init__(self, config=None, language='en-US', region='', self._access_token = access_token self._access_token_tv = access_token_tv - self._log_error_callback = None - super(LoginClient, self).__init__(exc_type=LoginException) @staticmethod @@ -106,15 +104,6 @@ def _error_hook(**kwargs): return None, None, None, json_data, False, InvalidGrant(json_data) return None, None, None, json_data, False, LoginException(json_data) - def set_log_error(self, callback): - self._log_error_callback = callback - - def log_error(self, text): - if self._log_error_callback: - self._log_error_callback(text) - else: - print(text) - def verify(self): return self._verify diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 272840e84..5b6d7df45 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -18,15 +18,12 @@ from .login_client import LoginClient from ..helper.video_info import VideoInfo from ..youtube_exceptions import InvalidJSON, YouTubeException -from ...kodion import Context from ...kodion.utils import datetime_parser, strip_html_from_text, to_unicode -_context = Context() - - class YouTube(LoginClient): - def __init__(self, **kwargs): + def __init__(self, context, **kwargs): + self._context = context if not kwargs.get('config'): kwargs['config'] = {} if 'items_per_page' in kwargs: @@ -381,11 +378,11 @@ def _get_recommendations_for_home(self): 'items': [] } - watch_history_id = _context.get_access_manager().get_watch_history_id() - if not watch_history_id or watch_history_id == 'HL': + history_id = self._context.get_access_manager().get_watch_history_id() + if not history_id or history_id == 'HL': return payload - cache = _context.get_data_cache() + cache = self._context.get_data_cache() # Do we have a cached result? cache_home_key = 'get-activities-home' @@ -401,7 +398,7 @@ def _get_recommendations_for_home(self): # Fetch history and recommended items. Use threads for faster execution. def helper(video_id, responses): - _context.log_debug( + self._context.log_debug( 'Method get_activities: doing expensive API fetch for related' 'items for video %s' % video_id ) @@ -412,7 +409,7 @@ def helper(video_id, responses): item['plugin_fetched_for'] = video_id responses.extend(di['items']) - history = self.get_playlist_items(watch_history_id, max_results=50) + history = self.get_playlist_items(history_id, max_results=50) if not history.get('items'): return payload @@ -712,10 +709,11 @@ def get_live_events(self, 'maxResults': str(self._max_results)} if location: - location = _context.get_settings().get_location() + settings = self._context.get_settings() + location = settings.get_location() if location: params['location'] = location - params['locationRadius'] = _context.get_settings().get_location_radius() + params['locationRadius'] = settings.get_location_radius() if page_token: params['pageToken'] = page_token @@ -887,10 +885,11 @@ def search(self, break if params['type'] == 'video' and location: - location = _context.get_settings().get_location() + settings = self._context.get_settings() + location = settings.get_location() if location: params['location'] = location - params['locationRadius'] = _context.get_settings().get_location_radius() + params['locationRadius'] = settings.get_location_radius() return self.perform_v3_request(method='GET', path='search', @@ -918,7 +917,7 @@ def _perform(_page_token, _offset, _result): 'items': [] } - cache = _context.get_data_cache() + cache = self._context.get_data_cache() # if new uploads is cached cache_items_key = 'my-subscriptions-items' @@ -1189,11 +1188,10 @@ def _perform(_playlist_idx, _page_token, _offset, _result): return result - @staticmethod - def _response_hook(**kwargs): + def _response_hook(self, **kwargs): response = kwargs['response'] - _context.log_debug('[data] v3 response: |{0.status_code}|\n' - '\theaders: |{0.headers}|'.format(response)) + self._context.log_debug('[data] v3 response: |{0.status_code}|\n' + '\theaders: |{0.headers}|'.format(response)) try: json_data = response.json() if 'error' in json_data: @@ -1205,8 +1203,7 @@ def _response_hook(**kwargs): response.raise_for_status() return json_data - @staticmethod - def _error_hook(**kwargs): + def _error_hook(self, **kwargs): exc = kwargs['exc'] json_data = getattr(exc, 'json_data', None) data = getattr(exc, 'pass_data', False) and json_data @@ -1224,10 +1221,10 @@ def _error_hook(**kwargs): ok_dialog = False timeout = 5000 if reason == 'accessNotConfigured': - notification = _context.localize('key.requirement.notification') + notification = self._context.localize('key.requirement') ok_dialog = True elif reason == 'keyInvalid' and message == 'Bad Request': - notification = _context.localize('api.key.incorrect') + notification = self._context.localize('api.key.incorrect') timeout = 7000 elif reason in ('quotaExceeded', 'dailyLimitExceeded'): notification = message @@ -1235,13 +1232,13 @@ def _error_hook(**kwargs): else: notification = message - title = '{0}: {1}'.format(_context.get_name(), reason) + title = '{0}: {1}'.format(self._context.get_name(), reason) if ok_dialog: - _context.get_ui().on_ok(title, notification) + self._context.get_ui().on_ok(title, notification) else: - _context.get_ui().show_notification(notification, - title, - time_ms=timeout) + self._context.get_ui().show_notification(notification, + title, + time_ms=timeout) info = ('[data] v3 error: {reason}\n' '\texc: |{exc}|\n' @@ -1282,15 +1279,16 @@ def perform_v3_request(self, method='GET', headers=None, path=None, else: log_params = None - _context.log_debug('[data] v3 request: |{method}|\n' - '\tpath: |{path}|\n' - '\tparams: |{params}|\n' - '\tpost_data: |{data}|\n' - '\theaders: |{headers}|'.format(method=method, - path=path, - params=log_params, - data=post_data, - headers=_headers)) + self._context.log_debug('[data] v3 request: |{method}|\n' + '\tpath: |{path}|\n' + '\tparams: |{params}|\n' + '\tpost_data: |{data}|\n' + '\theaders: |{headers}|' + .format(method=method, + path=path, + params=log_params, + data=post_data, + headers=_headers)) json_data = self.request(_url, method=method, @@ -1336,13 +1334,27 @@ def perform_v1_tv_request(self, method='GET', headers=None, path=None, log_params['location'] = 'xx.xxxx,xx.xxxx' else: log_params = None - _context.log_debug('[data] v1 request: |{0}| path: |{1}| params: |{2}| post_data: |{3}|'.format(method, path, log_params, post_data)) - - result = self.request(_url, method=method, headers=_headers, json=post_data, params=_params) + self._context.log_debug('[data] v1 request: |{method}|\n' + '\tpath: |{path}|\n' + '\tparams: |{params}|\n' + '\tpost_data: |{data}|\n' + '\theaders: |{headers}|' + .format(method=method, + path=path, + params=log_params, + data=post_data, + headers=_headers)) + + result = self.request(_url, + method=method, + headers=_headers, + json=post_data, + params=_params) if result is None: return {} - _context.log_debug('[data] v1 response: |{0}| headers: |{1}|'.format(result.status_code, result.headers)) + self._context.log_debug('[data] v1 response: |{0.status_code}|\n' + '\theaders: |{0.headers}|'.format(result)) if result.headers.get('content-type', '').startswith('application/json'): try: diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index d406c3739..e13be4b0c 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -194,14 +194,14 @@ def get_client(self, context): refresh_tokens = refresh_tokens.split('|') context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) - client = YouTube(language=language, + client = YouTube(context=context, + language=language, region=region, items_per_page=items_per_page, config=dev_keys if dev_keys else youtube_config) with client: if not refresh_tokens or not refresh_tokens[0]: - client.set_log_error(context.log_error) self._client = client # create new access tokens @@ -242,7 +242,6 @@ def get_client(self, context): client.set_access_token(access_token=access_tokens[1]) client.set_access_token_tv(access_token_tv=access_tokens[0]) - client.set_log_error(context.log_error) self._client = client return self._client From 58195b765658baa4ca09561edf4541212a0631ff Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:39:20 +1100 Subject: [PATCH 124/141] Fix regression after 72480b5 caused by typo --- .../lib/youtube_plugin/youtube/helper/url_to_item_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index c81aef0eb..bfc96ee8e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -97,7 +97,7 @@ def add_url(self, url, provider, context): return playlist_item = DirectoryItem( - '', context.create_uri(['playlist', playlist_id]), new_params + '', context.create_uri(['playlist', playlist_id], new_params), ) playlist_item.set_fanart(provider.get_fanart(context)) self._playlist_id_dict[playlist_id] = playlist_item From 9f3ac5dbe5cdaed36545a27324375cf3340db70d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:41:21 +1100 Subject: [PATCH 125/141] Fix incorrect MPEG-DASH mime type --- resources/lib/youtube_plugin/kodion/network/http_server.py | 4 ++-- resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 18d4a041e..f665346da 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -106,7 +106,7 @@ def do_GET(self): try: with open(file_path, 'rb') as f: self.send_response(200) - self.send_header('Content-Type', 'application/xml+dash') + self.send_header('Content-Type', 'application/dash+xml') self.send_header('Content-Length', str(os.path.getsize(file_path))) self.end_headers() @@ -208,7 +208,7 @@ def do_HEAD(self): self.send_error(404, response) else: self.send_response(200) - self.send_header('Content-Type', 'application/xml+dash') + self.send_header('Content-Type', 'application/dash+xml') self.send_header('Content-Length', str(os.path.getsize(file_path))) self.end_headers() diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index 0d3a0e615..d89cfba8f 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -81,7 +81,7 @@ def video_playback_item(context, video_item): and context.addon_enabled('inputstream.adaptive')): if video_item.use_mpd_video(): manifest_type = 'mpd' - mime_type = 'application/xml+dash' + mime_type = 'application/dash+xml' """ # MPD manifest update is currently broken # Following line will force a full update but restart live stream From 44251e48261d23268dbcccad6be93e0b80928502 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:44:22 +1100 Subject: [PATCH 126/141] Remove redundant is_empty method on Storage sub-classes - Just use re-named base class method --- resources/lib/youtube_plugin/kodion/sql_store/data_cache.py | 3 --- .../lib/youtube_plugin/kodion/sql_store/playback_history.py | 3 --- .../lib/youtube_plugin/kodion/sql_store/search_history.py | 3 --- resources/lib/youtube_plugin/kodion/sql_store/storage.py | 4 ++-- 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index e0271f939..197ca8add 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -24,9 +24,6 @@ def __init__(self, filename, max_file_size_mb=5): super(DataCache, self).__init__(filename, max_file_size_kb=max_file_size_kb) - def is_empty(self): - return self._is_empty() - def get_items(self, content_ids, seconds): result = self._get_by_ids(content_ids, seconds=seconds, as_dict=True) return result diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index 56874ba97..03a4a9ce3 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -21,9 +21,6 @@ class PlaybackHistory(Storage): def __init__(self, filename): super(PlaybackHistory, self).__init__(filename) - def is_empty(self): - return self._is_empty() - def _add_last_played(self, value, item): value['last_played'] = self._convert_timestamp(item[1]) return value diff --git a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py index b53110b32..6f6784534 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -25,9 +25,6 @@ def __init__(self, filename, max_item_count=10): super(SearchHistory, self).__init__(filename, max_item_count=max_item_count) - def is_empty(self): - return self._is_empty() - def get_items(self): result = self._get_by_ids(oldest_first=False, limit=self._max_item_count, diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 46c7c2ba9..41faa7d50 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -285,7 +285,7 @@ def _optimize_item_count(self, limit=-1, defer=False): # clear db if max item count has been set to 0 if not self._max_item_count: - if not self._is_empty(): + if not self.is_empty(): return self.clear(defer) return False @@ -329,7 +329,7 @@ def clear(self, defer=False): self._execute(cursor, 'VACUUM') return True - def _is_empty(self): + def is_empty(self): with self as (db, cursor), db: result = self._execute(cursor, self._sql['is_empty']) for item in result: From 478576c28b1237e71a92e7d88e613d41293a2635 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 24 Dec 2023 15:06:06 +1100 Subject: [PATCH 127/141] Change constants module import aliases - "content_type" to "content" - "setting" to "settings" - "sort_method" to "sort" --- .../kodion/abstract_provider.py | 15 +- .../kodion/constants/__init__.py | 16 ++- .../kodion/context/abstract_context.py | 9 +- .../kodion/settings/abstract_settings.py | 128 +++++++++--------- .../youtube/helper/yt_specials.py | 26 ++-- .../lib/youtube_plugin/youtube/provider.py | 47 ++++--- 6 files changed, 124 insertions(+), 117 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 57c5fdffa..f68cab2b0 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -12,7 +12,7 @@ import re -from . import constants +from .constants import settings, paths, content from .compatibility import quote, unquote from .exceptions import KodionException from .items import ( @@ -39,25 +39,25 @@ def __init__(self): self.register_path(r''.join([ '^/', - constants.paths.WATCH_LATER, + paths.WATCH_LATER, '/(?Padd|clear|list|remove)/?$' ]), '_internal_watch_later') self.register_path(r''.join([ '^/', - constants.paths.FAVORITES, + paths.FAVORITES, '/(?Padd|clear|list|remove)/?$' ]), '_internal_favorite') self.register_path(r''.join([ '^/', - constants.paths.SEARCH, + paths.SEARCH, '/(?Pinput|query|list|remove|clear|rename)/?$' ]), '_internal_search') self.register_path(r''.join([ '^/', - constants.paths.HISTORY, + paths.HISTORY, '/$' ]), 'on_playback_history') @@ -92,7 +92,7 @@ def _process_wizard(self, context): # start the setup wizard wizard_steps = [] if context.get_settings().is_setup_wizard_enabled(): - context.get_settings().set_bool(constants.setting.SETUP_WIZARD, False) + context.get_settings().set_bool(settings.SETUP_WIZARD, False) wizard_steps.extend(self.get_wizard_steps(context)) if wizard_steps and context.get_ui().on_yes_no_input(context.get_name(), @@ -324,8 +324,7 @@ def _internal_search(self, context, re_match): if isinstance(query, bytes): query = query.decode('utf-8') return self.on_search(query, context, re_match) - - context.set_content_type(constants.content_type.VIDEOS) + context.set_content_type(content.VIDEOS) result = [] location = context.get_param('location', False) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 9a297be63..819a9060e 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -10,18 +10,20 @@ from __future__ import absolute_import, division, unicode_literals -from . import const_settings as setting -from . import const_sort_methods as sort_method -from . import const_content_types as content_type -from . import const_paths as paths +from . import ( + const_content_types as content, + const_paths as paths, + const_settings as settings, + const_sort_methods as sort, +) ADDON_ID = 'plugin.video.youtube' __all__ = ( 'ADDON_ID', - 'content_type', + 'content', 'paths', - 'setting', - 'sort_method', + 'settings', + 'sort', ) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 1f3851bc5..3a789dbd9 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -12,8 +12,9 @@ import os -from .. import constants, logger +from .. import logger from ..compatibility import urlencode +from ..constants import settings from ..json_store import AccessManager from ..sql_store import ( DataCache, @@ -148,7 +149,7 @@ def get_playback_history(self): def get_data_cache(self): if not self._data_cache: - max_cache_size_mb = self.get_settings().get_int(constants.setting.CACHE_SIZE, -1) + max_cache_size_mb = self.get_settings().get_int(settings.CACHE_SIZE, -1) if max_cache_size_mb <= 0: max_cache_size_mb = 5 else: @@ -159,7 +160,7 @@ def get_data_cache(self): def get_function_cache(self): if not self._function_cache: - max_cache_size_mb = self.get_settings().get_int(constants.setting.CACHE_SIZE, -1) + max_cache_size_mb = self.get_settings().get_int(settings.CACHE_SIZE, -1) if max_cache_size_mb <= 0: max_cache_size_mb = 5 else: @@ -170,7 +171,7 @@ def get_function_cache(self): def get_search_history(self): if not self._search_history: - max_search_history_items = self.get_settings().get_int(constants.setting.SEARCH_SIZE, 50) + max_search_history_items = self.get_settings().get_int(settings.SEARCH_SIZE, 50) self._search_history = SearchHistory(os.path.join(self.get_cache_path(), 'search'), max_item_count=max_search_history_items) return self._search_history diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 527ab40ff..0375ce2e1 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -12,7 +12,7 @@ import sys -from ..constants import setting as SETTINGS +from ..constants import settings class AbstractSettings(object): @@ -57,7 +57,7 @@ def open_settings(self): raise NotImplementedError() def get_items_per_page(self): - return self.get_int(SETTINGS.ITEMS_PER_PAGE, 50) + return self.get_int(settings.ITEMS_PER_PAGE, 50) def get_video_quality(self, quality_map_override=None): vq_dict = {0: 240, @@ -69,75 +69,75 @@ def get_video_quality(self, quality_map_override=None): if quality_map_override is not None: vq_dict = quality_map_override - vq = self.get_int(SETTINGS.VIDEO_QUALITY, 1) + vq = self.get_int(settings.VIDEO_QUALITY, 1) return vq_dict[vq] def ask_for_video_quality(self): - return self.get_bool(SETTINGS.VIDEO_QUALITY_ASK, False) + return self.get_bool(settings.VIDEO_QUALITY_ASK, False) def show_fanart(self): - return self.get_bool(SETTINGS.SHOW_FANART, True) + return self.get_bool(settings.SHOW_FANART, True) def get_search_history_size(self): - return self.get_int(SETTINGS.SEARCH_SIZE, 50) + return self.get_int(settings.SEARCH_SIZE, 50) def is_setup_wizard_enabled(self): - return self.get_bool(SETTINGS.SETUP_WIZARD, False) + return self.get_bool(settings.SETUP_WIZARD, False) def is_support_alternative_player_enabled(self): - return self.get_bool(SETTINGS.SUPPORT_ALTERNATIVE_PLAYER, False) + return self.get_bool(settings.SUPPORT_ALTERNATIVE_PLAYER, False) def alternative_player_web_urls(self): - return self.get_bool(SETTINGS.ALTERNATIVE_PLAYER_WEB_URLS, False) + return self.get_bool(settings.ALTERNATIVE_PLAYER_WEB_URLS, False) def use_isa(self): - return self.get_bool(SETTINGS.USE_ISA, False) + return self.get_bool(settings.USE_ISA, False) def subtitle_languages(self): - return self.get_int(SETTINGS.SUBTITLE_LANGUAGE, 0) + return self.get_int(settings.SUBTITLE_LANGUAGE, 0) def subtitle_download(self): - return self.get_bool(SETTINGS.SUBTITLE_DOWNLOAD, False) + return self.get_bool(settings.SUBTITLE_DOWNLOAD, False) def audio_only(self): - return self.get_bool(SETTINGS.AUDIO_ONLY, False) + return self.get_bool(settings.AUDIO_ONLY, False) def set_subtitle_languages(self, value): - return self.set_int(SETTINGS.SUBTITLE_LANGUAGE, value) + return self.set_int(settings.SUBTITLE_LANGUAGE, value) def set_subtitle_download(self, value): - return self.set_bool(SETTINGS.SUBTITLE_DOWNLOAD, value) + return self.set_bool(settings.SUBTITLE_DOWNLOAD, value) def use_thumbnail_size(self): - size = self.get_int(SETTINGS.THUMB_SIZE, 0) + size = self.get_int(settings.THUMB_SIZE, 0) sizes = {0: 'medium', 1: 'high'} return sizes[size] def safe_search(self): - index = self.get_int(SETTINGS.SAFE_SEARCH, 0) + index = self.get_int(settings.SAFE_SEARCH, 0) values = {0: 'moderate', 1: 'none', 2: 'strict'} return values[index] def age_gate(self): - return self.get_bool(SETTINGS.AGE_GATE, True) + return self.get_bool(settings.AGE_GATE, True) def verify_ssl(self): - verify = self.get_bool(SETTINGS.VERIFY_SSL, False) + verify = self.get_bool(settings.VERIFY_SSL, False) if sys.version_info <= (2, 7, 9): verify = False return verify def get_timeout(self): - connect_timeout = self.get_int(SETTINGS.CONNECT_TIMEOUT, 9) + 0.5 - read_timout = self.get_int(SETTINGS.READ_TIMEOUT, 27) + connect_timeout = self.get_int(settings.CONNECT_TIMEOUT, 9) + 0.5 + read_timout = self.get_int(settings.READ_TIMEOUT, 27) return connect_timeout, read_timout def allow_dev_keys(self): - return self.get_bool(SETTINGS.ALLOW_DEV_KEYS, False) + return self.get_bool(settings.ALLOW_DEV_KEYS, False) def use_mpd_videos(self): if self.use_isa(): - return self.get_bool(SETTINGS.MPD_VIDEOS, False) + return self.get_bool(settings.MPD_VIDEOS, False) return False _LIVE_STREAM_TYPES = { @@ -149,26 +149,26 @@ def use_mpd_videos(self): def get_live_stream_type(self): if self.use_isa(): - stream_type = self.get_int(SETTINGS.LIVE_STREAMS + '.1', 0) + stream_type = self.get_int(settings.LIVE_STREAMS + '.1', 0) else: - stream_type = self.get_int(SETTINGS.LIVE_STREAMS + '.2', 0) + stream_type = self.get_int(settings.LIVE_STREAMS + '.2', 0) return self._LIVE_STREAM_TYPES.get(stream_type) or self._LIVE_STREAM_TYPES[0] def use_isa_live_streams(self): if self.use_isa(): - return self.get_int(SETTINGS.LIVE_STREAMS + '.1', 0) > 1 + return self.get_int(settings.LIVE_STREAMS + '.1', 0) > 1 return False def use_mpd_live_streams(self): if self.use_isa(): - return self.get_int(SETTINGS.LIVE_STREAMS + '.1', 0) == 3 + return self.get_int(settings.LIVE_STREAMS + '.1', 0) == 3 return False def httpd_port(self, port=None): default_port = 50152 if port is None: - port = self.get_int(SETTINGS.HTTPD_PORT, default_port) + port = self.get_int(settings.HTTPD_PORT, default_port) try: port = int(port) @@ -181,7 +181,7 @@ def httpd_listen(self, for_request=False, ip_address=None): default_octets = [0, 0, 0, 0,] if not ip_address: - ip_address = self.get_string(SETTINGS.HTTPD_LISTEN, + ip_address = self.get_string(settings.HTTPD_LISTEN, default_address) try: @@ -197,10 +197,10 @@ def httpd_listen(self, for_request=False, ip_address=None): return '.'.join(map(str, octets)) def set_httpd_listen(self, value): - return self.set_string(SETTINGS.HTTPD_LISTEN, value) + return self.set_string(settings.HTTPD_LISTEN, value) def httpd_whitelist(self): - allow_list = self.get_string(SETTINGS.HTTPD_WHITELIST, '') + allow_list = self.get_string(settings.HTTPD_WHITELIST, '') allow_list = ''.join(allow_list.split()).split(',') allow_list = [ self.httpd_listen(for_request=True, ip_address=ip_address) @@ -209,52 +209,52 @@ def httpd_whitelist(self): return allow_list def api_config_page(self): - return self.get_bool(SETTINGS.API_CONFIG_PAGE, False) + return self.get_bool(settings.API_CONFIG_PAGE, False) def api_id(self, new_id=None): if new_id is not None: - self.set_string(SETTINGS.API_ID, new_id) + self.set_string(settings.API_ID, new_id) return new_id - return self.get_string(SETTINGS.API_ID) + return self.get_string(settings.API_ID) def api_key(self, new_key=None): if new_key is not None: - self.set_string(SETTINGS.API_KEY, new_key) + self.set_string(settings.API_KEY, new_key) return new_key - return self.get_string(SETTINGS.API_KEY) + return self.get_string(settings.API_KEY) def api_secret(self, new_secret=None): if new_secret is not None: - self.set_string(SETTINGS.API_SECRET, new_secret) + self.set_string(settings.API_SECRET, new_secret) return new_secret - return self.get_string(SETTINGS.API_SECRET) + return self.get_string(settings.API_SECRET) def api_last_hash(self, new_hash=None): if new_hash is not None: - self.set_string(SETTINGS.API_LAST_HASH, new_hash) + self.set_string(settings.API_LAST_HASH, new_hash) return new_hash - return self.get_string(SETTINGS.API_LAST_HASH, '') + return self.get_string(settings.API_LAST_HASH, '') def user_access_token(self, new_access_token=None): if new_access_token is not None: - self.set_string(SETTINGS.USER_ACCESS_TOKEN, new_access_token) + self.set_string(settings.USER_ACCESS_TOKEN, new_access_token) return new_access_token - return self.get_string(SETTINGS.USER_ACCESS_TOKEN, '') + return self.get_string(settings.USER_ACCESS_TOKEN, '') def user_refresh_token(self, new_refresh_token=None): if new_refresh_token is not None: - self.set_string(SETTINGS.USER_REFRESH_TOKEN, new_refresh_token) + self.set_string(settings.USER_REFRESH_TOKEN, new_refresh_token) return new_refresh_token - return self.get_string(SETTINGS.USER_REFRESH_TOKEN, '') + return self.get_string(settings.USER_REFRESH_TOKEN, '') def user_token_expiration(self, new_token_expiration=None): if new_token_expiration is not None: - self.set_int(SETTINGS.USER_TOKEN_EXPIRATION, new_token_expiration) + self.set_int(settings.USER_TOKEN_EXPIRATION, new_token_expiration) return new_token_expiration - return self.get_int(SETTINGS.USER_TOKEN_EXPIRATION, -1) + return self.get_int(settings.USER_TOKEN_EXPIRATION, -1) def get_location(self): - location = self.get_string(SETTINGS.LOCATION, '').replace(' ', '').strip() + location = self.get_string(settings.LOCATION, '').replace(' ', '').strip() coords = location.split(',') latitude = longitude = None if len(coords) == 2: @@ -272,19 +272,19 @@ def get_location(self): return '' def set_location(self, value): - self.set_string(SETTINGS.LOCATION, value) + self.set_string(settings.LOCATION, value) def get_location_radius(self): - return ''.join([str(self.get_int(SETTINGS.LOCATION_RADIUS, 500)), 'km']) + return ''.join([str(self.get_int(settings.LOCATION_RADIUS, 500)), 'km']) def get_play_count_min_percent(self): - return self.get_int(SETTINGS.PLAY_COUNT_MIN_PERCENT, 0) + return self.get_int(settings.PLAY_COUNT_MIN_PERCENT, 0) def use_local_history(self): - return self.get_bool(SETTINGS.USE_LOCAL_HISTORY, False) + return self.get_bool(settings.USE_LOCAL_HISTORY, False) def use_remote_history(self): - return self.get_bool(SETTINGS.USE_REMOTE_HISTORY, False) + return self.get_bool(settings.USE_REMOTE_HISTORY, False) # Selections based on max width and min height at common (utra-)wide aspect ratios _QUALITY_SELECTIONS = { # Setting | Resolution @@ -304,14 +304,14 @@ def use_remote_history(self): def get_mpd_video_qualities(self): if not self.use_mpd_videos(): return [] - selected = self.get_int(SETTINGS.MPD_QUALITY_SELECTION, 4) + selected = self.get_int(settings.MPD_QUALITY_SELECTION, 4) return [quality for key, quality in sorted(self._QUALITY_SELECTIONS.items(), reverse=True) if selected >= key] def stream_features(self): - return self.get_string_list(SETTINGS.MPD_STREAM_FEATURES) + return self.get_string_list(settings.MPD_STREAM_FEATURES) _STREAM_SELECT = { 1: 'auto', @@ -320,32 +320,32 @@ def stream_features(self): } def stream_select(self): - select_type = self.get_int(SETTINGS.MPD_STREAM_SELECT, 1) + select_type = self.get_int(settings.MPD_STREAM_SELECT, 1) return self._STREAM_SELECT.get(select_type) or self._STREAM_SELECT[1] def remote_friendly_search(self): - return self.get_bool(SETTINGS.REMOTE_FRIENDLY_SEARCH, False) + return self.get_bool(settings.REMOTE_FRIENDLY_SEARCH, False) def hide_short_videos(self): - return self.get_bool(SETTINGS.HIDE_SHORT_VIDEOS, False) + return self.get_bool(settings.HIDE_SHORT_VIDEOS, False) def client_selection(self): - return self.get_int(SETTINGS.CLIENT_SELECTION, 0) + return self.get_int(settings.CLIENT_SELECTION, 0) def show_detailed_description(self): - return self.get_bool(SETTINGS.DETAILED_DESCRIPTION, True) + return self.get_bool(settings.DETAILED_DESCRIPTION, True) def get_language(self): - return self.get_string(SETTINGS.LANGUAGE, 'en_US').replace('_', '-') + return self.get_string(settings.LANGUAGE, 'en_US').replace('_', '-') def get_watch_later_playlist(self): - return self.get_string(SETTINGS.WATCH_LATER_PLAYLIST, '').strip() + return self.get_string(settings.WATCH_LATER_PLAYLIST, '').strip() def set_watch_later_playlist(self, value): - return self.set_string(SETTINGS.WATCH_LATER_PLAYLIST, value) + return self.set_string(settings.WATCH_LATER_PLAYLIST, value) def get_history_playlist(self): - return self.get_string(SETTINGS.HISTORY_PLAYLIST, '').strip() + return self.get_string(settings.HISTORY_PLAYLIST, '').strip() def set_history_playlist(self, value): - return self.set_string(SETTINGS.HISTORY_PLAYLIST, value) + return self.set_string(settings.HISTORY_PLAYLIST, value) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 5a4456ba8..6ac51e84b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -19,13 +19,13 @@ v3, ) from ...kodion import KodionException -from ...kodion.constants import content_type +from ...kodion.constants import content from ...kodion.items import DirectoryItem, UriItem from ...kodion.utils import strip_html_from_text def _process_related_videos(provider, context): - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) video_id = context.get_param('video_id', '') if not video_id: return [] @@ -42,7 +42,7 @@ def _process_related_videos(provider, context): def _process_parent_comments(provider, context): - provider.set_content_type(context, content_type.FILES) + provider.set_content_type(context, content.FILES) video_id = context.get_param('video_id', '') if not video_id: return [] @@ -56,7 +56,7 @@ def _process_parent_comments(provider, context): def _process_child_comments(provider, context): - provider.set_content_type(context, content_type.FILES) + provider.set_content_type(context, content.FILES) parent_id = context.get_param('parent_id', '') if not parent_id: return [] @@ -70,7 +70,7 @@ def _process_child_comments(provider, context): def _process_recommendations(provider, context): - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) json_data = provider.get_client(context).get_activities( channel_id='home', page_token=context.get_param('page_token', '') ) @@ -80,7 +80,7 @@ def _process_recommendations(provider, context): def _process_popular_right_now(provider, context): - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) json_data = provider.get_client(context).get_popular_videos( page_token=context.get_param('page_token', '') ) @@ -90,7 +90,7 @@ def _process_popular_right_now(provider, context): def _process_browse_channels(provider, context): - provider.set_content_type(context, content_type.FILES) + provider.set_content_type(context, content.FILES) client = provider.get_client(context) guide_id = context.get_param('guide_id', '') if guide_id: @@ -108,7 +108,7 @@ def _process_browse_channels(provider, context): def _process_disliked_videos(provider, context): - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) json_data = provider.get_client(context).get_disliked_videos( page_token=context.get_param('page_token', '') ) @@ -121,7 +121,7 @@ def _process_live_events(provider, context, event_type='live'): def _sort(x): return x.get_date() - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) # TODO: cache result json_data = provider.get_client(context).get_live_events( event_type=event_type, @@ -139,7 +139,7 @@ def _process_description_links(provider, context): addon_id = params.get('addon_id', '') def _extract_urls(video_id): - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) url_resolver = UrlResolver(context) progress_dialog = context.get_ui().create_progress_dialog( @@ -262,7 +262,7 @@ def _display_playlists(playlist_ids): def _process_saved_playlists_tv(provider, context): - provider.set_content_type(context, content_type.FILES) + provider.set_content_type(context, content.FILES) json_data = provider.get_client(context).get_saved_playlists( page_token=context.get_param('next_page_token', ''), offset=context.get_param('offset', 0) @@ -271,7 +271,7 @@ def _process_saved_playlists_tv(provider, context): def _process_new_uploaded_videos_tv(provider, context): - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) json_data = provider.get_client(context).get_my_subscriptions( page_token=context.get_param('next_page_token', ''), offset=context.get_param('offset', 0) @@ -280,7 +280,7 @@ def _process_new_uploaded_videos_tv(provider, context): def _process_new_uploaded_videos_tv_filtered(provider, context): - provider.set_content_type(context, content_type.VIDEOS) + provider.set_content_type(context, content.VIDEOS) json_data = provider.get_client(context).get_my_subscriptions( page_token=context.get_param('next_page_token', ''), offset=context.get_param('offset', 0) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index e13be4b0c..dab29cec9 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -33,8 +33,13 @@ yt_video, ) from .youtube_exceptions import InvalidGrant, LoginException -from ..kodion import (AbstractProvider, RegisterProviderPath, constants) +from ..kodion import AbstractProvider, RegisterProviderPath from ..kodion.compatibility import xbmcaddon, xbmcvfs +from ..kodion.constants import ( + content, + paths, + sort, +) from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem, menu_items from ..kodion.network import get_client_ip_address, is_httpd_live from ..kodion.utils import find_video_id, strip_html_from_text @@ -286,7 +291,7 @@ def on_uri2addon(self, context, re_match): @RegisterProviderPath('^(?:/channel/(?P[^/]+))?/playlist/(?P[^/]+)/$') def _on_playlist(self, context, re_match): - self.set_content_type(context, constants.content_type.VIDEOS) + self.set_content_type(context, content.VIDEOS) client = self.get_client(context) resource_manager = self.get_resource_manager(context) @@ -307,7 +312,7 @@ def _on_playlist(self, context, re_match): @RegisterProviderPath('^/channel/(?P[^/]+)/playlists/$') def _on_channel_playlists(self, context, re_match): - self.set_content_type(context, constants.content_type.FILES) + self.set_content_type(context, content.FILES) result = [] channel_id = re_match.group('channel_id') @@ -348,7 +353,7 @@ def _on_channel_playlists(self, context, re_match): @RegisterProviderPath('^/channel/(?P[^/]+)/live/$') def _on_channel_live(self, context, re_match): - self.set_content_type(context, constants.content_type.VIDEOS) + self.set_content_type(context, content.VIDEOS) result = [] channel_id = re_match.group('channel_id') @@ -391,7 +396,7 @@ def _on_channel(self, context, re_match): if method == 'channel' and not channel_id: return False - self.set_content_type(context, constants.content_type.VIDEOS) + self.set_content_type(context, content.VIDEOS) resource_manager = self.get_resource_manager(context) @@ -480,7 +485,7 @@ def _on_channel(self, context, re_match): # noinspection PyUnusedLocal @RegisterProviderPath('^/location/mine/$') def _on_my_location(self, context, re_match): - self.set_content_type(context, constants.content_type.FILES) + self.set_content_type(context, content.FILES) create_path = context.create_resource_path create_uri = context.create_uri @@ -635,7 +640,7 @@ def _on_subscriptions(self, context, re_match): subscriptions = yt_subscriptions.process(method, self, context) if method == 'list': - self.set_content_type(context, constants.content_type.FILES) + self.set_content_type(context, content.FILES) channel_ids = [] for subscription in subscriptions: channel_ids.append(subscription.get_channel_id()) @@ -818,9 +823,9 @@ def on_search(self, search_text, context, re_match): safe_search = context.get_settings().safe_search() if search_type == 'video': - self.set_content_type(context, constants.content_type.VIDEOS) + self.set_content_type(context, content.VIDEOS) else: - self.set_content_type(context, constants.content_type.FILES) + self.set_content_type(context, content.FILES) if page == 1 and search_type == 'video' and not event_type and not hide_folders: if not channel_id and not location: @@ -1228,7 +1233,7 @@ def on_root(self, context, re_match): _ = self.get_client(context) # required for self.is_logged_in() logged_in = self.is_logged_in() - self.set_content_type(context, constants.content_type.FILES) + self.set_content_type(context, content.FILES) result = [] @@ -1344,7 +1349,7 @@ def on_root(self, context, re_match): else: watch_history_item = DirectoryItem( localize('watch_later'), - create_uri([constants.paths.WATCH_LATER, 'list']), + create_uri([paths.WATCH_LATER, 'list']), image=create_path('media', 'watch_later.png'), fanart=self.get_fanart(context) ) @@ -1399,7 +1404,7 @@ def on_root(self, context, re_match): elif settings.use_local_history(): watch_history_item = DirectoryItem( localize('history'), - create_uri([constants.paths.HISTORY], params={'action': 'list'}), + create_uri([paths.HISTORY], params={'action': 'list'}), image=create_path('media', 'history.png'), fanart=self.get_fanart(context) ) @@ -1501,18 +1506,18 @@ def on_root(self, context, re_match): def set_content_type(context, content_type): context.set_content_type(content_type) context.add_sort_method( - (constants.sort_method.UNSORTED, '%T \u2022 %P', '%D | %J'), - (constants.sort_method.LABEL_IGNORE_THE, '%T \u2022 %P', '%D | %J'), + (sort.UNSORTED, '%T \u2022 %P', '%D | %J'), + (sort.LABEL_IGNORE_THE, '%T \u2022 %P', '%D | %J'), ) - if content_type != constants.content_type.VIDEOS: + if content_type != content.VIDEOS: return context.add_sort_method( - (constants.sort_method.PROGRAM_COUNT, '%T \u2022 %P | %D | %J', '%C'), - (constants.sort_method.VIDEO_RATING, '%T \u2022 %P | %D | %J', '%R'), - (constants.sort_method.DATE, '%T \u2022 %P | %D', '%J'), - (constants.sort_method.DATEADDED, '%T \u2022 %P | %D', '%a'), - (constants.sort_method.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'), - (constants.sort_method.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'), + (sort.PROGRAM_COUNT, '%T \u2022 %P | %D | %J', '%C'), + (sort.VIDEO_RATING, '%T \u2022 %P | %D | %J', '%R'), + (sort.DATE, '%T \u2022 %P | %D', '%J'), + (sort.DATEADDED, '%T \u2022 %P | %D', '%a'), + (sort.VIDEO_RUNTIME, '%T \u2022 %P | %J', '%D'), + (sort.TRACKNUM, '[%N. ]%T \u2022 %P', '%D | %J'), ) def handle_exception(self, context, exception_to_handle): From af1738e29519fb04abb32fa4e46e44fa59845c09 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 25 Dec 2023 12:07:04 +1100 Subject: [PATCH 128/141] Remove use of hardcoded temp and data path - make_dirs returns translated path on success - Use to check/create/get in one go - Rename native_path to addon_path - Use os.path.join rather than hardcoding path seperators - Fix unnecessarily nested os.path.join calls --- .../kodion/constants/__init__.py | 4 + .../kodion/context/abstract_context.py | 6 +- .../kodion/context/xbmc/xbmc_context.py | 22 +++-- .../kodion/json_store/json_store.py | 29 ++++--- .../kodion/network/http_server.py | 7 +- .../youtube_plugin/kodion/utils/methods.py | 39 +++++---- .../youtube_plugin/kodion/utils/monitor.py | 42 ++++++---- .../youtube/helper/subtitles.py | 81 +++++++++--------- .../youtube/helper/video_info.py | 39 +++++---- .../lib/youtube_plugin/youtube/provider.py | 82 ++++++++++++------- 10 files changed, 200 insertions(+), 151 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 819a9060e..e2ef4c566 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -19,9 +19,13 @@ ADDON_ID = 'plugin.video.youtube' +DATA_PATH = 'special://profile/addon_data/{id}'.format(id=ADDON_ID) +TEMP_PATH = 'special://temp/{id}'.format(id=ADDON_ID) __all__ = ( 'ADDON_ID', + 'DATA_PATH', + 'TEMP_PATH', 'content', 'paths', 'settings', diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 3a789dbd9..9cd7fda80 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -143,7 +143,7 @@ def get_cache_path(self): def get_playback_history(self): if not self._playback_history: uuid = self.get_access_manager().get_current_user_id() - db_file = os.path.join(os.path.join(self.get_data_path(), 'playback'), str(uuid)) + db_file = os.path.join(self.get_data_path(), 'playback', uuid) self._playback_history = PlaybackHistory(db_file) return self._playback_history @@ -286,7 +286,7 @@ def get_data_path(self): """ raise NotImplementedError() - def get_native_path(self): + def get_addon_path(self): raise NotImplementedError() def get_icon(self): @@ -299,7 +299,7 @@ def create_resource_path(self, *args): path_comps = [] for arg in args: path_comps.extend(arg.split('/')) - path = os.path.join(self.get_native_path(), 'resources', *path_comps) + path = os.path.join(self.get_addon_path(), 'resources', *path_comps) return path def get_uri(self): diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index d84843a6d..4c9e88aa8 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -31,7 +31,12 @@ from ...player.xbmc.xbmc_playlist import XbmcPlaylist from ...settings.xbmc.xbmc_plugin_settings import XbmcPluginSettings from ...ui.xbmc.xbmc_context_ui import XbmcContextUI -from ...utils import current_system_version, loose_version, to_unicode +from ...utils import ( + current_system_version, + loose_version, + make_dirs, + to_unicode, +) class XbmcContext(AbstractContext): @@ -290,17 +295,10 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override self._plugin_id = plugin_id or ADDON_ID self._plugin_name = plugin_name or self._addon.getAddonInfo('name') self._version = self._addon.getAddonInfo('version') - self._native_path = xbmcvfs.translatePath(self._addon.getAddonInfo('path')) + self._addon_path = make_dirs(self._addon.getAddonInfo('path')) + self._data_path = make_dirs(self._addon.getAddonInfo('profile')) self._settings = XbmcPluginSettings(self._addon) - """ - Set the data path for this addon and create the folder - """ - self._data_path = xbmcvfs.translatePath(self._addon.getAddonInfo('profile')) - - if not xbmcvfs.exists(self._data_path): - xbmcvfs.mkdir(self._data_path) - def get_region(self): pass # implement from abstract @@ -388,8 +386,8 @@ def get_debug_path(self): xbmcvfs.mkdir(self._debug_path) return self._debug_path - def get_native_path(self): - return self._native_path + def get_addon_path(self): + return self._addon_path def get_settings(self): return self._settings diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index ca6baad78..baede56d4 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -13,28 +13,21 @@ import os from io import open -from ..compatibility import xbmcaddon, xbmcvfs +from ..constants import DATA_PATH from ..logger import log_debug, log_error from ..utils import make_dirs, merge_dicts, to_unicode -_addon_id = 'plugin.video.youtube' -_addon = xbmcaddon.Addon(_addon_id) -_addon_data_path = _addon.getAddonInfo('profile') -del _addon - - class JSONStore(object): - def __init__(self, filename): - self.base_path = xbmcvfs.translatePath(_addon_data_path) + BASE_PATH = make_dirs(DATA_PATH) - if not xbmcvfs.exists(self.base_path) and not make_dirs(self.base_path): - log_error('JSONStore.__init__ - invalid path:\n|{path}|'.format( - path=self.base_path - )) - return + def __init__(self, filename): + if self.BASE_PATH: + self.filename = os.path.join(self.BASE_PATH, filename) + else: + log_error('JSONStore.__init__ - unable to access temp directory') + self.filename = None - self.filename = os.path.join(self.base_path, filename) self._data = {} self.load() self.set_defaults() @@ -43,6 +36,9 @@ def set_defaults(self, reset=False): raise NotImplementedError def save(self, data, update=False, process=None): + if not self.filename: + return + if update: data = merge_dicts(self._data, data) if data == self._data: @@ -75,6 +71,9 @@ def save(self, data, update=False, process=None): self.set_defaults(reset=True) def load(self, process=None): + if not self.filename: + return + log_debug('JSONStore.load - loading:\n|{filename}|'.format( filename=self.filename )) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index f665346da..e071c4818 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -26,6 +26,7 @@ xbmcgui, xbmcvfs, ) +from ..constants import TEMP_PATH from ..logger import log_debug from ..settings import Settings @@ -42,7 +43,7 @@ class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): - base_path = xbmcvfs.translatePath('special://temp/{0}'.format(_addon_id)) + BASE_PATH = xbmcvfs.translatePath(TEMP_PATH) chunk_size = 1024 * 64 local_ranges = ( '10.', @@ -98,7 +99,7 @@ def do_GET(self): self.wfile.write(client_json.encode('utf-8')) elif _settings.use_mpd_videos() and stripped_path.endswith('.mpd'): - file_path = os.path.join(self.base_path, + file_path = os.path.join(self.BASE_PATH, self.path.strip('/').strip('\\')) file_chunk = True log_debug('HTTPServer: Request file path |{file_path}|' @@ -200,7 +201,7 @@ def do_HEAD(self): if not self.connection_allowed(): self.send_error(403) elif _settings.use_mpd_videos() and self.path.endswith('.mpd'): - file_path = os.path.join(self.base_path, + file_path = os.path.join(self.BASE_PATH, self.path.strip('/').strip('\\')) if not os.path.isfile(file_path): response = ('File Not Found: |{proxy_path}| -> |{file_path}|' diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 2dbfc8ced..3528f25c8 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -18,6 +18,7 @@ from math import floor, log from ..compatibility import quote, xbmc, xbmcvfs +from ..logger import log_error __all__ = ( @@ -229,21 +230,31 @@ def print_items(items): def make_dirs(path): if not path.endswith('/'): - path = ''.join([path, '/']) + path = ''.join((path, '/')) + succeeded = xbmcvfs.exists(path) + + if succeeded: + return xbmcvfs.translatePath(path) + + try: + succeeded = xbmcvfs.mkdirs(path) + except OSError: + pass + + if succeeded: + return xbmcvfs.translatePath(path) + path = xbmcvfs.translatePath(path) - if not xbmcvfs.exists(path): - try: - _ = xbmcvfs.mkdirs(path) - except: - pass - if not xbmcvfs.exists(path): - try: - os.makedirs(path) - except: - pass - return xbmcvfs.exists(path) - - return True + try: + os.makedirs(path) + succeeded = True + except OSError: + pass + + if succeeded: + return path + log_error('Failed to create directory: |{0}|'.format(path)) + return False def find_video_id(plugin_path): diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index ecf05bb61..43439d719 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -15,6 +15,7 @@ import threading from ..compatibility import unquote, xbmc, xbmcaddon, xbmcvfs +from ..constants import TEMP_PATH from ..logger import log_debug from ..network import get_http_server, is_httpd_live from ..settings import Settings @@ -162,23 +163,30 @@ def restart_httpd(self): def ping_httpd(self): return is_httpd_live(port=self.httpd_port()) - def remove_temp_dir(self): - path = xbmcvfs.translatePath('special://temp/%s' % self._addon_id) + @staticmethod + def remove_temp_dir(): + temp_path = TEMP_PATH + succeeded = False - if os.path.isdir(path): + if xbmcvfs.exists(temp_path): try: - xbmcvfs.rmdir(path, force=True) - except: + succeeded = xbmcvfs.rmdir(temp_path, force=True) + except OSError: pass - if os.path.isdir(path): - try: - shutil.rmtree(path) - except: - pass - - if os.path.isdir(path): - log_debug('Failed to remove directory: {path}'.format( - path=path - )) - return False - return True + else: + succeeded = True + + if succeeded: + return True + + temp_path = xbmcvfs.translatePath(TEMP_PATH) + try: + shutil.rmtree(temp_path) + succeeded = not xbmcvfs.exists(temp_path) + except OSError: + pass + + if succeeded: + return True + log_debug('Failed to remove directory: {0}'.format(temp_path)) + return False diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 94a35d90f..5a011008b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -8,6 +8,8 @@ from __future__ import absolute_import, division, unicode_literals +import os + from ...kodion.compatibility import ( parse_qs, unescape, @@ -16,6 +18,7 @@ urlsplit, xbmcvfs, ) +from ...kodion.constants import TEMP_PATH from ...kodion.network import BaseRequestsClass from ...kodion.utils import make_dirs @@ -27,8 +30,7 @@ class Subtitles(object): LANG_CURR = 3 LANG_CURR_NO_ASR = 4 - BASE_PATH = 'special://temp/plugin.video.youtube/' - SRT_FILE = ''.join([BASE_PATH, '%s.%s.srt']) + BASE_PATH = make_dirs(TEMP_PATH) def __init__(self, context, video_id, captions, headers=None): self.video_id = video_id @@ -93,23 +95,6 @@ def __init__(self, context, video_id, captions, headers=None): 'is_asr': default_caption.get('kind') == 'asr', } - def srt_filename(self, sub_language): - return self.SRT_FILE % (self.video_id, sub_language) - - def _write_file(self, filepath, contents): - if not make_dirs(self.BASE_PATH): - self._context.log_debug('Failed to create directories: %s' % self.BASE_PATH) - return False - self._context.log_debug('Writing subtitle file: %s' % filepath) - - try: - with xbmcvfs.File(filepath, 'w') as srt_file: - success = srt_file.write(contents) - except (IOError, OSError): - self._context.log_debug('File write failed for: %s' % filepath) - return False - return success - def _unescape(self, text): try: text = unescape(text) @@ -201,10 +186,17 @@ def _prompt(self): return [] def _get(self, lang_code='en', language=None, no_asr=False, download=None): - filename = self.srt_filename(lang_code) - if xbmcvfs.exists(filename): - self._context.log_debug('Subtitle exists for: %s, filename: %s' % (lang_code, filename)) - return [filename] + filename = '.'.join((self.video_id, lang_code, 'srt')) + if not self.BASE_PATH: + self._context.log_error('Subtitles._get - ' + 'unable to access temp directory') + return [] + + filepath = os.path.join(self.BASE_PATH, filename) + if xbmcvfs.exists(filepath): + self._context.log_debug('Subtitle exists for |{lang}| - |{file}|' + .format(lang=lang_code, file=filepath)) + return [filepath] if download is None: download = self.pre_download @@ -257,25 +249,34 @@ def _get(self, lang_code='en', language=None, no_asr=False, download=None): ('tlang', lang_code) if has_translation else (None, None), ) - if subtitle_url: - self._context.log_debug('Subtitle url: %s' % subtitle_url) - if not download: - return [subtitle_url] - - response = BaseRequestsClass().request(subtitle_url, - headers=self.headers) - if response.text: - self._context.log_debug('Subtitle found for: %s' % lang_code) - self._write_file(filename, - bytearray(self._unescape(response.text), - encoding='utf8', - errors='ignore')) - return [filename] - - self._context.log_debug('Failed to retrieve subtitles for: %s' % lang_code) + if not subtitle_url: + self._context.log_debug('No subtitles found for: %s' % lang_code) + return [] + + if not download: + return [subtitle_url] + + response = BaseRequestsClass().request( + subtitle_url, + headers=self.headers, + error_info=('Failed to retrieve subtitles for: {lang}: {{exc}}' + .format(lang=lang_code)) + ) + if not response.text: return [] - self._context.log_debug('No subtitles found for: %s' % lang_code) + output = bytearray(self._unescape(response.text), + encoding='utf8', + errors='ignore') + try: + with xbmcvfs.File(filepath, 'w') as srt_file: + success = srt_file.write(output) + except (IOError, OSError): + self._context.log_error('Subtitles._get - ' + 'file write failed for: {file}' + .format(file=filepath)) + if success: + return [filepath] return [] @staticmethod diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index b494e20e0..94864a301 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -11,6 +11,7 @@ from __future__ import absolute_import, division, unicode_literals import json +import os import random import re from traceback import format_stack @@ -30,11 +31,14 @@ urlsplit, xbmcvfs, ) +from ...kodion.constants import TEMP_PATH, paths from ...kodion.network import is_httpd_live from ...kodion.utils import make_dirs class VideoInfo(YouTubeRequestClient): + BASE_PATH = make_dirs(TEMP_PATH) + FORMAT = { # === Non-DASH === '5': {'container': 'flv', @@ -1168,7 +1172,7 @@ def _get_video_info(self): 'default': ('https://i.ytimg.com/vi/{0}/default{1}.jpg' .format(self.video_id, is_live)), }, - 'subtitles': captions or [], + 'subtitles': captions, } if _settings.use_remote_history(): @@ -1609,10 +1613,9 @@ def _generate_mpd_manifest(self, video_data, audio_data, license_url): if not video_data or not audio_data: return None, None - basepath = 'special://temp/plugin.video.youtube/' - if not make_dirs(basepath): - self._context.log_debug('Failed to create temp directory: {0}' - .format(basepath)) + if not self.BASE_PATH: + self._context.log_error('VideoInfo._generate_mpd_manifest - ' + 'unable to access temp directory') return None, None def _filter_group(previous_group, previous_stream, item): @@ -1671,7 +1674,7 @@ def _filter_group(previous_group, previous_stream, item): 'multi_lang': False, } - out_list = [ + output = [ '\n' '", ">")) - out_list.extend(( + output.extend(( '\t\t\t\n' @@ -1778,7 +1781,7 @@ def _filter_group(previous_group, previous_stream, item): num_streams = len(streams) if media_type == 'audio': - out_list.extend((( + output.extend((( '\t\t\t\n') + output.append('\t\t\n') set_id += 1 - out_list.append('\t\n' - '\n') - out = ''.join(out_list) + output.append('\t\n' + '\n') + output = ''.join(output) if len(languages.difference({'', 'und'})) > 1: main_stream['multi_lang'] = True if roles.difference({'', 'main', 'dub'}): main_stream['multi_audio'] = True - filepath = '{0}{1}.mpd'.format(basepath, self.video_id) + filename = '.'.join((self.video_id, 'mpd')) + filepath = os.path.join(self.BASE_PATH, filename) try: with xbmcvfs.File(filepath, 'w') as mpd_file: - success = mpd_file.write(str(out)) + success = mpd_file.write(output) except (IOError, OSError): + self._context.log_error('VideoInfo._generate_mpd_manifest - ' + 'file write failed for: {file}' + .format(file=filepath)) success = False if success: return 'http://{0}:{1}/{2}.mpd'.format( diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index dab29cec9..8d6458c7f 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -36,6 +36,8 @@ from ..kodion import AbstractProvider, RegisterProviderPath from ..kodion.compatibility import xbmcaddon, xbmcvfs from ..kodion.constants import ( + DATA_PATH, + TEMP_PATH, content, paths, sort, @@ -1024,38 +1026,56 @@ def maintenance_actions(self, context, re_match): 'settings_xml': 'settings.xml', 'api_keys': 'api_keys.json', 'access_manager': 'access_manager.json', - 'temp_files': 'special://temp/plugin.video.youtube/'} - _file = _maint_files.get(maint_type, '') - success = False - if _file: - if 'sqlite' in _file: - _file_w_path = os.path.join(context.get_cache_path(), _file) - elif maint_type == 'temp_files': - _file_w_path = _file - elif _file == 'playback_history': - _file = ''.join([str(context.get_access_manager().get_current_user_id()), '.sqlite']) - _file_w_path = os.path.join(os.path.join(context.get_data_path(), 'playback'), _file) + 'temp_files': TEMP_PATH} + _file = _maint_files.get(maint_type) + succeeded = False + + if not _file: + return + + data_path = xbmcvfs.translatePath(DATA_PATH) + if 'sqlite' in _file: + _file_w_path = os.path.join(data_path, 'kodion', _file) + elif maint_type == 'temp_files': + _file_w_path = _file + elif maint_type == 'playback_history': + _file = ''.join(( + context.get_access_manager().get_current_user_id(), + '.sqlite' + )) + _file_w_path = os.path.join(data_path, 'playback', _file) + else: + _file_w_path = os.path.join(data_path, _file) + + if not ui.on_delete_content(_file): + return + + if maint_type == 'temp_files': + temp_path = _file_w_path + + if xbmcvfs.exists(temp_path): + try: + succeeded = xbmcvfs.rmdir(temp_path, force=True) + except OSError: + pass else: - _file_w_path = os.path.join(context.get_data_path(), _file) - if ui.on_delete_content(_file): - if maint_type == 'temp_files': - _trans_path = xbmcvfs.translatePath(_file_w_path) - try: - xbmcvfs.rmdir(_trans_path, force=True) - except: - pass - if xbmcvfs.exists(_trans_path): - try: - shutil.rmtree(_trans_path) - except: - pass - success = not xbmcvfs.exists(_trans_path) - elif _file_w_path: - success = xbmcvfs.delete(_file_w_path) - if success: - ui.show_notification(localize('succeeded')) - else: - ui.show_notification(localize('failed')) + succeeded = True + + if not succeeded: + temp_path = xbmcvfs.translatePath(_file_w_path) + try: + shutil.rmtree(temp_path) + succeeded = not xbmcvfs.exists(temp_path) + except OSError: + pass + + elif _file_w_path: + succeeded = xbmcvfs.delete(_file_w_path) + + if succeeded: + ui.show_notification(localize('succeeded')) + else: + ui.show_notification(localize('failed')) elif action == 'install' and maint_type == 'inputstreamhelper': if context.get_system_version().get_version()[0] >= 17: try: From 4307c02d34a3a9504538425d3f3c0ae4e5e7c7cc Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 25 Dec 2023 12:26:01 +1100 Subject: [PATCH 129/141] Remove use of hardcoded addon id and path --- .../youtube_plugin/kodion/constants/__init__.py | 2 ++ .../kodion/json_store/access_manager.py | 6 +++--- resources/lib/youtube_plugin/kodion/logger.py | 15 +++++++-------- .../youtube_plugin/kodion/network/http_server.py | 9 ++++----- .../youtube_plugin/kodion/network/requests.py | 3 ++- .../kodion/ui/xbmc/xbmc_context_ui.py | 13 ++++++++----- .../lib/youtube_plugin/kodion/utils/monitor.py | 16 ++++++---------- resources/lib/youtube_plugin/youtube/provider.py | 16 ++++++++-------- resources/lib/youtube_registration.py | 6 ++++-- 9 files changed, 44 insertions(+), 42 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index e2ef4c566..742c55fe8 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -19,11 +19,13 @@ ADDON_ID = 'plugin.video.youtube' +ADDON_PATH = 'special://home/addons/{id}'.format(id=ADDON_ID) DATA_PATH = 'special://profile/addon_data/{id}'.format(id=ADDON_ID) TEMP_PATH = 'special://temp/{id}'.format(id=ADDON_ID) __all__ = ( 'ADDON_ID', + 'ADDON_PATH', 'DATA_PATH', 'TEMP_PATH', 'content', diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index 82eb9febf..544a830b6 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -13,6 +13,7 @@ from hashlib import md5 from .json_store import JSONStore +from ..constants import ADDON_ID __author__ = 'bromix' @@ -34,8 +35,7 @@ def __init__(self, context): self._settings = context.get_settings() access_manager_data = self._data['access_manager'] self._user = access_manager_data.get('current_user', 0) - self._last_origin = access_manager_data.get('last_origin', - 'plugin.video.youtube') + self._last_origin = access_manager_data.get('last_origin', ADDON_ID) def set_defaults(self, reset=False): data = {} if reset else self.get_data() @@ -56,7 +56,7 @@ def set_defaults(self, reset=False): if 'current_user' not in data['access_manager']: data['access_manager']['current_user'] = 0 if 'last_origin' not in data['access_manager']: - data['access_manager']['last_origin'] = 'plugin.video.youtube' + data['access_manager']['last_origin'] = ADDON_ID if 'developers' not in data['access_manager']: data['access_manager']['developers'] = {} diff --git a/resources/lib/youtube_plugin/kodion/logger.py b/resources/lib/youtube_plugin/kodion/logger.py index 0940faf42..03cde49ab 100644 --- a/resources/lib/youtube_plugin/kodion/logger.py +++ b/resources/lib/youtube_plugin/kodion/logger.py @@ -11,6 +11,7 @@ from __future__ import absolute_import, division, unicode_literals from .compatibility import xbmc, xbmcaddon +from .constants import ADDON_ID DEBUG = xbmc.LOGDEBUG @@ -22,31 +23,29 @@ SEVERE = FATAL NONE = xbmc.LOGNONE -_ADDON_ID = 'plugin.video.youtube' - -def log(text, log_level=DEBUG, addon_id=_ADDON_ID): +def log(text, log_level=DEBUG, addon_id=ADDON_ID): if not addon_id: addon_id = xbmcaddon.Addon().getAddonInfo('id') log_line = '[%s] %s' % (addon_id, text) xbmc.log(msg=log_line, level=log_level) -def log_debug(text, addon_id=_ADDON_ID): +def log_debug(text, addon_id=ADDON_ID): log(text, DEBUG, addon_id) -def log_info(text, addon_id=_ADDON_ID): +def log_info(text, addon_id=ADDON_ID): log(text, INFO, addon_id) -def log_notice(text, addon_id=_ADDON_ID): +def log_notice(text, addon_id=ADDON_ID): log(text, NOTICE, addon_id) -def log_warning(text, addon_id=_ADDON_ID): +def log_warning(text, addon_id=ADDON_ID): log(text, WARNING, addon_id) -def log_error(text, addon_id=_ADDON_ID): +def log_error(text, addon_id=ADDON_ID): log(text, ERROR, addon_id) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index e071c4818..400b71177 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -26,13 +26,12 @@ xbmcgui, xbmcvfs, ) -from ..constants import TEMP_PATH +from ..constants import ADDON_ID, TEMP_PATH from ..logger import log_debug from ..settings import Settings -_addon_id = 'plugin.video.youtube' -_addon = xbmcaddon.Addon(_addon_id) +_addon = xbmcaddon.Addon(ADDON_ID) _settings = Settings(_addon) _i18n = _addon.getLocalizedString _addon_name = _addon.getAddonInfo('name') @@ -226,12 +225,12 @@ def do_POST(self): elif self.path.startswith('/widevine'): home = xbmcgui.Window(10000) - lic_url = home.getProperty('plugin.video.youtube-license_url') + lic_url = home.getProperty('-'.join((ADDON_ID, 'license_url'))) if not lic_url: self.send_error(404) return - lic_token = home.getProperty('plugin.video.youtube-license_token') + lic_token = home.getProperty('-'.join((ADDON_ID, 'license_token'))) if not lic_token: self.send_error(403) return diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index c5585b664..2d4c911d1 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -17,11 +17,12 @@ from requests.exceptions import InvalidJSONError, RequestException from ..compatibility import xbmcaddon +from ..constants import ADDON_ID from ..logger import log_error from ..settings import Settings -_settings = Settings(xbmcaddon.Addon(id='plugin.video.youtube')) +_settings = Settings(xbmcaddon.Addon(id=ADDON_ID)) class BaseRequestsClass(object): diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index da63599a7..fddb052aa 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -14,6 +14,7 @@ from ..abstract_context_ui import AbstractContextUI from ... import utils from ...compatibility import xbmc, xbmcgui +from ...constants import ADDON_ID, ADDON_PATH class XbmcContextUI(AbstractContextUI): @@ -136,8 +137,10 @@ def open_settings(self): self._xbmc_addon.openSettings() def refresh_container(self): - script_uri = "{}/resources/lib/youtube_plugin/refresh.py".format(self._xbmc_addon.getAddonInfo('path')) - xbmc.executebuiltin('RunScript(%s)' % script_uri) + xbmc.executebuiltin( + 'RunScript({path}/resources/lib/youtube_plugin/refresh.py)' + .format(path=ADDON_PATH) + ) @staticmethod def get_info_label(value): @@ -145,17 +148,17 @@ def get_info_label(value): @staticmethod def set_property(property_id, value): - property_id = ''.join(['plugin.video.youtube-', property_id]) + property_id = '-'.join((ADDON_ID, property_id)) xbmcgui.Window(10000).setProperty(property_id, value) @staticmethod def get_property(property_id): - property_id = ''.join(['plugin.video.youtube-', property_id]) + property_id = '-'.join((ADDON_ID, property_id)) return xbmcgui.Window(10000).getProperty(property_id) @staticmethod def clear_property(property_id): - property_id = ''.join(['plugin.video.youtube-', property_id]) + property_id = '-'.join((ADDON_ID, property_id)) xbmcgui.Window(10000).clearProperty(property_id) @staticmethod diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/monitor.py index 43439d719..fc1495d49 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/monitor.py @@ -15,15 +15,14 @@ import threading from ..compatibility import unquote, xbmc, xbmcaddon, xbmcvfs -from ..constants import TEMP_PATH +from ..constants import ADDON_ID, TEMP_PATH from ..logger import log_debug from ..network import get_http_server, is_httpd_live from ..settings import Settings class YouTubeMonitor(xbmc.Monitor): - _addon_id = 'plugin.video.youtube' - _settings = Settings(xbmcaddon.Addon(_addon_id)) + _settings = Settings(xbmcaddon.Addon(ADDON_ID)) # noinspection PyUnusedLocal,PyMissingConstructor def __init__(self, *args, **kwargs): @@ -40,8 +39,7 @@ def __init__(self, *args, **kwargs): super(YouTubeMonitor, self).__init__() def onNotification(self, sender, method, data): - if (sender == 'plugin.video.youtube' - and method.endswith('.check_settings')): + if sender == ADDON_ID and method.endswith('.check_settings'): if not isinstance(data, dict): data = json.loads(data) data = json.loads(unquote(data[0])) @@ -82,12 +80,12 @@ def onNotification(self, sender, method, data): else: self.start_httpd() - elif sender == 'plugin.video.youtube': + elif sender == ADDON_ID: log_debug('onNotification: |unhandled method| -> |{method}|' .format(method=method)) def onSettingsChanged(self): - self._settings.flush(xbmcaddon.Addon(self._addon_id)) + self._settings.flush(xbmcaddon.Addon(ADDON_ID)) data = { 'use_httpd': (self._settings.use_mpd_videos() @@ -96,9 +94,7 @@ def onSettingsChanged(self): 'whitelist': self._settings.httpd_whitelist(), 'httpd_address': self._settings.httpd_listen() } - self.onNotification('plugin.video.youtube', - 'Other.check_settings', - data) + self.onNotification(ADDON_ID, 'Other.check_settings', data) def use_httpd(self): return self._use_httpd diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 8d6458c7f..5c230b422 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -36,6 +36,7 @@ from ..kodion import AbstractProvider, RegisterProviderPath from ..kodion.compatibility import xbmcaddon, xbmcvfs from ..kodion.constants import ( + ADDON_ID, DATA_PATH, TEMP_PATH, content, @@ -135,14 +136,13 @@ def get_client(self, context): refresh_tokens = [] if dev_id: - dev_origin = dev_config.get('origin') if dev_config.get('origin') else dev_id - if api_last_origin != dev_origin: - context.log_debug('API key origin changed, clearing cache. |%s|' % dev_origin) - access_manager.set_last_origin(dev_origin) - self.get_resource_manager(context).clear() - elif api_last_origin != 'plugin.video.youtube': - context.log_debug('API key origin changed, clearing cache. |plugin.video.youtube|') - access_manager.set_last_origin('plugin.video.youtube') + origin = dev_config.get('origin') if dev_config.get('origin') else dev_id + else: + origin = ADDON_ID + + if api_last_origin != origin: + context.log_debug('API key origin changed, clearing cache. |%s|' % origin) + access_manager.set_last_origin(origin) self.get_resource_manager(context).clear() if dev_id: diff --git a/resources/lib/youtube_registration.py b/resources/lib/youtube_registration.py index a5585082d..4a8dbb7fc 100644 --- a/resources/lib/youtube_registration.py +++ b/resources/lib/youtube_registration.py @@ -10,8 +10,10 @@ from __future__ import absolute_import, division, unicode_literals from base64 import b64encode -from youtube_plugin.kodion.json_store import APIKeyStore + +from youtube_plugin.kodion.constants import ADDON_ID from youtube_plugin.kodion.context import Context +from youtube_plugin.kodion.json_store import APIKeyStore def register_api_keys(addon_id, api_key, client_id, client_secret): @@ -44,7 +46,7 @@ def register_api_keys(addon_id, api_key, client_id, client_secret): context = Context() - if not addon_id or addon_id == 'plugin.video.youtube': + if not addon_id or addon_id == ADDON_ID: context.log_error('Register API Keys: |%s| Invalid addon_id' % addon_id) return From 82b9525921507f059db57584a530adfe600324d6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:03:52 +1100 Subject: [PATCH 130/141] Update item creation - Add resource path constant - Removes multiple function calls to set default fanart - Simplify parameter creation/updates - Use plugin path constants rather than hardcoded paths - Use fanart parameter in *Item.__init__ - Use action parameter in DirectoryItem.__init__ - Add new location icon for My Location directory item - Add new icons with consistent sizing for the various search functions --- .../kodion/abstract_provider.py | 11 +- .../kodion/constants/__init__.py | 4 + .../youtube_plugin/kodion/items/base_item.py | 21 +- .../kodion/items/favorites_item.py | 16 +- .../kodion/items/new_search_item.py | 34 +- .../kodion/items/next_page_item.py | 14 +- .../kodion/items/search_history_item.py | 14 +- .../kodion/items/search_item.py | 26 +- .../kodion/items/watch_later_item.py | 16 +- .../youtube_plugin/youtube/client/youtube.py | 4 +- .../lib/youtube_plugin/youtube/helper/tv.py | 61 ++-- .../youtube/helper/url_to_item_converter.py | 8 +- .../youtube_plugin/youtube/helper/utils.py | 4 +- .../lib/youtube_plugin/youtube/helper/v3.py | 19 +- .../youtube/helper/yt_playlist.py | 9 +- .../youtube/helper/yt_specials.py | 2 - .../lib/youtube_plugin/youtube/provider.py | 320 +++++++++--------- resources/media/incognito_search.png | Bin 0 -> 4725 bytes resources/media/location.png | Bin 0 -> 5526 bytes resources/media/new_search.png | Bin 4673 -> 4394 bytes resources/media/quick_search.png | Bin 0 -> 4453 bytes resources/media/search.png | Bin 4282 -> 4246 bytes 22 files changed, 305 insertions(+), 278 deletions(-) create mode 100644 resources/media/incognito_search.png create mode 100644 resources/media/location.png create mode 100644 resources/media/quick_search.png diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index f68cab2b0..e8983dd8f 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -76,9 +76,6 @@ def __init__(self): if path: self.register_path(path, method_name) - def get_alternative_fanart(self, context): - return context.get_fanart() - def register_path(self, re_path, method_name): """ Registers a new method by name (string) for the given regular expression @@ -330,7 +327,9 @@ def _internal_search(self, context, re_match): location = context.get_param('location', False) # 'New Search...' - new_search_item = NewSearchItem(context, fanart=self.get_alternative_fanart(context), location=location) + new_search_item = NewSearchItem( + context, location=location + ) result.append(new_search_item) for search in search_history.get_items(): @@ -339,7 +338,9 @@ def _internal_search(self, context, re_match): search = search.get_name() # we create a new instance of the SearchItem - search_history_item = SearchHistoryItem(context, search, fanart=self.get_alternative_fanart(context), location=location) + search_history_item = SearchHistoryItem( + context, search, location=location + ) result.append(search_history_item) if search_history.is_empty(): diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 742c55fe8..d392adc67 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -21,12 +21,16 @@ ADDON_ID = 'plugin.video.youtube' ADDON_PATH = 'special://home/addons/{id}'.format(id=ADDON_ID) DATA_PATH = 'special://profile/addon_data/{id}'.format(id=ADDON_ID) +MEDIA_PATH = ADDON_PATH + '/resources/media' +RESOURCE_PATH = ADDON_PATH + '/resources' TEMP_PATH = 'special://temp/{id}'.format(id=ADDON_ID) __all__ = ( 'ADDON_ID', 'ADDON_PATH', 'DATA_PATH', + 'MEDIA_PATH', + 'RESOURCE_PATH', 'TEMP_PATH', 'content', 'paths', diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 9e71bb88a..85c46ec3b 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -15,6 +15,7 @@ from hashlib import md5 from ..compatibility import unescape +from ..constants import MEDIA_PATH class BaseItem(object): @@ -33,10 +34,11 @@ def __init__(self, name, uri, image='', fanart=''): self._uri = uri - self._image = '' + self._image = None self.set_image(image) + self._fanart = None + self.set_fanart(fanart) - self._fanart = fanart self._context_menu = None self._replace_context_menu = False self._added_utc = None @@ -96,8 +98,12 @@ def get_uri(self): return self._uri def set_image(self, image): - if image is None: + if not image: self._image = '' + return + + if '{media}/' in image: + self._image = image.format(media=MEDIA_PATH) else: self._image = image @@ -105,7 +111,14 @@ def get_image(self): return self._image def set_fanart(self, fanart): - self._fanart = fanart + if not fanart: + self._fanart = '{0}/fanart.jpg'.format(MEDIA_PATH) + return + + if '{media}/' in fanart: + self._fanart = fanart.format(media=MEDIA_PATH) + else: + self._fanart = fanart def get_fanart(self): return self._fanart diff --git a/resources/lib/youtube_plugin/kodion/items/favorites_item.py b/resources/lib/youtube_plugin/kodion/items/favorites_item.py index 8e25552bb..580c343ca 100644 --- a/resources/lib/youtube_plugin/kodion/items/favorites_item.py +++ b/resources/lib/youtube_plugin/kodion/items/favorites_item.py @@ -11,20 +11,22 @@ from __future__ import absolute_import, division, unicode_literals from .directory_item import DirectoryItem -from .. import constants +from ..constants import paths class FavoritesItem(DirectoryItem): - def __init__(self, context, alt_name=None, image=None, fanart=None): - name = alt_name + def __init__(self, context, name=None, image=None, fanart=None): if not name: name = context.localize('favorites') if image is None: - image = context.create_resource_path('media/favorites.png') + image = '{media}/favorites.png' + + super(FavoritesItem, self).__init__(name, + context.create_uri( + [paths.FAVORITES, 'list'] + ), + image=image) - super(FavoritesItem, self).__init__(name, context.create_uri([constants.paths.FAVORITES, 'list']), image=image) if fanart: self.set_fanart(fanart) - else: - self.set_fanart(context.get_fanart()) diff --git a/resources/lib/youtube_plugin/kodion/items/new_search_item.py b/resources/lib/youtube_plugin/kodion/items/new_search_item.py index 18f79c97e..73a052e98 100644 --- a/resources/lib/youtube_plugin/kodion/items/new_search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/new_search_item.py @@ -11,30 +11,40 @@ from __future__ import absolute_import, division, unicode_literals from .directory_item import DirectoryItem -from .. import constants +from ..constants import paths class NewSearchItem(DirectoryItem): - def __init__(self, context, alt_name=None, image=None, fanart=None, incognito=False, channel_id='', addon_id='', location=False): - name = alt_name + def __init__(self, + context, + name=None, + image=None, + fanart=None, + incognito=False, + channel_id='', + addon_id='', + location=False): if not name: name = context.get_ui().bold(context.localize('search.new')) if image is None: - image = context.create_resource_path('media/new_search.png') + image = '{media}/new_search.png' - item_params = {} + params = {} if addon_id: - item_params.update({'addon_id': addon_id}) + params['addon_id'] = addon_id if incognito: - item_params.update({'incognito': incognito}) + params['incognito'] = incognito if channel_id: - item_params.update({'channel_id': channel_id}) + params['channel_id'] = channel_id if location: - item_params.update({'location': location}) + params['location'] = location + + super(NewSearchItem, self).__init__(name, + context.create_uri( + [paths.SEARCH, 'input'], + params=params + ), image=image) - super(NewSearchItem, self).__init__(name, context.create_uri([constants.paths.SEARCH, 'input'], params=item_params), image=image) if fanart: self.set_fanart(fanart) - else: - self.set_fanart(context.get_fanart()) diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index ee266e52b..a73acb1c9 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -15,17 +15,19 @@ class NextPageItem(DirectoryItem): def __init__(self, context, current_page=1, image=None, fanart=None): - new_params = {} - new_params.update(context.get_params()) - new_params['page'] = current_page + 1 + new_params = dict(context.get_params(), page=(current_page + 1)) name = context.localize('next_page', 'Next Page') if name.find('%d') != -1: name %= current_page + 1 - super(NextPageItem, self).__init__(name, context.create_uri(context.get_path(), new_params), image=image) + super(NextPageItem, self).__init__(name, + context.create_uri( + context.get_path(), + new_params + ), + image=image) + if fanart: self.set_fanart(fanart) - else: - self.set_fanart(context.get_fanart()) self.next_page = True diff --git a/resources/lib/youtube_plugin/kodion/items/search_history_item.py b/resources/lib/youtube_plugin/kodion/items/search_history_item.py index b2aeab883..afd349683 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_history_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_history_item.py @@ -12,23 +12,27 @@ from . import menu_items from .directory_item import DirectoryItem -from ..constants.const_paths import SEARCH +from ..constants import paths class SearchHistoryItem(DirectoryItem): def __init__(self, context, query, image=None, fanart=None, location=False): if image is None: - image = context.create_resource_path('media/search.png') + image = '{media}/search.png' params = {'q': query} if location: params['location'] = location - super(SearchHistoryItem, self).__init__(query, context.create_uri([SEARCH, 'query'], params=params), image=image) + super(SearchHistoryItem, self).__init__(query, + context.create_uri( + [paths.SEARCH, 'query'], + params=params + ), + image=image) + if fanart: self.set_fanart(fanart) - else: - self.set_fanart(context.get_fanart()) context_menu = [ menu_items.search_remove(context, query), diff --git a/resources/lib/youtube_plugin/kodion/items/search_item.py b/resources/lib/youtube_plugin/kodion/items/search_item.py index 2ff9bbd1f..e505b67be 100644 --- a/resources/lib/youtube_plugin/kodion/items/search_item.py +++ b/resources/lib/youtube_plugin/kodion/items/search_item.py @@ -11,22 +11,32 @@ from __future__ import absolute_import, division, unicode_literals from .directory_item import DirectoryItem -from ..constants.const_paths import SEARCH +from ..constants import paths class SearchItem(DirectoryItem): - def __init__(self, context, alt_name=None, image=None, fanart=None, location=False): - name = alt_name + def __init__(self, + context, + name=None, + image=None, + fanart=None, + location=False): if not name: name = context.localize('search') if image is None: - image = context.create_resource_path('media/search.png') + image = '{media}/search.png' - params = {'location': location} if location else {} + params = {} + if location: + params['location'] = location + + super(SearchItem, self).__init__(name, + context.create_uri( + [paths.SEARCH, 'list'], + params=params + ), + image=image) - super(SearchItem, self).__init__(name, context.create_uri([SEARCH, 'list'], params=params), image=image) if fanart: self.set_fanart(fanart) - else: - self.set_fanart(context.get_fanart()) diff --git a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py index 01cfc0455..6ae79116e 100644 --- a/resources/lib/youtube_plugin/kodion/items/watch_later_item.py +++ b/resources/lib/youtube_plugin/kodion/items/watch_later_item.py @@ -11,20 +11,22 @@ from __future__ import absolute_import, division, unicode_literals from .directory_item import DirectoryItem -from .. import constants +from ..constants import paths class WatchLaterItem(DirectoryItem): - def __init__(self, context, alt_name=None, image=None, fanart=None): - name = alt_name + def __init__(self, context, name=None, image=None, fanart=None): if not name: name = context.localize('watch_later') if image is None: - image = context.create_resource_path('media/watch_later.png') + image = '{media}/watch_later.png' + + super(WatchLaterItem, self).__init__(name, + context.create_uri( + [paths.WATCH_LATER, 'list'] + ), + image=image) - super(WatchLaterItem, self).__init__(name, context.create_uri([constants.paths.WATCH_LATER, 'list']), image=image) if fanart: self.set_fanart(fanart) - else: - self.set_fanart(context.get_fanart()) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 5b6d7df45..c7e684622 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -604,9 +604,9 @@ def get_channel_by_username(self, username, **kwargs): """ params = {'part': 'id'} if username == 'mine': - params.update({'mine': 'true'}) + params['mine'] = True else: - params.update({'forUsername': username}) + params['forUsername'] = username return self.perform_v3_request(method='GET', path='channels', diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index 716a151b1..cd30681f5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -18,8 +18,6 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): result = [] video_id_dict = {} - incognito = context.get_param('incognito', False) - filter_list = [] black_list = False if do_filter: @@ -29,6 +27,11 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): filter_list = filter_list.split(',') filter_list = [x.lower() for x in filter_list] + item_params = {'video_id': None} + incognito = context.get_param('incognito', False) + if incognito: + item_params['incognito'] = incognito + items = json_data.get('items', []) for item in items: channel = item['channel'].lower() @@ -36,9 +39,7 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): if not do_filter or (do_filter and (not black_list) and (channel in filter_list)) or \ (do_filter and black_list and (channel not in filter_list)): video_id = item['id'] - item_params = {'video_id': video_id} - if incognito: - item_params.update({'incognito': incognito}) + item_params['video_id'] = video_id item_uri = context.create_uri(['play'], item_params) video_item = VideoItem(item['title'], uri=item_uri) if incognito: @@ -59,15 +60,12 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): # next page next_page_token = json_data.get('next_page_token', '') if next_page_token or json_data.get('continue', False): - new_params = {} - new_params.update(context.get_params()) - new_params['next_page_token'] = next_page_token - new_params['offset'] = int(json_data.get('offset', 0)) - + new_params = dict(context.get_params(), + next_page_token=next_page_token, + offset=int(json_data.get('offset', 0))) new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page) result.append(next_page_item) return result @@ -77,14 +75,15 @@ def tv_videos_to_items(provider, context, json_data): result = [] video_id_dict = {} + item_params = {'video_id': None} incognito = context.get_param('incognito', False) + if incognito: + item_params['incognito'] = incognito items = json_data.get('items', []) for item in items: video_id = item['id'] - item_params = {'video_id': video_id} - if incognito: - item_params.update({'incognito': incognito}) + item_params['video_id'] = video_id item_uri = context.create_uri(['play'], item_params) video_item = VideoItem(item['title'], uri=item_uri) if incognito: @@ -106,15 +105,12 @@ def tv_videos_to_items(provider, context, json_data): # next page next_page_token = json_data.get('next_page_token', '') if next_page_token or json_data.get('continue', False): - new_params = {} - new_params.update(context.get_params()) - new_params['next_page_token'] = next_page_token - new_params['offset'] = int(json_data.get('offset', 0)) - + new_params = dict(context.get_params(), + next_page_token=next_page_token, + offset=int(json_data.get('offset', 0))) new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page) result.append(next_page_item) return result @@ -124,8 +120,11 @@ def saved_playlists_to_items(provider, context, json_data): result = [] playlist_id_dict = {} - incognito = context.get_param('incognito', False) thumb_size = context.get_settings().use_thumbnail_size() + incognito = context.get_param('incognito', False) + item_params = {} + if incognito: + item_params['incognito'] = incognito items = json_data.get('items', []) for item in items: @@ -134,17 +133,12 @@ def saved_playlists_to_items(provider, context, json_data): playlist_id = item['id'] image = utils.get_thumbnail(thumb_size, item.get('thumbnails', {})) - item_params = {} - if incognito: - item_params.update({'incognito': incognito}) - if channel_id: item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) else: item_uri = context.create_uri(['playlist', playlist_id], item_params) playlist_item = DirectoryItem(title, item_uri, image=image) - playlist_item.set_fanart(provider.get_fanart(context)) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_item @@ -155,15 +149,12 @@ def saved_playlists_to_items(provider, context, json_data): # next page next_page_token = json_data.get('next_page_token', '') if next_page_token or json_data.get('continue', False): - new_params = {} - new_params.update(context.get_params()) - new_params['next_page_token'] = next_page_token - new_params['offset'] = int(json_data.get('offset', 0)) - + new_params = dict(context.get_params(), + next_page_token=next_page_token, + offset=int(json_data.get('offset', 0))) new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page) result.append(next_page_item) return result diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index bfc96ee8e..90ddfa73e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -99,7 +99,6 @@ def add_url(self, url, provider, context): playlist_item = DirectoryItem( '', context.create_uri(['playlist', playlist_id], new_params), ) - playlist_item.set_fanart(provider.get_fanart(context)) self._playlist_id_dict[playlist_id] = playlist_item elif 'channel_id' in new_params: @@ -115,7 +114,6 @@ def add_url(self, url, provider, context): ) if live else DirectoryItem( '', context.create_uri(['channel', channel_id], new_params) ) - channel_item.set_fanart(provider.get_fanart(context)) self._channel_id_dict[channel_id] = channel_item else: @@ -137,9 +135,8 @@ def get_items(self, provider, context, skip_title=False): context.create_uri(['special', 'description_links'], { 'channel_ids': ','.join(self._channel_ids), }), - context.create_resource_path('media', 'playlist.png') + image='{media}/playlist.png' ) - channels_item.set_fanart(provider.get_fanart(context)) result.append(channels_item) if self._playlist_ids: @@ -157,9 +154,8 @@ def get_items(self, provider, context, skip_title=False): context.create_uri(['special', 'description_links'], { 'playlist_ids': ','.join(self._playlist_ids), }), - context.create_resource_path('media', 'playlist.png') + image='{media}/playlist.png' ) - playlists_item.set_fanart(provider.get_fanart(context)) result.append(playlists_item) if self._channel_id_dict: diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 311d3719b..3ff8505c7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -239,10 +239,8 @@ def update_channel_infos(provider, context, channel_id_dict, for banner in banners: fanart = fanart_images.get(banner) if fanart: + channel_item.set_fanart(fanart) break - else: - fanart = '' - channel_item.set_fanart(fanart) # update channel mapping if channel_items_dict is not None: diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 345a85ca7..0cc753aea 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -67,7 +67,6 @@ def _process_list_response(provider, context, json_data): video_item.video_id = video_id if incognito: video_item.set_play_count(0) - video_item.set_fanart(provider.get_fanart(context)) result.append(video_item) video_id_dict[video_id] = video_item elif kind == 'channel': @@ -82,7 +81,6 @@ def _process_list_response(provider, context, json_data): item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) channel_item = DirectoryItem(title, item_uri, image=image) - channel_item.set_fanart(provider.get_fanart(context)) # if logged in => provide subscribing to the channel if provider.is_logged_in(): @@ -105,7 +103,6 @@ def _process_list_response(provider, context, json_data): item_params['addon_id'] = addon_id item_uri = context.create_uri(['special', 'browse_channels'], item_params) guide_item = DirectoryItem(title, item_uri) - guide_item.set_fanart(provider.get_fanart(context)) result.append(guide_item) elif kind == 'subscription': snippet = yt_item['snippet'] @@ -119,7 +116,6 @@ def _process_list_response(provider, context, json_data): item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) channel_item = DirectoryItem(title, item_uri, image=image) - channel_item.set_fanart(provider.get_fanart(context)) channel_item.set_channel_id(channel_id) # map channel id with subscription id - we need it for the unsubscription subscription_id_dict[channel_id] = yt_item['id'] @@ -144,7 +140,6 @@ def _process_list_response(provider, context, json_data): item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) playlist_item = DirectoryItem(title, item_uri, image=image) - playlist_item.set_fanart(provider.get_fanart(context)) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_item elif kind == 'playlistitem': @@ -166,7 +161,6 @@ def _process_list_response(provider, context, json_data): video_item.video_id = video_id if incognito: video_item.set_play_count(0) - video_item.set_fanart(provider.get_fanart(context)) # Get Track-ID from Playlist video_item.set_track_number(snippet['position'] + 1) result.append(video_item) @@ -197,7 +191,6 @@ def _process_list_response(provider, context, json_data): video_item.video_id = video_id if incognito: video_item.set_play_count(0) - video_item.set_fanart(provider.get_fanart(context)) result.append(video_item) video_id_dict[video_id] = video_item @@ -234,7 +227,6 @@ def _process_list_response(provider, context, json_data): video_item.video_id = video_id if incognito: video_item.set_play_count(0) - video_item.set_fanart(provider.get_fanart(context)) result.append(video_item) video_id_dict[video_id] = video_item # playlist @@ -256,7 +248,6 @@ def _process_list_response(provider, context, json_data): item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) playlist_item = DirectoryItem(title, item_uri, image=image) - playlist_item.set_fanart(provider.get_fanart(context)) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_item elif kind == 'channel': @@ -271,7 +262,6 @@ def _process_list_response(provider, context, json_data): item_params['addon_id'] = addon_id item_uri = context.create_uri(['channel', channel_id], item_params) channel_item = DirectoryItem(title, item_uri, image=image) - channel_item.set_fanart(provider.get_fanart(context)) result.append(channel_item) channel_id_dict[channel_id] = channel_item else: @@ -462,14 +452,11 @@ def response_to_items(provider, context, json_data, sort=None, reverse=False, pr client = provider.get_client(context) yt_next_page_token = client.calculate_next_page_token(page + 1, yt_results_per_page) - new_params = {} - new_params.update(context.get_params()) - new_params['page_token'] = yt_next_page_token - + new_params = dict(context.get_params(), + page_token=yt_next_page_token) new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page, fanart=provider.get_fanart(new_context)) + next_page_item = NextPageItem(new_context, current_page) result.append(next_page_item) return result diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 936d859d8..6939c6013 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -207,18 +207,15 @@ def _process_select_playlist(provider, context): playlist_id = json_data.get('id', '') if playlist_id: - new_params = {} - new_params.update(context.get_params()) - new_params['playlist_id'] = playlist_id + new_params = dict(context.get_params(), + playlist_id=playlist_id) new_context = context.clone(new_params=new_params) _process_add_video(provider, new_context, keymap_action) break if result == 'playlist.next': continue if result != -1: - new_params = {} - new_params.update(context.get_params()) - new_params['playlist_id'] = result + new_params = dict(context.get_params(), playlist_id=result) new_context = context.clone(new_params=new_params) _process_add_video(provider, new_context, keymap_action) break diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 6ac51e84b..544c70120 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -204,7 +204,6 @@ def _display_channels(channel_ids): channel_item = DirectoryItem( '', context.create_uri(['channel', channel_id], item_params) ) - channel_item.set_fanart(provider.get_fanart(context)) channel_id_dict[channel_id] = channel_item channel_item_dict = {} @@ -230,7 +229,6 @@ def _display_playlists(playlist_ids): playlist_item = DirectoryItem( '', context.create_uri(['playlist', playlist_id], item_params) ) - playlist_item.set_fanart(provider.get_fanart(context)) playlist_id_dict[playlist_id] = playlist_item channel_item_dict = {} diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 5c230b422..70f694ca4 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -258,13 +258,6 @@ def get_resource_manager(self, context): self._resource_manager = ResourceManager(context, self.get_client(context)) return self._resource_manager - def get_alternative_fanart(self, context): - return self.get_fanart(context) - - @staticmethod - def get_fanart(context): - return context.create_resource_path('media', 'fanart.jpg') - # noinspection PyUnusedLocal @RegisterProviderPath('^/uri2addon/$') def on_uri2addon(self, context, re_match): @@ -318,25 +311,31 @@ def _on_channel_playlists(self, context, re_match): result = [] channel_id = re_match.group('channel_id') - page_token = context.get_param('page_token', '') resource_manager = self.get_resource_manager(context) - item_params = {} - incognito = context.get_param('incognito') - addon_id = context.get_param('addon_id') + params = context.get_params() + page_token = params.get('page_token', '') + incognito = params.get('incognito') + addon_id = params.get('addon_id') + + new_params = {} if incognito: - item_params.update({'incognito': incognito}) + new_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + new_params['addon_id'] = addon_id playlists = resource_manager.get_related_playlists(channel_id) uploads_playlist = playlists.get('uploads', '') if uploads_playlist: - uploads_item = DirectoryItem(context.get_ui().bold(context.localize('uploads')), - context.create_uri(['channel', channel_id, 'playlist', uploads_playlist], - item_params), - image=context.create_resource_path('media', 'playlist.png')) + uploads_item = DirectoryItem( + context.get_ui().bold(context.localize('uploads')), + context.create_uri( + ['channel', channel_id, 'playlist', uploads_playlist], + new_params + ), + image='{media}/playlist.png', + ) result.append(uploads_item) # no caching @@ -380,10 +379,8 @@ def _on_channel_live(self, context, re_match): def _on_channel(self, context, re_match): client = self.get_client(context) localize = context.localize - create_path = context.create_resource_path create_uri = context.create_uri function_cache = context.get_function_cache() - params = context.get_params() ui = context.get_ui() listitem_channel_id = ui.get_info_label('Container.ListItem(0).Property(channel_id)') @@ -431,15 +428,18 @@ def _on_channel(self, context, re_match): return False channel_fanarts = resource_manager.get_fanarts([channel_id]) + + params = context.get_params() page = params.get('page', 1) page_token = params.get('page_token', '') incognito = params.get('incognito') addon_id = params.get('addon_id') - item_params = {} + + new_params = {} if incognito: - item_params.update({'incognito': incognito}) + new_params['incognito'] = incognito if addon_id: - item_params.update({'addon_id': addon_id}) + new_params['addon_id'] = addon_id hide_folders = params.get('hide_folders') @@ -449,25 +449,31 @@ def _on_channel(self, context, re_match): hide_live = params.get('hide_live') if not hide_playlists: - playlists_item = DirectoryItem(ui.bold(localize('playlists')), - create_uri(['channel', channel_id, 'playlists'], item_params), - image=create_path('media', 'playlist.png')) - playlists_item.set_fanart(channel_fanarts.get(channel_id, self.get_fanart(context))) + playlists_item = DirectoryItem( + ui.bold(localize('playlists')), + create_uri(['channel', channel_id, 'playlists'], new_params), + image='{media}/playlist.png', + fanart=channel_fanarts.get(channel_id), + ) result.append(playlists_item) search_live_id = mine_id if mine_id else channel_id if not hide_search: - search_item = NewSearchItem(context, alt_name=ui.bold(localize('search')), - image=create_path('media', 'search.png'), - fanart=self.get_fanart(context), channel_id=search_live_id, incognito=incognito, addon_id=addon_id) - search_item.set_fanart(self.get_fanart(context)) + search_item = NewSearchItem( + context, name=ui.bold(localize('search')), + image='{media}/search.png', + channel_id=search_live_id, + incognito=incognito, + addon_id=addon_id, + ) result.append(search_item) if not hide_live: - live_item = DirectoryItem(ui.bold(localize('live')), - create_uri(['channel', search_live_id, 'live'], item_params), - image=create_path('media', 'live.png')) - live_item.set_fanart(self.get_fanart(context)) + live_item = DirectoryItem( + ui.bold(localize('live')), + create_uri(['channel', search_live_id, 'live'], new_params), + image='{media}/live.png', + ) result.append(live_item) playlists = resource_manager.get_related_playlists(channel_id) @@ -489,7 +495,6 @@ def _on_channel(self, context, re_match): def _on_my_location(self, context, re_match): self.set_content_type(context, content.FILES) - create_path = context.create_resource_path create_uri = context.create_uri localize = context.localize settings = context.get_settings() @@ -498,8 +503,7 @@ def _on_my_location(self, context, re_match): # search search_item = SearchItem( context, - image=create_path('media', 'search.png'), - fanart=self.get_fanart(context), + image='{media}/search.png', location=True ) result.append(search_item) @@ -508,29 +512,35 @@ def _on_my_location(self, context, re_match): if settings.get_bool('youtube.folder.completed.live.show', True): live_events_item = DirectoryItem( localize('live.completed'), - create_uri(['special', 'completed_live'], params={'location': True}), - image=create_path('media', 'live.png') + create_uri( + ['special', 'completed_live'], + params={'location': True} + ), + image='{media}/live.png', ) - live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # upcoming live events if settings.get_bool('youtube.folder.upcoming.live.show', True): live_events_item = DirectoryItem( localize('live.upcoming'), - create_uri(['special', 'upcoming_live'], params={'location': True}), - image=create_path('media', 'live.png') + create_uri( + ['special', 'upcoming_live'], + params={'location': True} + ), + image='{media}/live.png', ) - live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) # live events live_events_item = DirectoryItem( localize('live'), - create_uri(['special', 'live'], params={'location': True}), - image=create_path('media', 'live.png') + create_uri( + ['special', 'live'], + params={'location': True} + ), + image='{media}/live.png', ) - live_events_item.set_fanart(self.get_fanart(context)) result.append(live_events_item) return result @@ -563,7 +573,7 @@ def on_play(self, context, re_match): video_id = find_video_id(path) if video_id: context.set_param('video_id', video_id) - params = context.get_params() + params['video_id'] = video_id else: return False else: @@ -646,10 +656,11 @@ def _on_subscriptions(self, context, re_match): channel_ids = [] for subscription in subscriptions: channel_ids.append(subscription.get_channel_id()) + channel_ids = {subscription.get_channel_id(): subscription + for subscription in subscriptions} channel_fanarts = resource_manager.get_fanarts(channel_ids) - for subscription in subscriptions: - if channel_fanarts.get(subscription.get_channel_id()): - subscription.set_fanart(channel_fanarts.get(subscription.get_channel_id())) + for channel_id, fanart in channel_fanarts: + channel_ids[channel_id].set_fanart(fanart) return subscriptions @@ -821,7 +832,6 @@ def on_search(self, search_text, context, re_match): page = params.get('page', 1) page_token = params.get('page_token', '') search_type = params.get('search_type', 'video') - safe_search = context.get_settings().safe_search() if search_type == 'video': @@ -831,35 +841,36 @@ def on_search(self, search_text, context, re_match): if page == 1 and search_type == 'video' and not event_type and not hide_folders: if not channel_id and not location: - channel_params = {} - channel_params.update(params) - channel_params['search_type'] = 'channel' - channel_item = DirectoryItem(context.get_ui().bold(context.localize('channels')), - context.create_uri([context.get_path()], channel_params), - image=context.create_resource_path('media', 'channels.png')) - channel_item.set_fanart(self.get_fanart(context)) + channel_params = dict(params, search_type='channel') + channel_item = DirectoryItem( + context.get_ui().bold(context.localize('channels')), + context.create_uri([context.get_path()], channel_params), + image='{media}/channels.png', + ) result.append(channel_item) if not location: - playlist_params = {} - playlist_params.update(params) - playlist_params['search_type'] = 'playlist' - playlist_item = DirectoryItem(context.get_ui().bold(context.localize('playlists')), - context.create_uri([context.get_path()], playlist_params), - image=context.create_resource_path('media', 'playlist.png')) - playlist_item.set_fanart(self.get_fanart(context)) + playlist_params = dict(params, search_type='playlist') + playlist_item = DirectoryItem( + context.get_ui().bold(context.localize('playlists')), + context.create_uri([context.get_path()], playlist_params), + image='{media}/playlist.png', + ) result.append(playlist_item) if not channel_id: # live - live_params = {} - live_params.update(params) - live_params['search_type'] = 'video' - live_params['event_type'] = 'live' - live_item = DirectoryItem(context.get_ui().bold(context.localize('live')), - context.create_uri([context.get_path().replace('input', 'query')], live_params), - image=context.create_resource_path('media', 'live.png')) - live_item.set_fanart(self.get_fanart(context)) + live_params = dict(params, + search_type='video', + event_type='live') + live_item = DirectoryItem( + context.get_ui().bold(context.localize('live')), + context.create_uri( + [context.get_path().replace('input', 'query')], + live_params + ), + image='{media}/live.png', + ) result.append(live_item) function_cache = context.get_function_cache() @@ -936,10 +947,10 @@ def configure_addon(self, context, re_match): # noinspection PyUnusedLocal @RegisterProviderPath('^/my_subscriptions/filter/$') def manage_my_subscription_filter(self, context, re_match): - params = context.get_params() settings = context.get_settings() ui = context.get_ui() + params = context.get_params() action = params.get('action') channel = params.get('channel_name') if (not channel) or (not action): @@ -1076,6 +1087,7 @@ def maintenance_actions(self, context, re_match): ui.show_notification(localize('succeeded')) else: ui.show_notification(localize('failed')) + elif action == 'install' and maint_type == 'inputstreamhelper': if context.get_system_version().get_version()[0] >= 17: try: @@ -1090,10 +1102,10 @@ def maintenance_actions(self, context, re_match): @RegisterProviderPath('^/api/update/$') def api_key_update(self, context, re_match): localize = context.localize - params = context.get_params() settings = context.get_settings() ui = context.get_ui() + params = context.get_params() api_key = params.get('api_key') client_id = params.get('client_id') client_secret = params.get('client_secret') @@ -1244,7 +1256,6 @@ def on_root(self, context, re_match): if old_action: return yt_old_actions.process_old_action(self, context, re_match) - create_path = context.create_resource_path create_uri = context.create_uri localize = context.localize settings = context.get_settings() @@ -1258,12 +1269,13 @@ def on_root(self, context, re_match): result = [] # sign in - if not self.is_logged_in() and settings.get_bool('youtube.folder.sign.in.show', True): - sign_in_item = DirectoryItem(ui.bold(localize('sign.in')), - create_uri(['sign', 'in']), - image=create_path('media', 'sign_in.png')) - sign_in_item.set_action(True) - sign_in_item.set_fanart(self.get_fanart(context)) + if not logged_in and settings.get_bool('youtube.folder.sign.in.show', True): + sign_in_item = DirectoryItem( + ui.bold(localize('sign.in')), + create_uri(['sign', 'in']), + image='{media}/sign_in.png', + action=True + ) result.append(sign_in_item) if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions.show', True): @@ -1276,8 +1288,8 @@ def on_root(self, context, re_match): my_subscriptions_item = DirectoryItem( ui.bold(localize('my_subscriptions')), create_uri(['special', 'new_uploaded_videos_tv']), - create_path('media', 'new_uploads.png')) - my_subscriptions_item.set_fanart(self.get_fanart(context)) + image='{media}/new_uploads.png', + ) result.append(my_subscriptions_item) if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions_filtered.show', True): @@ -1285,8 +1297,8 @@ def on_root(self, context, re_match): my_subscriptions_filtered_item = DirectoryItem( localize('my_subscriptions.filtered'), create_uri(['special', 'new_uploaded_videos_tv_filtered']), - create_path('media', 'new_uploads.png')) - my_subscriptions_filtered_item.set_fanart(self.get_fanart(context)) + image='{media}/new_uploads.png', + ) result.append(my_subscriptions_filtered_item) access_manager = context.get_access_manager() @@ -1298,8 +1310,8 @@ def on_root(self, context, re_match): recommendations_item = DirectoryItem( localize('recommendations'), create_uri(['special', 'recommendations']), - create_path('media', 'popular.png')) - recommendations_item.set_fanart(self.get_fanart(context)) + image='{media}/popular.png', + ) result.append(recommendations_item) # what to watch @@ -1307,36 +1319,41 @@ def on_root(self, context, re_match): what_to_watch_item = DirectoryItem( localize('popular_right_now'), create_uri(['special', 'popular_right_now']), - create_path('media', 'popular.png')) - what_to_watch_item.set_fanart(self.get_fanart(context)) + image='{media}/popular.png', + ) result.append(what_to_watch_item) # search if settings.get_bool('youtube.folder.search.show', True): - search_item = SearchItem(context, image=create_path('media', 'search.png'), - fanart=self.get_fanart(context)) + search_item = SearchItem( + context, + ) result.append(search_item) if settings.get_bool('youtube.folder.quick_search.show', True): - quick_search_item = NewSearchItem(context, - alt_name=localize('search.quick'), - fanart=self.get_fanart(context)) + quick_search_item = NewSearchItem( + context, + name=localize('search.quick'), + image='{media}/quick_search.png', + ) result.append(quick_search_item) if settings.get_bool('youtube.folder.quick_search_incognito.show', True): - quick_search_incognito_item = NewSearchItem(context, - alt_name=localize('search.quick.incognito'), - image=create_path('media', 'search.png'), - fanart=self.get_fanart(context), - incognito=True) + quick_search_incognito_item = NewSearchItem( + context, + name=localize('search.quick.incognito'), + image='{media}/incognito_search.png', + incognito=True, + ) result.append(quick_search_incognito_item) # my location if settings.get_bool('youtube.folder.my_location.show', True) and settings.get_location(): - my_location_item = DirectoryItem(localize('my_location'), - create_uri(['location', 'mine']), - image=create_path('media', 'channel.png')) - my_location_item.set_fanart(self.get_fanart(context)) + my_location_item = DirectoryItem( + localize('my_location'), + create_uri(['location', 'mine']), + image='{media}/location.png', + ) result.append(my_location_item) # my channel @@ -1344,8 +1361,7 @@ def on_root(self, context, re_match): my_channel_item = DirectoryItem( localize('my_channel'), create_uri(['channel', 'mine']), - image=create_path('media', 'channel.png'), - fanart=self.get_fanart(context) + image='{media}/channel.png', ) result.append(my_channel_item) @@ -1356,8 +1372,7 @@ def on_root(self, context, re_match): watch_later_item = DirectoryItem( localize('watch_later'), create_uri(['channel', 'mine', 'playlist', playlist_id]), - image=create_path('media', 'watch_later.png'), - fanart=self.get_fanart(context) + image='{media}/watch_later.png', ) context_menu = [ menu_items.play_all_from_playlist( @@ -1370,8 +1385,7 @@ def on_root(self, context, re_match): watch_history_item = DirectoryItem( localize('watch_later'), create_uri([paths.WATCH_LATER, 'list']), - image=create_path('media', 'watch_later.png'), - fanart=self.get_fanart(context) + image='{media}/watch_later.png', ) result.append(watch_history_item) @@ -1383,8 +1397,7 @@ def on_root(self, context, re_match): liked_videos_item = DirectoryItem( localize('video.liked'), create_uri(['channel', 'mine', 'playlist', playlists['likes']]), - image=create_path('media', 'likes.png'), - fanart=self.get_fanart(context) + image='{media}/likes.png', ) context_menu = [ menu_items.play_all_from_playlist( @@ -1399,8 +1412,7 @@ def on_root(self, context, re_match): disliked_videos_item = DirectoryItem( localize('video.disliked'), create_uri(['special', 'disliked_videos']), - image=create_path('media', 'dislikes.png'), - fanart=self.get_fanart(context) + image='{media}/dislikes.png', ) result.append(disliked_videos_item) @@ -1411,8 +1423,7 @@ def on_root(self, context, re_match): watch_history_item = DirectoryItem( localize('history'), create_uri(['channel', 'mine', 'playlist', playlist_id]), - image=create_path('media', 'history.png'), - fanart=self.get_fanart(context) + image='{media}/history.png', ) context_menu = [ menu_items.play_all_from_playlist( @@ -1425,8 +1436,7 @@ def on_root(self, context, re_match): watch_history_item = DirectoryItem( localize('history'), create_uri([paths.HISTORY], params={'action': 'list'}), - image=create_path('media', 'history.png'), - fanart=self.get_fanart(context) + image='{media}/history.png', ) result.append(watch_history_item) @@ -1435,8 +1445,7 @@ def on_root(self, context, re_match): playlists_item = DirectoryItem( localize('playlists'), create_uri(['channel', 'mine', 'playlists']), - image=create_path('media', 'playlist.png'), - fanart=self.get_fanart(context) + image='{media}/playlist.png', ) result.append(playlists_item) @@ -1445,8 +1454,7 @@ def on_root(self, context, re_match): playlists_item = DirectoryItem( localize('saved.playlists'), create_uri(['special', 'saved_playlists']), - image=create_path('media', 'playlist.png'), - fanart=self.get_fanart(context) + image='{media}/playlist.png', ) result.append(playlists_item) @@ -1455,8 +1463,7 @@ def on_root(self, context, re_match): subscriptions_item = DirectoryItem( localize('subscriptions'), create_uri(['subscriptions', 'list']), - image=create_path('media', 'channels.png'), - fanart=self.get_fanart(context) + image='{media}/channels.png', ) result.append(subscriptions_item) @@ -1465,59 +1472,64 @@ def on_root(self, context, re_match): browse_channels_item = DirectoryItem( localize('browse_channels'), create_uri(['special', 'browse_channels']), - image=create_path('media', 'browse_channels.png'), - fanart=self.get_fanart(context) + image='{media}/browse_channels.png', ) result.append(browse_channels_item) # completed live events if settings.get_bool('youtube.folder.completed.live.show', True): - live_events_item = DirectoryItem(localize('live.completed'), - create_uri(['special', 'completed_live']), - image=create_path('media', 'live.png')) - live_events_item.set_fanart(self.get_fanart(context)) + live_events_item = DirectoryItem( + localize('live.completed'), + create_uri(['special', 'completed_live']), + image='{media}/live.png', + ) result.append(live_events_item) # upcoming live events if settings.get_bool('youtube.folder.upcoming.live.show', True): - live_events_item = DirectoryItem(localize('live.upcoming'), - create_uri(['special', 'upcoming_live']), - image=create_path('media', 'live.png')) - live_events_item.set_fanart(self.get_fanart(context)) + live_events_item = DirectoryItem( + localize('live.upcoming'), + create_uri(['special', 'upcoming_live']), + image='{media}/live.png', + ) result.append(live_events_item) # live events if settings.get_bool('youtube.folder.live.show', True): - live_events_item = DirectoryItem(localize('live'), - create_uri(['special', 'live']), - image=create_path('media', 'live.png')) - live_events_item.set_fanart(self.get_fanart(context)) + live_events_item = DirectoryItem( + localize('live'), + create_uri(['special', 'live']), + image='{media}/live.png', + ) result.append(live_events_item) # switch user if settings.get_bool('youtube.folder.switch.user.show', True): - switch_user_item = DirectoryItem(localize('user.switch'), - create_uri(['users', 'switch']), - image=create_path('media', 'channel.png')) - switch_user_item.set_action(True) - switch_user_item.set_fanart(self.get_fanart(context)) + switch_user_item = DirectoryItem( + localize('user.switch'), + create_uri(['users', 'switch']), + image='{media}/channel.png', + action=True, + ) result.append(switch_user_item) # sign out - if self.is_logged_in() and settings.get_bool('youtube.folder.sign.out.show', True): - sign_out_item = DirectoryItem(localize('sign.out'), - create_uri(['sign', 'out']), - image=create_path('media', 'sign_out.png')) - sign_out_item.set_action(True) - sign_out_item.set_fanart(self.get_fanart(context)) + if logged_in and settings.get_bool('youtube.folder.sign.out.show', True): + sign_out_item = DirectoryItem( + localize('sign.out'), + create_uri(['sign', 'out']), + image='{media}/sign_out.png', + action=True, + ) result.append(sign_out_item) if settings.get_bool('youtube.folder.settings.show', True): - settings_menu_item = DirectoryItem(localize('settings'), - create_uri(['config', 'youtube']), - image=create_path('media', 'settings.png')) - settings_menu_item.set_action(True) - settings_menu_item.set_fanart(self.get_fanart(context)) + settings_menu_item = DirectoryItem( + localize('settings'), + create_uri(['config', 'youtube']), + image='{media}/settings.png', + action=True, + ) result.append(settings_menu_item) return result diff --git a/resources/media/incognito_search.png b/resources/media/incognito_search.png new file mode 100644 index 0000000000000000000000000000000000000000..2dcb821a33e65d5d06d16b1923f7830b6d2cc9eb GIT binary patch literal 4725 zcmbtYc{r5o`yX3GGU}vI43(pp8HUD~h8SauBKxi}GnO%C#w;59mTcLMN{S*nwrs~< zM6#rkgHvd+@05y3g!zrO<9Dv}TdwQ-UhjLo@AG-?`}5rQ^SPhh=dNg``l0q$I^xv7=D zqN1X*va+YAr;CdV1OhQNH6@eDNl8gLIXR`Jr8PA*4@n1Y&)Cox|Y_4GlRtIW;ymj*N_a`SN9AVxqORH8V5Q-Q67qgIQTwB_t%U z+3b~-m9(_9vuDp57#O^L`_{(BMqOP!BqZd?lP4)DDQGnM^5x4PK71%IFGrzJi;IgE z78cpr*~`nz4<0-?dGaKK!I+tudH(!)MMXtHLBaLw*E>2o8X6k>{QTzT=BB2mjvhVQ z-rjC!XLtJa>FDTaZEbBh9PZ}khR5UIym=EF8~f_jtMKsf^z`(|$jF6-1qTO*zP>&r z5=kTyF&GSs#R>=rFfuZtQmHyRI^EsfN=iyWK|xhjRZ&q<@$vD!y}ic9#z&4E$;->D ztgM`$pD!sX`Sj`2&6_t13kz*+Z6_xuH#RnI-@bkC-o4@BVQXva@$vE1)z#YC+Pin} zE-fvYn3zmYPy74(kByDBwY6QndNn2{hD0J26%{cH0v>Ti+DD~-;6q?wK`b8$$uqAa=$lW zs65vS1VYj9?nn!a!FMa}NK4+6!Js0c&~xX`LC&c`C^QeKs-~tUR0Rfw!N6P#Fx{Wb zz_P$(`oV7`-)S%eI*#T=Wq47@piNq=8->Zxl9&I^t;=B0yxe}$lj#ubSA@Cg0O4j1 z#p3BuRfx*>&_LV^_00*Xi>H4LcN6=*nkB(^3-lc%d3{}(ilxyBR{m6imb?XlPGQn; zgddT9WB8j{k4C^U2zYIn3QQHOf&jzdDCqY^e@t!e)8E&Rkn~v$W3C&50CU$+LxACM zH8@yZ9j5`-RKvl+s%q};aCNMky1EYsGSTn;EOO z+gPIQxJyhJe>72_cbCxP5fHBdzxa7p7T;q$7Gu~R$@KHfc+=e==I8ebI)uDmNJgX2 zrJl~O9xA;x6Ec%K*AQUstg%xU$p8Pj#gC`s#1V;5`>xp3JcrXYH@v(TrV+1R3@;PYnKYpGEDN5Pt*38uHACVw;>_m&D3ch#gHHQ3%&od(c~uq^g{Pga z4gP`H5x=hID;eq$(Vv~nYb_`0t}46l&5jS*Z3=O|cYP@5@HMIW(v0Rk z9l4jlth@O&%?($+91e2oh>SMr?Lm*y)(7EHX1gEF-s<5gq`qvI9jTDYn4X=nzG4yH zctv)sqUFQ$m~K5H47R_}{CYsGLc-w`Kk1SQn2n{>6)8YkXK^3#-Tc8`Hb$y*-@@!s zZDZiC$fy?z<2Pxaj(-8GB8@sEcDmjR)>he> ze)|&UilKDU67#?k^(0K2cGEZVRfKT~OH%jB~#zSr1&7-Bt5$ zYRCn$EY-VHuce&af2XU3#`9ahXSh^7Vd65lTJ$oKypQn}d6?Jw&|kbniWT0d;;zO7trx`qlAH<#SsdY~nd4bSHg zt#_8is_YFFw>*(#y7W$G*YUYB6`Yw|d93nin~l_qvq!YL;m@p}-YN?VhmB6&kILnF z9g!eeQ5s5;R>eILeR}hD{L9B4a^=y1F_M9Tx_T^8B{g@)zDN9{t&c_D{_*H)h?{GC zlduC2T7yjjw1VcJ(t!ODl3hFU`TlZ@4lEGuoWCZ%tkdo-7$J30n*&ZPd>`U|yjJXk z%#P0c(M>yLX8nfyfL6bTj;$>at5XM)AZzcxEF%g!=svT@gyzp~KlQ#@g8zAX|C;aZ zD+09}7lT zzZ{jNuu>(?oU>lSED@$y%7l$QN-nmnaF@``tg;b8$ zKI>2qtHHQlz6afl<`~0$+0qd@^?6tfXXCX!bYSSW9c%c)B5BADU;WaKdm|L}o>CMQP(+ zGjE@-OyY7*sK>l)FsH?fg-_hA+oh}d@yy4H*}R<2v%gIY;Xk-;AhC$2``sGu%uiQ@ z#U)7}7I`$qG2@i z%0198YpVImGG!b2{y~?zu_H?8ja4&-NK9OE%Qf*HQON^omwjctYvQ&M$}R zpLcq1Jlhq^gwui_O@cg-p45tK9HnN*EM@l8WVNSeLHZ#XbK#Tg-j$&bukfj%gFjIA z@=Dic?GG!L5F)BLzTxa?7IcX0dt6$?8&(GwO&}~C>JEdXgmeCS;cAz)W>O{+<13!% z-S<+wQqbOaN5m|?dc)B%^xd0*;Slz2w|IdWUO8E*PThpWk3bc5i`934^}6Wcb->-+ zj~N7^d7Z>05h-=EmwJ%j_ZT2V!w=t46g;a;jWY};72H;Pz3tPEgp*d0$%bHR947dY zuA=WvO(D`quIauT>huJk+|m(v^#q`Opxe0j&-8icS;!hZTk1yVc(JME4fwqTM);}V zG^0rKQ&aaJjGw_G&f3JO_N^vd z+Mm~MQtYH$m*uDt&gw6(6dpI6vc*QE?7ItAcu{GlT21ObS$++*SRG7@o*fjTyFatd z)xRFY!lP7*0ubx$s@FFq>PBT`LDd;!`l;?0GmCS4QSaN*)?kNPc!yYyL{%H@C6}z_ z){yOcxJXiTvfIbTY9*lItw3Gkosf2s?d3Wy;5i*$iBkR0poYmt&@Y3AXyy0lcWjU~ z5WDPE+5EomKym%r#ve1Z2{}0lRvm7C*g?3hSVL0|qlnLSS9rx@k0b1=R!cCM zc#(Aer5ll*IjCgNELIY~0}r1W=K4|Zv#o`!o}L@rru_Lp|GQM@&_P=}p0_voSHl+3 zlX%5j(>}8IopLbBZzwZL!VQ{(8k3z0w~Uv!{C>|Y6$V*$cn#)T}TCV9Z zX%(1-`TNVwDWOuQ`JVCHSWfe|s)mZb^{cF5#+|6E@!qJ6Tf?{%mV{Q-aGg=6F13d19JpI27XHSwaKCwJu6>iQOeV5;zwTVg9Z*u=M$G}@ zwe0ud;|^hJaJteLce*`Wk}5p0q9#4ouGbX+oY46L3^<7^s;JogCVg73qrdLR2MGkq zt(47h9Hw^&L`N$RLj~vU(fy|Nw5H$nFLkzz^KQ5nf-QhLA9kMIdG^E3&OAFi4^m$jN(o>F;Njs>YQfZ>;o;%mrTBQH1b5(CX=e0a z*!K)r(-MT9X9aVKy@$kMRlV95s;o;rG=|45n_^$~c9{>Q*)6=uFv-9)wKYaM` z$&)9Jj*h{>!DuwPqN1X&uWw;tVQp<~V`F1^d3khnw7$MRBO@a+GV;}{S4KugGBPrP zf`Xi!oQ#Z&l$4YN1O%Fznx3AXnVFe&b#)jFW`2HtZ*LEW!`9`_$CbWo2a-7Z<;N{rdRvuXa}QyUwb?d|Q^+1a;m-;$D&T3cJEq@=L0 zuvAr5ZEbC(rlzW^tJ~Sx9UmVP6BECG|6WQ;%H7?4XlO`FOG{Z<`RmuOPEJnZ;^K{s zjSdbDfq{W}d3n0Jx;i>KbaZsm($WzT5d{SWH8nM~w6rrbGfYfO#l^*;p`j`&Dqp^Q ziH?q5TwK)F))o^J>*(k>I5;pfGkg8|wW_MBnwnaBd;8_(<@)-1c6K%p2vky1!eX%& z78bIyvRz$W-rnAyKYuPNDhdw|KRG!G3JT)k;d%D#+4t|?ZEbBqAkg&mG%qi&fq_9* zR@U?9&*S6cqoSgknwlmiCR|)xl9H0l&CNf2_~7B;VQ+6gIXTI|!0__r%doI85fPCe zKYrxq=KlQo)62`Nx3@PvJ-xHDQ$az&*Vk7!ti;HVuU?4U&HaR)j$H(XK z#>U1|Q&T0{$sKnBAoGNo`{3b`*!&mx&?t*LEzxpmsjC_V zSnuWr2GDCT_l$?D+&9u6y8kgXGlo>gGE*QnVxCxQB+Nj+8bcXlqq(ZDN=x(L;2sVA zgW5xYz+xClr9k|LxUhIY!V3Q1@}2GGojs-Jih*f2&7~DBu|+|03MW^>ffa9>-<0fJ zZdcT-5Z(({0o^D4KcGB`{zZj9B)1uPqFb@F{7+)&)%0d%9m`|q*~-9#qVztD_cj-A zy|(Pe{INyp<+a?ZwA=WQ$JCE5qSC#L`*sc4;xk`;}3 zmRtj({=U-B8mynFUM^M1pSM=TlsDbd(?A^FUEzHQGzJ}SySiWrTp7%{b$hvuGE=7f zf5uL--kVC?PFOJ|_%W)}vmEJ4YTrBKa&c765$ZoocHT?>q7Zq$nbkh6m1f{Bu_`EN z_-;P@Nr`zfHST79>v5EFEAb-%W$I|@mzt^?$4bXN5GRKy=0zj!%SiD$cTHsd>JU4! zt@B^51S8|4)D7oxxwG~c7Dw)a?L_SBlXDcVx7S3C4@Ekc00jvNUaIexKNrCC(fek; z8+L^Sbsh&+?4e}ddwI#V&j9jE@9Fscx4wa2cA}jd@!2q4ys#i-Z5wbtWpjdr|Evi zH;QIbkboMF={6v!xa?i%X0?RAnq%wt4lWV}n9(h5VQ(Kbs+nMdY3vB=tsxB28JscZ zlEoC>M7w=5{REXeN#4c>+H6Hpf+Z*C#?@7N77-$V9jW8CwHmhws(3MIJinPQaXgW< zo94agF=jHh>_{WX{TQABGect>Fw>H7`=`2LW~$e7Fnq*AbqJ~tSRD9R zZM=cAAhi+3G408xt`?CyF${2vHPgD5pG8hyhr{9oWhErB$`>~n{$NID0CGfzf^+FN zm6MRUsY_+0w}k?&9c&08pd*`!?cq{=`6~zphgV=()HcI7|1qdP*h1{C+L*5{%&)r- zUe~-T?Ra&vXH*1cP{DdLf`wYi6ZNn0>xIs7xgtOGd%qb=+@9@%nOApBcKoWH>3}7i zwP0o@#H>Q%MdfB7M%?a~n*a>BgcB`r4Pfm!=l{iQejTuUc+NvR5{1K~@ zj1KJb#BrI$!!;33e67_DJ}+bHlms=#cSI6is>#5{@rpG5h1yy}YwtO-!<Z<{8XqYmg$W!{47(QG82(6 znOCsK$u>1N^7I0j$pkKq*xy1?*p`$zki2#-W4gD$hB|y+mUgXA;#4xh^};pCNv?>- zS7qX{9cGg%+)E%V!UJ!D)Y@cFb|zm%!jYp^Li6}9d&c ze4CFObJ?bl=em}uZGw9>`CGIrCI7cbqAiMR0rnz%c!7h4Y<>V+!@93Co{TaI<;YO2iSP3|z|%&`oN9@DFs`JC+a2Q(lVbyNuRuFX^YZIxYxHo8GaNvCrJqoi^c za=jsHxCdeekB*5qVWty~s=krx&n{+FCE!9|yfhD!*tue)4Y9ZWweHuc3F#@vUV70d z&U~%Vm;|?inPo*VrzQCydZ>u%mojQhR?h?Xtcw4@Dq%&I`o!_*ettnfQeFJ?#rLRd zM*0!whM-#FM@^cUE+y=H8?x&y65Ig&3_U#q8Bz`3^rSbhfy3ynA+t|ZQa=L{a;qo8 zb(^wDwwyP^%?Dvw8e|@$y%V^iEEreUzU*0+T1kB(XeLVXD-?7H%1v#hkM>P9X=36Y z?zW^`cZI{hN@Pwlt5n)ufevAOVswnEKB@Hc3F7lE=H9$u@DJo3c>qm@xQJUBH8(dF z45Q+rNpsVy5Q5HQCsk_uI4yf%(cLd;!C*k{KJm)%&+kAMkzlFF?)FgnES>dogd1kN z;0JHjGjgXPgB^wWc-s?wA7h_54>HMYLZs{G-|;KYyLGVgcWibO&Z5dc*$RaU37t`J zk|v|gM*ps9j%~`2+ZRh!S?^qeU{$Yy-r0~kEp+hkk{VV#4fu&YVx3j_ew%&;?s&90&$CtN2jWPMbTe9oFesskj|W-qG-_!SWKV?o;cfUyR=^ zK*DT=oLOv#ie3n6q3OE^hArueJSBYN8@lPKk>aAZ&Lnjr+J6}mpfDu{I>4&C(j(+)p3qiP+}ms$*NcKU$>#Ep%c0qfL%CVy>bNq`)n-qy{U z5GWNK6K+C#@k@&Bpi{)B58aC45su#`j^8zhEGTO~WJgviSuAcq;P6+9goEOkgGKM* zAuc~j3CVs^Ann%dUSv;nWe4@^dD9q~T8j|;rE>d;g5H7|b(dIjru5@a4=@J92@}=%yTf#!>A;U-;XbaB`+L2=<5t49>}?kouVmOrPv0 zl=^7G68Txs0XDn>iFw&h0ao5xKcCf?_-!HhT(;!I1P3Q~YeA-SC-y|6Y)1kJtu-`z z#Ciui2m8QL#!ELj&5qJ%eooRUamj}OCj~I(VE&?t&ewDIarA-HI8huJTl4Do;&(ia0n%7>UW3w4~GJvkYj@^cD;!7Zo-y!yKll5 zc;MR8PH)LVUTFRIev^=8zqq-QV_U|wKK;)u57~8z;M-2JAEr{H27qoLuL~k3rf*_@ zIb4&2#K$WC>PeteY@LjYd>|1Bt% z$5)Jb*9AEl`7N_rK|(5}N|Vd`y`Kh~Ye)B3*Q%bjh=oD%r+x;P;L~_gwrv{b4`I3z zqo9cj{&PmarsPCm$CI4BA%FZH6$IK)4+PzcHzSIO^wQJoAUD||+(p`YK9p2r=Uh({ zKuIN|6`4vLN@vpHc0Z7^NKku+`RLV7 z8L?>0l>!8(uhYE?Y>Kko#>q#t;(e>mgFsqma2n4+%&L6dP%f^1AI=W73{|k5SR(wN zN8BE|koe8v6$khtMH56_n2gwem+VQ_^@^w=D(VZ+Wlhgwa0G{S)yWAYN5}rDtKB!P z&7qLeNuOB130D9VT2cv2X2;MQ zXT@TS=;B<7;yWxOgi(?lFu|)4`0(|CK4hb)ac1AHBC#5dn_S9wjmLmgqw~WYI&4p5 zQo+@w=RUrg4Do#hEc@ev2He*CFP<`o=bgSvpG(R8g50rs&GG$8cywmI0xABLMhF{y zl`;9r?Zh*vG@6Egw6E2#V{VJaZGr6!E$zUS=xXjOZ=}dJq4^=d%JTq!tTlv6XPD@p zN+i0NBiHacwm}Uid$Y`lhK(c^WI_h_a+usFD||`O40VyQ?jYOGxFaR3jFRm3r1&kp zQwe%)Hr1nDo3 zh)hNHtjTPd`!lubf3N4yVoSK-WMR`n;q^rArc`GU6`8kmcKSa|Nlv5Wk_xF&40f+S zZuu#?RUF8R(-5RlevVc!!q|~)Nk1!Q*4TSmRcCyB<$>DW0xZiO5lBFw+>8AWVN$X- z%}XYiMuS37ii6atQA6Pwo@sBrGWmIpB(%->`X<2>p0wZL;Bnr+0V~ndSmC&Ltz{=e zee=8Lnzq^VMG4jEZBSE6Sd;IqltWd!7r0jL(+;@K7as+2I1R|iERt7hsdEppj*U_? zbF*6;;0zl3*Rn<%GJzOmv9u(s<#MD17^^PnNlD9HIt=g?xND;MhDsN1{(aq3k-rV7 zX}z^A8t_vh$n4d2k^*I0T+=mW%VV9tF8Y9Cwod32zna+Q#D;`M`y$Y)vO#2lWZ=#4 zoc#lfa19=XUP9&~X9I#k&jj&B`{7}k@?CN>ZmAo(`hh1rdXF-(W4XG|!`xjQXI&rG z%L_zF2W;aW@?YrCLK<@t$A#UDgD@@`zsNM6CZfF8Qi>0egnZruz_jxJs?+4Xg-0!zD}EniXBVkWl%t7FlBWWkvmSGcy=I@b^3V>Eg2E#u3E z^h(#--wONb&mhUnOpg zJV@u7WA#zyc;8>-eV6<8R~d@6N)!3?R9`)Ow+!k_Q`k!VLL(mG^DhS@{sTl93eR`hHIMm|PVJAPlge!etb(=%)BR>-; zG#AC5JbLN)K;WWv_TvGg|8qx4-+yx9YF3iMj{JiujP}d72BL(qf5&nM*cG{lUACKV z4W&}+)}B;xDrp$lB^m3+1|He7X+r&5EK zclLXJRrOW$4dX xMaQ4yQI`8vh1`Gi*a<=Ap=$c0{{S- zoi z^z`)X?Ch9K=9x2Ru3Wj2mzP&qSXf&c zRaLgOwmcrMzrVk&t?lmJyEks!NJ~qrtE($7FAoR^7#JAn>+93f(t7;(F@ZpEaBzr@ zjy5+pH#RmVlgU&n_3hiYF)=Z(U%!rsh&Xrd+|i>)M@B|GJUm{$e2GG#4jw#+#bWt< zesFNGjg1YP&9<E=R5PfXkv-g=ZAClaV-w7wIR`u7DZ#!JBo(^d zTn3EhqeKjcXgrcS{}6-8Me)s`OT;MA^CAp`imcSt(HyEb$_b16VI^9bK@aeFY!nO@ z7#OG*XsE~H&|&&WBoYQUfEgI*iY#=wK};Trugm0a`cCqL220~oI6iEi4~wb3NJ}EK z{CQ?j=nrlTkH_&L|DtDd^+-#I|DuDQC~_Ey%7y9c!GB0o7hUM@PACkOyCioJ`*E5x z&2Jg>17!Ftoy;b2xHNnan`Q=eqH$UN9188H((eqvnXNc95|2hTH-H=H>%vWS4UEyS zABX!&1ZK5wVU&6H-~ZDMGuYiw+2tcyTUOmvZk6k}a|LvL?m1c{75kWGIP zes}qIIviQVW@KPwU~GicN9da%jZERc9G5o#-4D<5ryN)e)!g8h`EP8w4;r>u6o${T ztbK=;XYv=v?~H#pT2|a21WT>64F0#8Qz^gd&GzRomYRu5fzcSWLo_CDvD*5}t4*b# zcs@J^ZMnon(HI7A8T7l>qSi(+NKCpJl&?#rd6WDZJgB)l%OCGgrm63wu$bKcGw9`% zXxKjv{%=BNFMEX+dwl6_UhHnshA3Xc{Wn@f zBj8v5|Cbg%iT%$vO=OE&>Ub6h&tfsmtw_uO5*qeT`2Q32U8-oBh>9ih{5gRZSHGqh zjVT&k9MO!bo{Yu;0N}u#SPY);HI%;6>UGiDD0^W_^G=||({2mR=8cYjX@WEqpC{A) zYF=fhDgHL~{SAdWYEqrKjGbu$D{YZSu)8^rU5d21s{Xw0jzRayGgrU6`{(o zYn&HhuUEZO3&eT#cC{OQu{{B*u6GZ`=A6^!d)?TNQu^@NOHW5nk0)rLUzIZN_Qy=Q z3|1)9Q#xa(cfpI%qa`B(--?EUEETr`<5|e8S8k2`=+)GxUmslGFEsII#qwv+Bkjpq znU>w`k-%0(uTvWbc0H9Wdg!C}w$j(lvGrX`W};KfLJp(*-mB5ax2LRv4bYJ5PH7&m zAY_tn&;QD80+&Moj7mYbokm=~=+*DbEDjjC;S1e@j7}>a$UO(U7 zb*-Z-E#`XJ)R}u%89-y-a~>^0y4Gv+itj)mQ4LNfFVAJ2xKpb#jdJl!J|dZxewBy2 zd!$lvrL1jSlAE~kc1x7v+y;f|_KqX;KR!KwzWO0`(_2(Z>}@P=ed2pv?T`xT*OPNc z>W79G^s1GoWpvtGva@gYgD2=C%IbQQA-E7HJNEb$odBh^CZs40=15LYay#@1&=1nR zG{(BMrTnNj`M1E6`*Z6h8BnFmd1PkO@y2)B1SowS*zmr1to_wEllva|D|U^RXZh7W z*rQm!xwl~sybEAHk!Ch?{%Ea0_;Pzd_=S9>qXTye4h67PXULlqR4Nf-7Yc^A-p{pmj1Dcv?FZJ%cTQN*lOw9uPK?`K2?6v- zu}`Bjgn@*?&h(VLHIPV>8nAJlRKt8k)#~=LfKJKp$kyGTbRmO8cXU~oW~rZJ(3y+9lNJHSIXJ3pVY7^S&-K#Y~WM zVoJE06{Dfx4l`-PxQedl<}Yi2ogUnk1slbZZ*NFt?>}1hbPu6*>s9h|TH7}j{K*dZ z!u8{UfbJ05%I6XH0o_lD^dPq5(h2S>Pf zDSE7I4jyecfj_=g%j%iY;e#8r#Jr>zBw(T(oqPw_(^{pU0VO(qflU~PfL->KWoX0l zHzYdBe|G-b6vIP|4aGHJ8VkBWF^=+%@$=kb-SZANC1cjLN0OjKToG(O{UK5O3?2Y? zm9(;Wh^@ST6OiiLdL_TW#O4KCm76Lh*JN#aYae$Oe{(*Oho%d?wVBXl$X51wE-NusjwF(IL4=04N(sQpbWogV{F>J;g)R zPTAs*#?c!NoDgA8q8+=@5)ejLgbu_DA(4P92?8;?5cl^1PjkDKPSx22v{pTLC`Czc zKjl+>w>vaxb9McPg7`T^$UApINQYLVxyLc3_2p5gCBx#81RYENRbvx5oQ-S&H?&xNA|{D(tVEB znL6t_j>B#VB%KuOna?Cp_df*TBnN^dY_?O%a(Wj?q(M*zCP?b)LYzRzEH(Lc8r%igZP$P_Y_Uo{(o^_hrcJ zWAeTu^Lq(vhb}>C!?_xHRMX2J52~q_T*m1<$Q@{uu0YOj`!ET;>?eqrFqv(U%R^`G zoyk<*W<_TXWIZ@Dg{ZTt0e*r9@r_+vgKG6xT=08#?K4>M^(?)!AhY;}{cK&i^IVoo zulAUu|Lmx6zQVLNya!!-iQqglRe1P?HZ8209Zc$fLZ}T-P;+T30o+Pr9n|c-#?m~r zpEDrW1nJyANIrC+$*^kX&|nBIh4x^dd3WQP2!dv(={D)5QMbsTN7H`f;#nqg_+Xgv+;BnTWr6h6`_Box1i3IJw3| zLY02yzL!K~U#Sy@$(kEcZ-!gRi;ud58yXmJX1Fr2?ldsdGM-{__6@{QKCTz?*(Sg~RdmaM*aWZo(-jB< z#H)$NxB`H2M2XHYCnb%sV09t5*`;&#;zYJZ=(>2Zi(>VzyW!c85;dFa3ngkFZEh|U zffe@7i`}xgJl6--c}xu3XPqAuY4jib1PqMoncc80N4e-aSgmI9KlV;*2kb3NV&wk- Dm^Vpq literal 4673 zcmb_gc{r49+n*U^%@&CkLxnKLI+!eD5~Gmpq-8b?W5~=f_MxPbCre1OM_ICD8ABpO zB9g5LNs$oQJ+^nKujhH*^4Ce zS^@w-))ENdVq?AB3+$~~FLoCLGXnsiOKWCkW@2I@D=TYlZEbjX zSV>7KEiDa&LVfx2B_<}OtE+2yd3k(%9DzWzw6xr~aU(7+PESuSB_#!kL{3jnTUuIL zTU*m;G;eS3j*gDLzP`l7#LmvnhK7byr%t_i@xs*9bYNfrgTX{aMKw1!4-O96+S+Dk zXIEEOtE;QSV6c&qk>KFq7cSV_+Xn^) z>gec@NF-%t<({6N`1trMSFV(omnS79wYIiKMn)DD6?u7im6Vj2o16Rl`%@^C_wU~u z8XD&2<{B9pg@%S&SXf-TbjioZCpSZDx1 zN1x_}#=7GvU<}@cK+=_+sjig;6L7k+b{b~NW?m=ot^}07H{QzM+#2iej@8D=>g)07 z(BLcr4?G18rg;!aWH?P%c83?vT5qeNvMi7{&KYiLX!Hxhl5}NVDHJa_6zc2itK_Su zQ&56c%QYpH! zvOM5l)CLraHv#jTokUhb?`WvoCQ2-)p=cZ#s-mR4eJwCp=O-R+fFo~d-~0#Vk2sKRuh|F*S# z#$N^}X%rI{Tnn%2tf{62fx*;Z5OsB|CPZ5e3xlYrIXlDD(HM1gjMi_WpCMEMr8d}P~4R$vFiMRHoVqLdg(Np~m`%AZj)q!qDmq_>-lpWz$RDN6h z4B1cM-_r8GPZbXPJ563xZ{kh~;IL3U5$}N~QMQw$@+(O=ESy515b=Md$bd-sXNp+0 z2PdLQF1oTb2oCRzrV=T#dJdjcYbpj0HoB#_*7wgLa6Vio#Mxc^TfWp(23nEvAiG_=>>ik=07 z?-Z-2x3#AyQSStrTj2cE|0InB;ai0N0F1~Wm=y;g*`XE1Z@kM6WG~PwT7w6-w%DF^ ze$kQvYL}e6Wjqo4F5z>MaZ%Hg0vz}AtL)JLVFU;T;06Pa(EnQkA_6f!Wlmn}#E*QN zB0ep?+?~3AZH{5ijEI)~q1yMP-@r4ct?gsc=M%HDFiE2^-p6|%uVYXcySR&c7&e06 zuxaFc&4IxotW^sG8s7g}z6n}%@Qu6~)QMq&D3lH1x56c5Iqr{LE#UlW*@bQdVCG+J_O%m{F<CHXO`bCjn%F^x8)u+A-P96F>`u#Y&S9yhZ^7txH& z5F>oZs-6FXBp^8-=V;)uHl7&`Tx(tHz-hFo7tUOA2z>H+7x~CwQBZZ**Ov4+(KDQUsXw{zt~r&9(%mp?K#~2HEp@hsVTIthHb=S<<6znmsaPPBh-__7W)k@$$GiKOvn3=riSd<#UIF`9}QH60R#H3?d1RIM{#_ zp8 z{_6d-<4J8=m3aMSU9S2N{T169{+K9B=xp{W@ihG0=M^Y-gZ>)s-A)5*f zj(7T_`8VfiQO>*Uhz7ay%9UinIa_p26zsqO%YzdMp?lI%?@Ebk$tD(43;4!fP~rTOTT?M|)UooxVX@i`9mHPteEXXI{9y49v8E z_F8CFO83o~Tp}t|lCR&3b2u70lz!$*#wJauKRlaGx^h29q%CLNh<^ zE-7L&ex7!Ejc@)#0Dac78ueP-_zuY5t+0M8Eb%FueOXVP?x_QN2XBhdo_2ClW#9nF zSFv1O7v9AEU+d@})F0n!u9HFaraOmLJuy24eADEITDTD)AP7q0rF2Qy7}Rl*mftIo zwlFd!INZ@LVSUkcLau9D(A0X1DMqm0^nOiL8jtz1;V5xt%uEqBl*mKNK*tC;bl;1~ zG&)c4e;kuPq!?lX+$WKmkAGm@@VyKVs*ACpi>^FQpQYVj)JjkvyU-^0QM`0leR@WO zkJ>HG073y6mIWXQ?ZY8Gz##i@#n`5&>Eaxwp}JteWR}sx1u5Q{%ceky{INEr4>GdTn!pjy53VW3!G@UEzii}3Sx*U#>^1P1vsu1jizKVN^KB8xKuBIEXI z_=<}(+I?>#$n08wA!?HQJXA0dfn=k{obJ-oY|hNB-aU8NIIi+X@e_&oq_Y6io26nJ zWVN!kGaN2mLidyQ&E8c2O!;SZ%1~X9H`r$Hd^qB9m(fD<*NyDEgp)J;0e2WeLpfidj37uj=C--QqBM1JZBzJ zqKZ#gHD4sUx6-5tQ_%kXp4JffV(fc8t!KYXXE9 zy@#ikro0nEtfe-jgI>lE59L!|mbyD~*mgs$I>q`GRQTlS6zZd0RlRe~B&=VkV0DTKH@$C#UhjN$wvtLf6p4S7IC;`qC|9Fb$3zhBBo*U~&cfSFC`o z8*ZcinGD!%cs^4pO9in0?sHTlA4G{znVoWS^Ey|4P<@wHYcc5MD>ymzsKU&>CvSZ! zkWH}F6van6OWE~5zEztl&=sH4Eac*y)qA1Vj1PWCw2$WDTy=xVLuDYY3A>V4nUQ+u z`Qmt;teosFBua_f0i+<=y{}f@^7iLE$VvZ1+K`Hlj`rga?78Tm_|o0{oYvy^O&#_) zguI5|cN^!urxOR(TQ(u0ZsnVD`eQ-v`+|?3B)sZI8qU}zBx+UE_OL-TzfR=YTo59j z$0fy1Ei^~*?PiWrdr4}inkOr@&7b+I>x&^#TO?SU9EG< zpQ>DcJj>k6?lJyhH1=-e!)U>MEvF|}&-rvre&G#P1i4*O8b(P;y?GHbY4EKsb5d+5 zY$RmxmEX{Jv!y!<-TQ<=HM5T%bO%p0_rz5llnVBjSv-7$dap3(^A#)t?DTE(8TI&;&tLh{{=`m0jx~PXIK!oZ zsG^-|iqLN>>^;=PLFp@tS)#|7oAJRxEUCcXZ=Du1U>6s#f72Ci{qLJu?=y z(n99DqgRftdAJ@UjeUsfn6uB#!tpBW8H}X~#LfFkF4Pp98}~L^vPkmsGlu z`vL+lIHLs{R1XR2@wKUmKeC?t;B%eXXz5(6v%nNY?M4h03xRR8;^fGS{!phxRHL54 zd~-^dt2Do|MR2RTk{|-CiO>4tL=JFmhOgrHIjcgz2 za^D5_BL7R5!5`RhMSnmAOwS0EAI;;?u^aa#zPd=_Awa#fEa#iYikHLg>DqBfo`PCz z(n5?u?OL-hkJoWpWQuSuJTqJcIQcLyErTj046WE=0kzVGcj#EKxEV;1zqoN%@7bdk zRoOS?f!TCdce?M@mrEcaw#gbX{IkV z>c+XVB)F5f8fza3%e_u8jzt`JRxawd+ptD#ojcT`1sq#L5Aqe_mIL*PCfuzYvyuFi zv$3F)xw%Jr6meR8AdPUZIrL>k?GZu3mH}H|ih#;fzuA_xd%ObiSlj>J_cpkUeN%wP XSJG9?E#~(2uLqNp=7xpHOJV;4UsLz} diff --git a/resources/media/quick_search.png b/resources/media/quick_search.png new file mode 100644 index 0000000000000000000000000000000000000000..25623a3ba7932544261b3f09846c8b7d3dbfa553 GIT binary patch literal 4453 zcmbtX2{e@L+n*Um5?K-oVM^JDF~k^SX{;gH5+$=)$ILV{7(2<9Eqk_#vPHa-2xY5~ zP}$#PNwy?Pl5F2-^}Xl)mUI5kdFDLN{kyO0cmJ;Cxz3p=b29@jj{O_}0D#NL5N!bf z0GY2q06Pow;a*~EwY8#I80Z2@J4I%g3sz?xQyl=HJbu?22F%<;o10kaNl8h`$;r96 zxHvdCC@3hHn3#}Ar1<#w8#iv0m6cUhRW&y^cXV_N4i0v8b=|*zKPf4RN~M~cn=2|R zs;Q~1udi=zZjOwMoH=u*zP^5JY;1LPbz)-T@#DwY+1XA`PB0kE%E~G>Hg;)gX=P<4 zEiKK_(NSMte{OEh#>NH?hX)1*wzsz@BqZqS>ZYWmeEj&ayu2KVL@q8aT3A@*GYYIncm*sii(P&qN2>q%(k|+2M->2d3nvw&Q48D$;imGw6xgT+M1b} zMMOlPP$*SZRV)^Z$KzkWejODR_3G8DkdTn{^z_is(E0g!J3G7n{(b}k;pXOsMxz-F zhOe)$p`js#LebXNe(~aktgNiRzkg+AWms5PbaZrgcXvTSL0(>7ZEfxN_&AYBymswc zadB~u29}?h-<=f0(=-5J_oJ;JFhzE^Ff-@#u2!~mTT>GRj_jd;!IQBB1%?NOX$=4% zwHOo(&YeIP#}b@fNt%%P+Ion%D_#?FO2t&slyZ#V;%ewiC0P2JS>b%$aq4)87K)1l z$v`j}JP34*IKzWTq9GWXknh9@=KB^5g)pte#gSCJ6T$+m|I>=O(uBCs=@bMM>h0~V z;H|7craD8F)Ya9YiZCb)CeO5xr}>cR7=}EFcJK$uPZ~6VhNHSt=&odv_!cb&OZKE| zLLfi6b?9`eEA}@%iKc-0j(Bc4C@>?3V(>Jml7iw-Y2wTa`NIjJgQtC$yM_Hc&603_ z8}t(-x_&>If}zp~Rz4JhCd7h3BYRSDgkMU3F#KUYMkQeA1Uw3+2vd?*RFj9PBB4JI z{k64~r$5&(NqP*r5z|eL0CPI3tR}Cjs;nvxhvSaQt1IJF<&~73oK)c$EF6wi`%U=6 z<=^S_u}n4+(1|5kH6?svT@o>b!ZX2Rp31R}wMK%#F|TWNc>@i+wC zl};pVm$)Tbhe+QB{i!vxwGl)N$ypP^kjE38FrGv@1a+G1Y2}F}h#TR^B-;NN^ma-l z^dATRCm~a|eL`D3{{3s->Tc!+GhXHG@iHNVjwjuPOhs9fh^{1e&CT`Bag8JZSXn}T4;2-fn3dm=l|;@@KXO2ahxPw+C#cjNy0?u)KQSJW}C< zdnNzhoG&%&+D@~vEgl==j7YjZ09zA}z%}lz0Uz)hN^-DiL1S;u@D5Y*ghYkQO6{^i z{$5kdJL~3dJy%ipw7Cy9dz3r%k*jV=M)CF0gXt9aSY0kx<(nIa1c+k3s zgs!5R4Q^(vBz`eohZWsb+@PO#buIVv&k+yh5qA*ezjkWH!9=htbu4;A@`_!$zY^tH z<%pAp2I+RjzyfZ;Z|-oM;gN|r>}d2iYyZKow_?&xeEQb$G$!V<+L_JH6XU)7cOI7b z2g_!qmRB+6IQJuJ{j6?Uz0oQbX2^T4DPh+xUyb23km<=EAwG39!ViUaCB}3oN965j zaL&73aZ971)$?JJj&h^$xfWUJ+?=xv4~ww?7Kg|u9bmNCXhmg@ra)}Q%X8*KasEqO zVlp~wLd9!4CX*E(G%I_E*v2hsSGhCrGj<*kzTYrsb@Z^Q*Lp(8p#x)IMkKPbOqbmwgmps65;?P# zevZ0N1u(SK8SA8eW32BShpQu}s)2O$w`-6#K6l-tx4sfTs1QjnF)+I|mV9*eNgZ1s zB_7{f8_*gS$jNwAXjks~u{YJLhPW%q8kzbBBDka7S(a zh_^`U#g$77uzTQWd!)e!+Y_;Wm-P{LWE$qZjlL4S*oU82my4-87vRz@778X|ORua@~dx}voFcgvT{%J89q|Ml0%bpi32;A_3{ypk%pXL))AHt z#p;IZpRdvc!y_M^YzVe#*w3jzIqo}OA<>R776B(0Ji5>7HDPM%YJC*Yj?4axnP3z*mD$L6=+p=iO zvG7^ZB7D8Q;IwqZ9hugsf?!pjb@fh8!4+w0^D!R!-`TmoP=_#4$62BJ!gtFuz^^9p z;txM7=%ydspmk1|+&Ne)8j`C(iP#HeM@L!B67@q6Er#)}q{JG`iu#yTNieE!fA0Y(5D$DEX#_t}mUJR`*-+ zZa!YS@$KU=r-+MQR`5G3PAz zeHxS%%H=x_%mv%%BjbGCnvbPOOuqW^cZ)^CuCe;jot$YWPIL2Aii+$x4s31;;w9HQ z6lUZ`lp-GoT4AL}3qD^d!p0&+1D@T($yX%@9+z^osfKQd zHjaY4QvgEl7bPI)`Txq}08G~QsHAp>qPtxAiu6Umk%7E%^L<0SJv#=6q`o%`Tlu34 z$-$PpVhV>(UVUGqkCnP^iRd~V*2sn5tZpC$Yjc8I+wMZxO%>IeGWY;EF-+ve2OzKB zo_z;cyleC&Z|SyHIH{nU9DsNB7-to*hRP0I77Ra^k!ULU+5woU7VGXEJSF<>I8Hh7 zwbx&F`-?i8PT=f6mZ*=sO&RN-EKHq)YkZUIyG>Sc7F21G0}HPpZOR6f@7UTnPQScQYSxS6CD zN6`3UI$2rzMx1{bNN9CODte#dY!UyNER~jYp(Zf)po&Y$O#Ka1_9E!;l*>f`BU@@^ zX+&VcIr=Uj+V5>o9dAOL#}z(YOi*F&^a8e6_#NNQ(#B+-s>g8P(o9lFjYuY{>vVEw zG_mtg4H7QE%lrP~)sP{3#`;`BXMt9tW0JMtB>p2ebU55}bhK$U{_Em(gqZ9h)xew) zxkGg9WSw7C@RWz^`U+7w2ihE{Y#FoC3BCE&=XGPR%ps~EeMW}oL_{146!v1dn7z>u z)FH4Z)6O#Hb`9L}y$On~9kp^y(1D$Hk6-3x=f22kzOs&z17;(S-9F4uRgVKVo8GJS zPd!t?mCS)s#y84x+M$-lkOk+zZVKR12HZm)^36(SiA(Yh2sQ0Z+MEz;BI@-gezpxT zyKKQ10#v!pB1j><+?bu+waGfRAkD*{s6S!JdoMgo<_s)EW+sjh>`7`>h#i$dUhMP1TZWNrcBYvVpi?20vRK5HR!PaUpN z4w9kI_4eU-eE9UN^y>`!z)u^E4MdIro$`*L z@HjjBPjhchd=O37x(zv(5OHgqJFaRo?4e6>Zu7E>YvE(lJuefAdV>}(ykVvICEe$ zcRmHC9X{E&hbFkN2@?etU5wG!b_kRX;Y?!z@rIV@Ys-qi+HZcc^$r{VU~jO6cB;zt zHZP+q1TNOgY~k9CqV1Ky*Pyz4N*P&qz2wP&6bZ znydGOY`#cq;by+bbmy(j>Rm9ububc?{^gp1qa=87*3I}+_wvpQ;_Uy&ZK!p1IcVKr WNn|O}@5R=Cb0a-7bg8c6h5rK818b20 literal 0 HcmV?d00001 diff --git a/resources/media/search.png b/resources/media/search.png index da5138ed5dad19069a0ec168649be32e27a5c648..e658c7b32fd454776dd27abdefb197beab0f7586 100644 GIT binary patch literal 4246 zcmbtX2T)V#7EYur0-`9=1i`=}HGw1qh@pib5uzeB0%9t~gd`>b0+ywSsECLxC`}*I zq^Q_HK$>(=SYQREND~z4pwa|hu<~~Hz1f*}Cz*Ty??31Jzf^T54gdi1AAtZ70sf7ai+5OCaqO(nfICeJBm9M+r}=(!0N`Gf*k_^;f6v?&=U}O> zt_}u+aX1`ZEbaRb+xs% z-Q3*D%F6ot`Cdy-MfcEp=M`i?d$;p8Q8`Js8-N?co<^TW^Rcl{htd_(Me!?fc9q?TIejJj_^Z^qoOcE8$^I`F= z0RWUKk3}TYs9a?d)zh0{44N!111Wn`j6u%&`*ruTET~@I*g!VbKJb78Igmy+qJT_I zHi@EmNIruPl}l9S`Oq00B+nT1g&4_yUV|YZzLl~vicRrA+F|y7v*NFeL0()g3kiYv z`}>3aVPGcP6QXBiWCYQLLZDC`zJ(4afWam5bQm0!uO#1SFjNki?aktPGa1Tjv_ulq zmun0Hed9Lga@pRbAM^|knD_rC%9-GFz~zL@t$L0@a1;>F65jKoKa& zw?n^gt>x+G^<9!BkGqfWW=Ms47{Cm55C|AT2M#A2=orDs2pv6`hX(>qB*Eb%!yklS zUH+bKFNx2l57mbv^o{i3dIm=NhPpo-zij@!p99mE?6nrE3G|2gPi(yp3bIxdy7#)Q zeTCL%@(0JyjDIy+SKMy|Us`7!{C72{kbl&h<;$jjX(kF8LZwrEs0{8}we{9ln?go% zy}5MidWmbI&FS2A(9c@)TN_CyGCYkzJRJ(vgXl}=f=muGeI0yBRONkSCWG@|gI-UG zg8b#+|0iVDx>sne$G^PIYu(M?;KvJFA1@z5n)`CSm~0bA2Hl%ML#1xm9>Z(Ag(e-#KK_725T-{RQv6upH*kp`B%rlC|lt_s3KlfKX<=+eweHcTC`#V=_#kny z^urAM;?{*>tNEw5NnHAqr}Z{=LCrf?k->L+q}4Jr!pNq5A=1VNC;D;$HV@vzEdTVu2nI1NSXPYui219_t>dvOLu0j zbQ+q(S-zf7k*P=FSS|pZ4SmS2^JAj&MMWomx6_>N=tP)AD3%RHDz(OZW;up+KC8JT zWdrivQL0Hv%7AMd158|B-YzPN83|OegbhS82V!Jf{?K@W&?xQ*+MJzwT(V)~$^4zg zT{jQeFN(339y$B{2Ki-C&uK+q_?j-K9Ok2dFKplRoERZ>u>4o`m5m&2USrNFHGrs9 zUi&kw6Rqy3@gyM<2%Gb*2|ya2{V0#~v%sWyG#|}FHhWQl`EIWL{?Qc|ibLFR3=!zf z&lU|w_I%c#o3XuOFB{b2;}U)3z^*f7L;>RsWdV60v+f%7)yG)@V)xVJC&5WcigUE^ zF7r?(B5lxj{x!BCj=+hF*N^F7drcz`OA3(ljI8(lxhh4B!*44(RP9;akyp5{lLDXH zflFa!iXRTp>zh$YBe$&VnjS4(N%l;u#qJT0iTIG<2Cm#Hzuz{lBe%Om+An@-_0!_S zDp+O9+^^bAEzAqqAz`1!DuvE9;$?7mTEai;eFDbWzG|PjFJ#bIVKLk7uMl^6K_oz~ zE38`Qx{7~pt=-a#jFaVRt>}46+%i3W%sBj4u3w5S`9$7hjC=rjv|Up>-N0oq_e6>M zVq;z6>PBg?XBOm|Zko~_bkoa(>e&a}qdDEch&@j*O)ouVbEklsu;I?}KEavl2B$=A zXh!nONAl*|W`bs-8J{gEJ@=`{F z%PI+x{x0toWxENEs3-jz{vx6Ji;huRO&hhrwwkO-p}1`G*t9J`?Kg<*xsC8dO;zDeGd4$3)0A7{tTrCi-Xs&7;zq9_x@gm~m z00hSGj7W?wo@zI7A+uJu8?|&U$UE0{@0^?lVR?9=fujOH_wJLMzX}y!7U1F_`0}o( zCi2bnTf?>^ZxZV^T~)pr?pLA-I5~{lOo~@oaj7n%B8U3<+u-@@o_p1&1V`hdJ(I9a z9}-4w7tUw@5;I~k_AKwdd#sY_P5Ztb8_NBMrQTr#a|u1rCv}Gm+tkuXt)4AMgkuLx z<$jY=JWBP$|89WZ>UsV0MB~_e?nU3m++7a*Uq+`>4D1hi}WM_=BYQgvFIzzW+lQ$A(!fKYIy zG_>cjpD^!rmI>VH#C`v)lBphy-E?l)!I`=#GRFfZDDlA^bnH}4u*IJuw?Dl`R|@pj zqqHSb+F#CHWSa~Jd z5WZP~kaKT>`PpRlwnW2Y6D3LD#bq&@TjJLi9PWcJB@E)k`vLc!gw-Uh)V}X9f)Lo3 z)C2){w}tg0@>PFxO|!&ztEARMp2JO4O39NyTWvQA-|gDt6po$RjN5hGS$vyqnaR-1 zScpw)K=gC;tu_ntR>_Ls&^a@pt0(ftX_G3ooP8JT2TZRQizTL_2IfN10(a1ehbv;s z9-v*$R!z=%IUCIC^0u{B=k;EcBA3o?(+F_)XFa^1k|Y#UBwE#$m0DmH`)<||%TMAf z63MDoicAAXN)=xzsCamA#kOY~u?hSR9hP|x32JYo!Lq~KoO>N?(>v7HOt99eY3R5v_96Bfq<@3nA;Jj}E8NP}KoQP<9M~r6#iXEy znyC0KqM~6?byg?jsM0({B)m*TO;n+=aA%qrD0%#alkCcg?BuJxM`8Q#psF#!G4|U4 z3VMZsF9cB5v*Ytuut{w(ggV{bYNC`7}SZYTMDN?Qs^%yU*(NDK^67E~y80p0&E8E@^W&Au!@8^T1T3 zCG5PH0$F)h?l(9_>EMXd@u88|b%kAv@euMUjl!&#W-i$SFL zva?N{U3SR>2Pcx_O>cutqZ6~odv7pTBgo>s*8-o<>&H$KjLc5i^&5zL_U>1dYm}XW zlC$wL<&PRHRl?lFwTF6a7>WaO0-v9PhtIZO-v|bikL)-Vw|p`ns#sZ=lpgd}Kf@uT zIe;cD#Fos@zmwBhY7_cW)lZa{7`om5jF!x^t?j>Sbj?*-DV+nm0?|c8a@B2E>>v#UH>)fv6x)W_}%mn$Q_y7QapoO`y zJpce?4}kz4PWC6@fisHz;qo=GHUI$1Q~B4?JGN$n?ad4UC9n5Sv&Y7^Rwxq%1%=7U zNo8ea9UUE8TU!>3wYs{xxw$zvH)n5euc)Zl-rhbvJ?-b`r=p^=wzf7fFo3~ey1Kgh z`uY|Z7jtuSt*oq|Q0S*mpDZmcZ``<{sj0cVyquSpr?0PHSy?$eJWM1KJ32ZL2t;FJ zBa_L@&(AkBH1zTD0fWI;u3U+Zj%F|zr%s)knVHGT%JTH|OiWBvS69!@&VKv$t+%&# zeSLjlVPR)yCkzHlO-*fXZZ7w6(Q4Iy%P2#wH~txx2e(WMrhJ zr9FK3kiRpJ!457j+1w=<0NC|x>j7pc?b^4M0DqJ-)!Et#jwJyOiwchay{u_hbg{rt@%D0pOq4GJ3(fYrvy>mm4b zXmB<`AfAerr3Dg*!ElF770tpKX1Ba=B zNfcj*swkBlD1k83Cg2n|yRKY4++me;l`H6=c;DWcrpNk;zLEFMl ziQvDz5*bYi#-qZ?cwKpWd@w15g2it$Y#IF`F{0qnR6Gs=Re`F4RA3;erVixarnc7j z%fN(2wP3?>cpMtzqYcu)X{&@VFmRtK^bU4sA5pll1jqVn6~XUKj6 z|CW~jeX4NS-)SO;Pzc*4fWt!Y1biT#NZm@3>aQf>uyCqBm4N>%MFs@wKU2i6JvaeP z^wpK8fpB;qbO?bek8mM{ph7TsSqm(Q82lex`o*FH`P%~j4~NKqE9 zy;1w+4O@a6gi!rR6odnj;7<(D*#i7a#V+*iaQ~k|%I?J9G5yC5XlU}^ik=OFZx<_x zf+CR!2qQEx6s-gKclCeV%Fo@gdzszUkl($}7Ji=|cq04Aps>#d`xn#+0Dv#q!q@;s z1OAz{>L#Ww*>*0pJNBy42aZWkW1z?-BSpmnk7OUS-{-ksf@eJ0Cp|-}opJ1AXvv+d z7spQ@xSM4$^kmMrfoTK1vZpIAfkzm4NT2IGKtz_qnEu}sf_Qdkch1QS4hs~LrXYbC4n#Y&_aJGA^9OdYoeniSS6w7Rr zJAd!9>{>?u^kaX-<~MM<;Nd2vknfK~@J5iNB3ciB`&KE z<`{$NqYA*}7@F{xpz@geLMDI=){J#x!pDGs*i~JD9q%U>Z}<&8fsc6L&wD)ikXv~z zuIP}!Vu6n9^0U%Z9EAgJO&Oi({gGeMu;`_8mV6k}DUMw|jKTwKzR+A~PjDXD#%P$IXS>0#4k`iYx+`Y*<`9@+OYsUgpK&4hLHfuap5z zU$Q=L&ic!n)>ZLN9GePtq962u`}6va_SM*Nn3h70>V6030x*M{$zPjv6Z(cPdKNr9 z!o+;#4C{Wa`|8k;Jl>-?MTrtnWSN%`HKAC}_eHtvK#}VVAG1yiv+7;co-IC#m&>yn z_iS+bx+;{+aNM~pj&5vo&$4S_x|N2)P{fu)HP zSNrCrRIUx0I(J_C(0zbcssAiX*AgI}6gZ6H^IisNUhesxLwWs$Gg;bXHQXKodfyVz zBcBvV>+88**6kzLPLSv=&S@yeE#~!(wOHTS>2o`Sgf0Y4_E-1Xyp$xFDDMLafLxJZ z9O;{0Rbv(>7A!aoIr{64(K&%h^#JDtT<*^mcXk*f>c;;ei5&Gl^fpXSkqUS z2ITgX_2NLv(ZrA;@!fsEqVZAxCIR7!Fl0|1SQiVq{3glOcaK8wYco5gs?r#S$)o(bd^&y=&v&!zU<%ngD!XM-f%oXl3{sD_Q9>2 zpbjW(clxbya*m3`9*YXrx+gtdU$YC)RhKv%u4lf-+Zl8-{@K~5mem1zQO$zyyqhh} z9J;Qo508Nje(8*phk)d@&^E^Ohh{6)@|$-nkp-cp zw56S*9Ye)*sj$V9J}l45w=V{;M*xoHaBlhH(r!y2p?}`J5VZP7ijpjY8Lt6DB$zb3 z8j|V=@^8<-!+80bagg=A{q&VxPazdA`jA(A0zZr)_f8hg7icp{^RF;7pOalH)T7Q| zbfKYPFM`qp#%q{d|IY`X3i-oyx(H!2-%8lOSveuz=q94YX1^H9=-3=Mr66k9X- zt^jD=85tLF?hRdi^7_svf-ktb~+0Gp`0t!Fv%UBJ(9tlQj=VdcEy}3OUx1lc%zP@i6{7JKDYh^v=|JMEmj! zjfBn6lXAX4ZtfNe+GFng+MllWM`qK8m2hd41P?u60yod98SBgD6nqq_k$!OE>fP^M zakqF~#ikFeyP3JoSzr4qUS~mXQ~y3OntO3M8F2A#i6&ynT<#4$fCWqjyboqA`+9A@ z^SG+003qo;S$e29RwdfT)CcX{h^CpcDsxHIT5FPiP5qTQE;NxP79d>KFun5vmlRoB zNIgk~Fr1pf{q%Nmjour;WGwT9e%R5$eG)xlA+v$$+zLXrF|uy}i`@GoJ%x{tYup$1 z8(H@hTs*x)VopQ@UZz^jEtR9AA5Q<)w0cB;oP%{|bkK|c^_+`73;2>Vo!gfE>|Z%$ zmpl~x{dR;qSt}w-rs&RNv+tvA7Zh#O4LPS^(bw&!vu9ScV`pO?LNDGaRQ|8u?EZC6 dyezgNqpsE{B+a4Tx%I Date: Mon, 25 Dec 2023 16:16:49 +1100 Subject: [PATCH 131/141] Add http server path constants - Adds "/youtube/" to path - for future ISA requirements to identify license provider --- .../kodion/constants/const_paths.py | 7 ++ .../kodion/network/http_server.py | 75 ++++++++++--------- .../youtube/helper/video_info.py | 16 ++-- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index b0ba36f27..cd14cf4bb 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -15,3 +15,10 @@ FAVORITES = 'kodion/favorites' WATCH_LATER = 'kodion/watch_later' HISTORY = 'kodion/playback_history' + +API = '/youtube/api' +API_SUBMIT = '/youtube/api/submit' +DRM = '/youtube/widevine' +IP = '/youtube/client_ip' +MPD = '/youtube/manifest/dash/' +PING = '/youtube/ping' diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 400b71177..0c5c9880e 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -26,7 +26,7 @@ xbmcgui, xbmcvfs, ) -from ..constants import ADDON_ID, TEMP_PATH +from ..constants import ADDON_ID, TEMP_PATH, paths from ..logger import log_debug from ..settings import Settings @@ -41,7 +41,7 @@ _server_requests = BaseRequestsClass() -class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): +class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): BASE_PATH = xbmcvfs.translatePath(TEMP_PATH) chunk_size = 1024 * 64 local_ranges = ( @@ -53,11 +53,9 @@ class YouTubeProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): '::1', ) - def __init__(self, request, client_address, server): + def __init__(self, *args, **kwargs): self.whitelist_ips = _settings.httpd_whitelist() - super(YouTubeProxyRequestHandler, self).__init__(request, - client_address, - server) + super(RequestHandler, self).__init__(*args, **kwargs) def connection_allowed(self): client_ip = self.client_address[0] @@ -71,7 +69,7 @@ def connection_allowed(self): if not conn_allowed: log_debug('HTTPServer: Connection from |{client_ip| not allowed'. format(client_ip=client_ip)) - elif self.path != '/ping': + elif self.path != paths.PING: log_debug(' '.join(log_lines)) return conn_allowed @@ -80,15 +78,15 @@ def do_GET(self): api_config_enabled = _settings.api_config_page() # Strip trailing slash if present - stripped_path = self.path.rstrip('/').lower() - if stripped_path != '/ping': + stripped_path = self.path.rstrip('/') + if stripped_path != paths.PING: log_debug('HTTPServer: GET Request uri path |{stripped_path}|' - .format(stripped_path=stripped_path)) + .format(stripped_path=self.path)) if not self.connection_allowed(): self.send_error(403) - elif stripped_path == '/client_ip': + elif stripped_path == paths.IP: client_json = json.dumps({"ip": "{ip}" .format(ip=self.client_address[0])}) self.send_response(200) @@ -97,9 +95,9 @@ def do_GET(self): self.end_headers() self.wfile.write(client_json.encode('utf-8')) - elif _settings.use_mpd_videos() and stripped_path.endswith('.mpd'): + elif self.path.startswith(paths.MPD): file_path = os.path.join(self.BASE_PATH, - self.path.strip('/').strip('\\')) + self.path[len(paths.MPD):]) file_chunk = True log_debug('HTTPServer: Request file path |{file_path}|' .format(file_path=file_path)) @@ -119,7 +117,7 @@ def do_GET(self): .format(proxy_path=self.path, file_path=file_path)) self.send_error(404, response) - elif api_config_enabled and stripped_path == '/api': + elif api_config_enabled and stripped_path == paths.API: html = self.api_config_page() html = html.encode('utf-8') @@ -131,7 +129,7 @@ def do_GET(self): for chunk in self.get_chunks(html): self.wfile.write(chunk) - elif api_config_enabled and stripped_path.startswith('/api_submit'): + elif api_config_enabled and self.path.startswith(paths.API_SUBMIT): xbmc.executebuiltin('Dialog.Close(addonsettings, true)') query = urlsplit(self.path).query @@ -186,7 +184,7 @@ def do_GET(self): for chunk in self.get_chunks(html): self.wfile.write(chunk) - elif stripped_path == '/ping': + elif stripped_path == paths.PING: self.send_error(204) else: @@ -199,9 +197,10 @@ def do_HEAD(self): if not self.connection_allowed(): self.send_error(403) - elif _settings.use_mpd_videos() and self.path.endswith('.mpd'): + + elif self.path.startswith(paths.MPD): file_path = os.path.join(self.BASE_PATH, - self.path.strip('/').strip('\\')) + self.path[len(paths.MPD):]) if not os.path.isfile(file_path): response = ('File Not Found: |{proxy_path}| -> |{file_path}|' .format(proxy_path=self.path, file_path=file_path)) @@ -212,6 +211,7 @@ def do_HEAD(self): self.send_header('Content-Length', str(os.path.getsize(file_path))) self.end_headers() + else: self.send_error(501) @@ -222,7 +222,8 @@ def do_POST(self): if not self.connection_allowed(): self.send_error(403) - elif self.path.startswith('/widevine'): + + elif self.path.startswith(paths.DRM): home = xbmcgui.Window(10000) lic_url = home.getProperty('-'.join((ADDON_ID, 'license_url'))) @@ -296,6 +297,7 @@ def do_POST(self): for chunk in self.get_chunks(response_body): self.wfile.write(chunk) + else: self.send_error(501) @@ -351,31 +353,31 @@ class Pages(object): - {title} - + {{title}} +
-
{header}
-
+
{{header}}
+ - +
- '''), + '''.format(action_url=paths.API_SUBMIT)), 'css': ''.join('\t\t\t'.expandtabs(2) + line for line in dedent(''' body { background: #141718; @@ -530,8 +532,7 @@ def get_http_server(address=None, port=None): address = _settings.httpd_listen(for_request=False, ip_address=address) port = _settings.httpd_port(port) try: - server = BaseHTTPServer.HTTPServer((address, port), - YouTubeProxyRequestHandler) + server = BaseHTTPServer.HTTPServer((address, port), RequestHandler) return server except socket_error as exc: log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|' @@ -547,7 +548,9 @@ def get_http_server(address=None, port=None): def is_httpd_live(address=None, port=None): address = _settings.httpd_listen(for_request=True, ip_address=address) port = _settings.httpd_port(port=port) - url = 'http://{address}:{port}/ping'.format(address=address, port=port) + url = 'http://{address}:{port}{path}'.format(address=address, + port=port, + path=paths.PING) try: response = _server_requests.request(url) result = response.status_code == 204 @@ -566,7 +569,9 @@ def is_httpd_live(address=None, port=None): def get_client_ip_address(address=None, port=None): address = _settings.httpd_listen(for_request=True, ip_address=address) port = _settings.httpd_port(port=port) - url = 'http://{address}:{port}/client_ip'.format(address=address, port=port) + url = 'http://{address}:{port}{path}'.format(address=address, + port=port, + path=paths.IP) response = _server_requests.request(url) ip_address = None if response.status_code == 200: diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 94864a301..d2072f770 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1217,9 +1217,10 @@ def _get_video_info(self): .format(url)) license_info = { 'url': url, - 'proxy': 'http://{0}:{1}/widevine||R{{SSM}}|'.format( - _settings.httpd_listen(for_request=True), - _settings.httpd_port() + 'proxy': 'http://{address}:{port}{path}||R{{SSM}}|'.format( + address=_settings.httpd_listen(for_request=True), + port=_settings.httpd_port(), + path=paths.DRM, ), 'token': self._access_token, } @@ -1855,9 +1856,10 @@ def _filter_group(previous_group, previous_stream, item): .format(file=filepath)) success = False if success: - return 'http://{0}:{1}/{2}.mpd'.format( - _settings.httpd_listen(for_request=True), - _settings.httpd_port(), - self.video_id + return 'http://{address}:{port}{path}{file}'.format( + address=_settings.httpd_listen(for_request=True), + port=_settings.httpd_port(), + path=paths.MPD, + file=filename, ), main_stream return None, None From 5a1b497e0c6a1ab78b1b9ff23553e0711d2a174e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:23:04 +1100 Subject: [PATCH 132/141] Misc tidy ups --- resources/lib/default.py | 2 +- resources/lib/youtube_authentication.py | 33 ++++--- .../kodion/abstract_provider.py | 40 +++++---- .../kodion/context/abstract_context.py | 2 +- .../kodion/context/xbmc/xbmc_context.py | 4 +- .../youtube_plugin/kodion/network/ip_api.py | 2 +- .../youtube_plugin/kodion/network/requests.py | 10 ++- .../kodion/plugin/xbmc/xbmc_runner.py | 42 +++++---- .../lib/youtube_plugin/kodion/service.py | 2 +- .../kodion/settings/abstract_settings.py | 2 +- .../kodion/sql_store/storage.py | 2 +- .../kodion/ui/xbmc/xbmc_context_ui.py | 12 +-- .../kodion/ui/xbmc/xbmc_items.py | 2 +- .../lib/youtube_plugin/kodion/utils/player.py | 2 +- .../youtube/client/login_client.py | 13 ++- .../youtube_plugin/youtube/client/youtube.py | 3 +- .../youtube/helper/signature/cipher.py | 2 +- .../helper/signature/json_script_engine.py | 2 +- .../lib/youtube_plugin/youtube/helper/tv.py | 32 +++++-- .../youtube/helper/url_to_item_converter.py | 6 +- .../youtube_plugin/youtube/helper/utils.py | 4 +- .../youtube/helper/yt_playlist.py | 23 +++-- .../youtube/helper/yt_specials.py | 6 +- .../youtube/helper/yt_subscriptions.py | 2 +- .../youtube_plugin/youtube/helper/yt_video.py | 6 +- .../lib/youtube_plugin/youtube/provider.py | 86 ++++++++++--------- 26 files changed, 202 insertions(+), 140 deletions(-) diff --git a/resources/lib/default.py b/resources/lib/default.py index ecb6f2d3a..ea6555469 100644 --- a/resources/lib/default.py +++ b/resources/lib/default.py @@ -10,8 +10,8 @@ from __future__ import absolute_import, division, unicode_literals -from youtube_plugin.kodion import runner from youtube_plugin import youtube +from youtube_plugin.kodion import runner __provider__ = youtube.Provider() runner.run(__provider__) diff --git a/resources/lib/youtube_authentication.py b/resources/lib/youtube_authentication.py index 6c7f0739c..531640692 100644 --- a/resources/lib/youtube_authentication.py +++ b/resources/lib/youtube_authentication.py @@ -9,17 +9,22 @@ from __future__ import absolute_import, division, unicode_literals -from youtube_plugin.youtube.provider import Provider from youtube_plugin.kodion.constants import ADDON_ID from youtube_plugin.kodion.context import Context from youtube_plugin.youtube.helper import yt_login +from youtube_plugin.youtube.provider import Provider +from youtube_plugin.youtube.youtube_exceptions import LoginException -# noinspection PyUnresolvedReferences -from youtube_plugin.youtube.youtube_exceptions import LoginException # NOQA +__all__ = ( + 'LoginException', + 'reset_access_tokens', + 'sign_in', + 'sign_out', +) -SIGN_IN = 'in' -SIGN_OUT = 'out' +_SIGN_IN = 'in' +_SIGN_OUT = 'out' def __add_new_developer(addon_id): @@ -38,7 +43,7 @@ def __add_new_developer(addon_id): context.log_debug('Creating developer user: |%s|' % addon_id) -def __auth(addon_id, mode=SIGN_IN): +def __auth(addon_id, mode=_SIGN_IN): """ :param addon_id: id of the add-on being signed in @@ -53,28 +58,28 @@ def __auth(addon_id, mode=SIGN_IN): provider = Provider() context = Context(params={'addon_id': addon_id}) - _ = provider.get_client(context=context) # NOQA + _ = provider.get_client(context=context) logged_in = provider.is_logged_in() - if mode == SIGN_IN: + if mode == _SIGN_IN: if logged_in: return True else: provider.reset_client() yt_login.process(mode, provider, context, sign_out_refresh=False) - elif mode == SIGN_OUT: + elif mode == _SIGN_OUT: if not logged_in: return True else: provider.reset_client() try: yt_login.process(mode, provider, context, sign_out_refresh=False) - except: + except LoginException: reset_access_tokens(addon_id) else: raise Exception('Unknown mode: |%s|' % mode) - _ = provider.get_client(context=context) # NOQA - if mode == SIGN_IN: + _ = provider.get_client(context=context) + if mode == _SIGN_IN: return provider.is_logged_in() else: return not provider.is_logged_in() @@ -115,7 +120,7 @@ def sign_in(addon_id): :return: boolean, True when signed in """ - return __auth(addon_id, mode=SIGN_IN) + return __auth(addon_id, mode=_SIGN_IN) def sign_out(addon_id): @@ -146,7 +151,7 @@ def sign_out(addon_id): :return: boolean, True when signed out """ - return __auth(addon_id, mode=SIGN_OUT) + return __auth(addon_id, mode=_SIGN_OUT) def reset_access_tokens(addon_id): diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index e8983dd8f..0fbbb337f 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -187,7 +187,8 @@ def _internal_favorite(context, re_match): return False - def _internal_watch_later(self, context, re_match): + @staticmethod + def _internal_watch_later(context, re_match): params = context.get_params() command = re_match.group('command') if not command: @@ -249,11 +250,13 @@ def _internal_search(self, context, re_match): command = re_match.group('command') search_history = context.get_search_history() + if command == 'remove': query = params['q'] search_history.remove(query) ui.refresh_container() return True + if command == 'rename': query = params['q'] result, new_query = ui.on_keyboard_input( @@ -263,10 +266,27 @@ def _internal_search(self, context, re_match): search_history.rename(query, new_query) ui.refresh_container() return True + if command == 'clear': search_history.clear() ui.refresh_container() return True + + if command == 'query': + incognito = context.get_param('incognito', False) + channel_id = context.get_param('channel_id', '') + query = params['q'] + query = to_unicode(query) + + if not incognito and not channel_id: + try: + search_history.update(query) + except: + pass + if isinstance(query, bytes): + query = query.decode('utf-8') + return self.on_search(query, context, re_match) + if command == 'input': self.data_cache = context @@ -307,20 +327,6 @@ def _internal_search(self, context, re_match): query = query.decode('utf-8') return self.on_search(query, context, re_match) - if command == 'query': - incognito = context.get_param('incognito', False) - channel_id = context.get_param('channel_id', '') - query = params['q'] - query = to_unicode(query) - - if not incognito and not channel_id: - try: - search_history.update(query) - except: - pass - if isinstance(query, bytes): - query = query.decode('utf-8') - return self.on_search(query, context, re_match) context.set_content_type(content.VIDEOS) result = [] @@ -343,10 +349,6 @@ def _internal_search(self, context, re_match): ) result.append(search_history_item) - if search_history.is_empty(): - # context.execute('RunPlugin(%s)' % context.create_uri([constants.paths.SEARCH, 'input'])) - pass - return result, {self.RESULT_CACHE_TO_DISC: False} def handle_exception(self, context, exception_to_handle): diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 9cd7fda80..69f4f7b46 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -223,7 +223,7 @@ def create_uri(self, path='/', params=None): uri = "%s://%s/" % ('plugin', str(self._plugin_id)) if params: - uri = '?'.join([uri, urlencode(params)]) + uri = '?'.join((uri, urlencode(params))) return uri diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 4c9e88aa8..b3c84e1e0 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -105,7 +105,7 @@ class XbmcContext(AbstractContext): 'httpd.not.running': 30699, 'inputstreamhelper.is_installed': 30625, 'isa.enable.confirm': 30579, - 'key.requirement.notification': 30731, + 'key.requirement': 30731, 'latest_videos': 30109, 'library': 30103, 'liked.video': 30716, @@ -280,7 +280,7 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id='', override if num_args > 2: params = sys.argv[2][1:] if params: - self._uri = '?'.join([self._uri, params]) + self._uri = '?'.join((self._uri, params)) self.parse_params(dict(parse_qsl(params))) if num_args > 3 and sys.argv[3].lower() == 'resume:true': diff --git a/resources/lib/youtube_plugin/kodion/network/ip_api.py b/resources/lib/youtube_plugin/kodion/network/ip_api.py index ed009455e..93a2cd2be 100644 --- a/resources/lib/youtube_plugin/kodion/network/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/network/ip_api.py @@ -25,7 +25,7 @@ def response(self): return self._response def locate_requester(self): - request_url = '/'.join([self._base_url, 'json']) + request_url = '/'.join((self._base_url, 'json')) response = self.request(request_url) self._response = response.json() diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 2d4c911d1..fe3912dd3 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -22,6 +22,11 @@ from ..settings import Settings +__all__ = ( + 'BaseRequestsClass', + 'InvalidJSONError' +) + _settings = Settings(xbmcaddon.Addon(id=ADDON_ID)) @@ -95,7 +100,7 @@ def request(self, url, method='GET', stream=stream, verify=verify, cert=cert, - json=json,) + json=json) if response_hook: if response_hook_kwargs is None: response_hook_kwargs = {} @@ -163,6 +168,3 @@ def request(self, url, method='GET', raise self._default_exc(error_title)(exc) return response - - -__all__ = ('BaseRequestsClass', 'InvalidJSONError') diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py index 3d5282dc9..6300f1bd4 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_runner.py @@ -59,9 +59,7 @@ def run(self, provider, context): self.settings = context.get_settings() - result = results[0] - options = {} - options.update(results[1]) + result, options = results if isinstance(result, bool): xbmcplugin.endOfDirectory(self.handle, succeeded=result) @@ -78,10 +76,14 @@ def run(self, provider, context): elif isinstance(result, (list, tuple)): item_count = len(result) items = [ - self._add_directory(item, show_fanart) if isinstance(item, DirectoryItem) - else self._add_video(context, item) if isinstance(item, VideoItem) - else self._add_audio(context, item) if isinstance(item, AudioItem) - else self._add_image(item, show_fanart) if isinstance(item, ImageItem) + self._add_directory(item, show_fanart) + if isinstance(item, DirectoryItem) + else self._add_video(context, item) + if isinstance(item, VideoItem) + else self._add_audio(context, item) + if isinstance(item, AudioItem) + else self._add_image(item, show_fanart) + if isinstance(item, ImageItem) else None for item in result ] @@ -135,7 +137,7 @@ def _add_directory(directory_item, show_fanart=False): item_info = info_labels.create_from_item(directory_item) xbmc_items.set_info_tag(item, item_info, 'video') - # only set fanart is enabled + # only set fanart if enabled if show_fanart: fanart = directory_item.get_fanart() if fanart: @@ -143,9 +145,11 @@ def _add_directory(directory_item, show_fanart=False): item.setArt(art) - if directory_item.get_context_menu() is not None: - item.addContextMenuItems(directory_item.get_context_menu(), - replaceItems=directory_item.replace_context_menu()) + context_menu = directory_item.get_context_menu() + if context_menu is not None: + item.addContextMenuItems( + context_menu, replaceItems=directory_item.replace_context_menu() + ) item.setPath(directory_item.get_uri()) @@ -154,8 +158,10 @@ def _add_directory(directory_item, show_fanart=False): if directory_item.next_page: item.setProperty('specialSort', 'bottom') - if directory_item.get_channel_subscription_id(): # make channel_subscription_id property available for keymapping - item.setProperty('channel_subscription_id', directory_item.get_channel_subscription_id()) + # make channel_subscription_id property available for keymapping + subscription_id = directory_item.get_channel_subscription_id() + if subscription_id: + item.setProperty('channel_subscription_id', subscription_id) return directory_item.get_uri(), item, is_folder @@ -180,10 +186,14 @@ def _add_image(image_item, show_fanart=False): item.setArt(art) - if image_item.get_context_menu() is not None: - item.addContextMenuItems(image_item.get_context_menu(), replaceItems=image_item.replace_context_menu()) + context_menu = image_item.get_context_menu() + if context_menu is not None: + item.addContextMenuItems( + context_menu, replaceItems=image_item.replace_context_menu() + ) - item.setInfo(type='picture', infoLabels=info_labels.create_from_item(image_item)) + item.setInfo(type='picture', + infoLabels=info_labels.create_from_item(image_item)) item.setPath(image_item.get_uri()) return image_item.get_uri(), item, False diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index 609ab63d6..4bb49a8ca 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -10,8 +10,8 @@ from __future__ import absolute_import, division, unicode_literals -from datetime import datetime import time +from datetime import datetime from .context import Context from .utils import YouTubeMonitor, YouTubePlayer diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 0375ce2e1..3dac23250 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -275,7 +275,7 @@ def set_location(self, value): self.set_string(settings.LOCATION, value) def get_location_radius(self): - return ''.join([str(self.get_int(settings.LOCATION_RADIUS, 500)), 'km']) + return ''.join((str(self.get_int(settings.LOCATION_RADIUS, 500)), 'km')) def get_play_count_min_percent(self): return self.get_int(settings.PLAY_COUNT_MIN_PERCENT, 0) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 41faa7d50..8174b5240 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -133,7 +133,7 @@ class Storage(object): def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): self._filename = filename if not self._filename.endswith('.sqlite'): - self._filename = ''.join([self._filename, '.sqlite']) + self._filename = ''.join((self._filename, '.sqlite')) self._db = None self._cursor = None self._max_item_count = max_item_count diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index fddb052aa..718805ff0 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -12,9 +12,9 @@ from .xbmc_progress_dialog import XbmcProgressDialog, XbmcProgressDialogBG from ..abstract_context_ui import AbstractContextUI -from ... import utils from ...compatibility import xbmc, xbmcgui from ...constants import ADDON_ID, ADDON_PATH +from ...utils import to_unicode class XbmcContextUI(AbstractContextUI): @@ -41,15 +41,15 @@ def on_keyboard_input(self, title, default='', hidden=False): keyboard = xbmc.Keyboard(default, title, hidden) keyboard.doModal() if keyboard.isConfirmed() and keyboard.getText(): - text = utils.to_unicode(keyboard.getText()) + text = to_unicode(keyboard.getText()) return True, text return False, '' # Starting with Gotham (13.X > ...) dialog = xbmcgui.Dialog() - result = dialog.input(title, utils.to_unicode(default), type=xbmcgui.INPUT_ALPHANUM) + result = dialog.input(title, to_unicode(default), type=xbmcgui.INPUT_ALPHANUM) if result: - text = utils.to_unicode(result) + text = to_unicode(result) return True, text return False, '' @@ -71,11 +71,11 @@ def on_ok(self, title, text): return dialog.ok(title, text) def on_remove_content(self, content_name): - text = self._context.localize('content.remove') % utils.to_unicode(content_name) + text = self._context.localize('content.remove') % to_unicode(content_name) return self.on_yes_no_input(self._context.localize('content.remove.confirm'), text) def on_delete_content(self, content_name): - text = self._context.localize('content.delete') % utils.to_unicode(content_name) + text = self._context.localize('content.delete') % to_unicode(content_name) return self.on_yes_no_input(self._context.localize('content.delete.confirm'), text) def on_select(self, title, items=None): diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py index d89cfba8f..5ede3e8c1 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_items.py @@ -114,7 +114,7 @@ def video_playback_item(context, video_item): mime_type = mime_type.replace('%2F', '/') if not alternative_player and headers and uri.startswith('http'): - video_item.set_uri('|'.join([uri, headers])) + video_item.set_uri('|'.join((uri, headers))) list_item = xbmcgui.ListItem(**kwargs) if mime_type: diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player.py index f68f8ec00..b9ccee0f2 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player.py @@ -385,7 +385,7 @@ def run(self): do_refresh = playlist.size() < 2 or playlist.getposition() == -1 if (do_refresh and settings.get_bool('youtube.post.play.refresh', False) and not xbmc.getInfoLabel('Container.FolderPath').startswith( - self._context.create_uri(['kodion', 'search', 'input']) + self._context.create_uri(('kodion', 'search', 'input')) )): # don't refresh search input it causes request for new input, # (Container.Update in abstract_provider /kodion/search/input/ diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 76964ea66..ca4652c42 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -62,14 +62,19 @@ class LoginClient(YouTubeRequestClient): 'developer': developer_keys } - def __init__(self, config=None, language='en-US', region='', - access_token='', access_token_tv=''): + def __init__(self, + config=None, + language='en_US', + region='', + access_token='', + access_token_tv=''): self._config = self.CONFIGS['main'] if config is None else config self._config_tv = self.CONFIGS['youtube-tv'] # the default language is always en_US (like YouTube on the WEB) if not language: language = 'en_US' - language = language.replace('-', '_') + else: + language = language.replace('-', '_') self._language = language self._region = region @@ -275,7 +280,7 @@ def authenticate(self, username, password): post_data = { 'device_country': self._region.lower(), 'operatorCountry': self._region.lower(), - 'lang': self._language.replace('-', '_'), + 'lang': self._language, 'sdk_version': '19', # 'google_play_services_version': '6188034', 'accountType': 'HOSTED_OR_GOOGLE', diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index c7e684622..f6f9317b5 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1356,7 +1356,8 @@ def perform_v1_tv_request(self, method='GET', headers=None, path=None, self._context.log_debug('[data] v1 response: |{0.status_code}|\n' '\theaders: |{0.headers}|'.format(result)) - if result.headers.get('content-type', '').startswith('application/json'): + result_type = result.headers.get('content-type') + if result_type and result_type.startswith('application/json'): try: return result.json() except ValueError: diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py index b031daf16..a8549afcd 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py @@ -164,7 +164,7 @@ def _get_object_function(self, object_name, function_name, javascript): _object_body = _object_body.split('},') for _function in _object_body: if not _function.endswith('}'): - _function = ''.join([_function, '}']) + _function = ''.join((_function, '}')) _function = _function.strip() match = re.match(r'(?P[^:]*):function\((?P[^)]*)\){(?P[^}]+)}', _function) diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py b/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py index eac6dc8f7..90fb3a9d2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/json_script_engine.py @@ -18,7 +18,7 @@ def execute(self, signature): _actions = self._json_script['actions'] for action in _actions: - func = ''.join(['_', action['func']]) + func = ''.join(('_', action['func'])) params = action['params'] if func == '_return': diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index cd30681f5..6c532038c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -36,8 +36,9 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): for item in items: channel = item['channel'].lower() channel = channel.replace(',', '') - if not do_filter or (do_filter and (not black_list) and (channel in filter_list)) or \ - (do_filter and black_list and (channel not in filter_list)): + if (not do_filter + or (black_list and channel not in filter_list) + or (not black_list and channel in filter_list)): video_id = item['id'] item_params['video_id'] = video_id item_uri = context.create_uri(['play'], item_params) @@ -51,7 +52,11 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): use_play_data = not incognito and context.get_settings().use_local_history() channel_item_dict = {} - utils.update_video_infos(provider, context, video_id_dict, channel_items_dict=channel_item_dict, use_play_data=use_play_data) + utils.update_video_infos(provider, + context, + video_id_dict, + channel_items_dict=channel_item_dict, + use_play_data=use_play_data) utils.update_fanarts(provider, context, channel_item_dict) if context.get_settings().hide_short_videos(): @@ -96,7 +101,11 @@ def tv_videos_to_items(provider, context, json_data): use_play_data = not incognito and context.get_settings().use_local_history() channel_item_dict = {} - utils.update_video_infos(provider, context, video_id_dict, channel_items_dict=channel_item_dict, use_play_data=use_play_data) + utils.update_video_infos(provider, + context, + video_id_dict, + channel_items_dict=channel_item_dict, + use_play_data=use_play_data) utils.update_fanarts(provider, context, channel_item_dict) if context.get_settings().hide_short_videos(): @@ -134,16 +143,25 @@ def saved_playlists_to_items(provider, context, json_data): image = utils.get_thumbnail(thumb_size, item.get('thumbnails', {})) if channel_id: - item_uri = context.create_uri(['channel', channel_id, 'playlist', playlist_id], item_params) + item_uri = context.create_uri( + ['channel', channel_id, 'playlist', playlist_id], + item_params, + ) else: - item_uri = context.create_uri(['playlist', playlist_id], item_params) + item_uri = context.create_uri( + ['playlist', playlist_id], + item_params, + ) playlist_item = DirectoryItem(title, item_uri, image=image) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_item channel_items_dict = {} - utils.update_playlist_infos(provider, context, playlist_id_dict, channel_items_dict) + utils.update_playlist_infos(provider, + context, + playlist_id_dict, + channel_items_dict) utils.update_fanarts(provider, context, channel_items_dict) # next page diff --git a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py index 90ddfa73e..0bde8abe2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_to_item_converter.py @@ -40,7 +40,7 @@ def __init__(self, flatten=True): self._channel_items = [] self._channel_ids = [] - def add_url(self, url, provider, context): + def add_url(self, url, context): parsed_url = urlsplit(url) if parsed_url.hostname.lower() not in self.VALID_HOSTNAMES: context.log_debug('Unknown hostname "{0}" in url "{1}"'.format( @@ -119,9 +119,9 @@ def add_url(self, url, provider, context): else: context.log_debug('No items found in url "{0}"'.format(url)) - def add_urls(self, urls, provider, context): + def add_urls(self, urls, context): for url in urls: - self.add_url(url, provider, context) + self.add_url(url, context) def get_items(self, provider, context, skip_title=False): result = [] diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 3ff8505c7..6d0d7e1f2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -546,7 +546,7 @@ def update_video_infos(provider, context, video_id_dict, if not image: image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) if image.endswith('_live.jpg'): - image = ''.join([image, '?ct=', thumb_stamp]) + image = ''.join((image, '?ct=', thumb_stamp)) video_item.set_image(image) # update channel mapping @@ -702,7 +702,7 @@ def update_play_info(provider, context, video_id, video_item, video_stream, meta_data.get('images', {})) if image: if video_item.live: - image = ''.join([image, '?ct=', get_thumb_timestamp()]) + image = ''.join((image, '?ct=', get_thumb_timestamp())) video_item.set_image(image) if 'headers' in video_stream: diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 6939c6013..526a0af28 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -171,8 +171,11 @@ def _process_select_playlist(provider, context): items = [] if current_page == 1: # create playlist - items.append((ui.bold(context.localize('playlist.create')), '', - 'playlist.create', context.create_resource_path('media', 'playlist.png'))) + items.append(( + ui.bold(context.localize('playlist.create')), '', + 'playlist.create', + context.create_resource_path('media', 'playlist.png') + )) # add the 'Watch Later' playlist resource_manager = provider.get_resource_manager(context) @@ -180,17 +183,25 @@ def _process_select_playlist(provider, context): if 'watchLater' in my_playlists: watch_later_playlist_id = context.get_access_manager().get_watch_later_id() if watch_later_playlist_id: - items.append((ui.bold(context.localize('watch_later')), '', - watch_later_playlist_id, context.create_resource_path('media', 'watch_later.png'))) + items.append(( + ui.bold(context.localize('watch_later')), '', + watch_later_playlist_id, + context.create_resource_path('media', 'watch_later.png') + )) + default_thumb = context.create_resource_path('media', 'playlist.png') for playlist in playlists: snippet = playlist.get('snippet', {}) title = snippet.get('title', '') description = snippet.get('description', '') - thumbnail = snippet.get('thumbnails', {}).get('default', {}).get('url', context.create_resource_path('media', 'playlist.png')) + thumbnail = snippet.get('thumbnails', {}).get('default', {}) playlist_id = playlist.get('id', '') if title and playlist_id: - items.append((title, description, playlist_id, thumbnail)) + items.append(( + title, description, + playlist_id, + thumbnail.get('url') or default_thumb + )) if page_token: items.append((ui.bold(context.localize('next_page')).replace('%d', str(current_page + 1)), '', diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 544c70120..ec3f19393 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -148,7 +148,7 @@ def _extract_urls(video_id): resource_manager = provider.get_resource_manager(context) - video_data = resource_manager.get_videos([video_id]) + video_data = resource_manager.get_videos((video_id, )) yt_item = video_data[video_id] if not yt_item or 'snippet' not in yt_item: context.get_ui().on_ok( @@ -179,7 +179,7 @@ def _extract_urls(video_id): context.sleep(50) url_to_item_converter = UrlToItemConverter() - url_to_item_converter.add_urls(res_urls, provider, context) + url_to_item_converter.add_urls(res_urls, context) result = url_to_item_converter.get_items(provider, context) progress_dialog.close() @@ -295,7 +295,7 @@ def process(category, provider, context): and category in ['new_uploaded_videos_tv', 'new_uploaded_videos_tv_filtered', 'disliked_videos']): - return UriItem(context.create_uri(['sign', 'in'])) + return UriItem(context.create_uri(('sign', 'in'))) if category == 'related_videos': return _process_related_videos(provider, context) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index 39e81309b..9a7a774c3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -83,7 +83,7 @@ def process(method, provider, context): # we need a login _ = provider.get_client(context) if not provider.is_logged_in(): - return UriItem(context.create_uri(['sign', 'in'])) + return UriItem(context.create_uri(('sign', 'in'))) if method == 'list': result.extend(_process_list(provider, context)) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index aa6086e92..b1e2511a7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -96,8 +96,8 @@ def _process_more_for_video(context): items = [] - is_logged_in = context.get_param('logged_in', '0') - if is_logged_in == '1': + logged_in = context.get_param('logged_in', '0') + if logged_in == '1': # add video to a playlist items.append((context.localize('video.add_to_playlist'), 'RunPlugin(%s)' % context.create_uri(['playlist', 'select', 'playlist'], {'video_id': video_id}))) @@ -111,7 +111,7 @@ def _process_more_for_video(context): 'Container.Update(%s)' % context.create_uri(['special', 'description_links'], {'video_id': video_id}))]) - if is_logged_in == '1': + if logged_in == '1': # rate a video refresh_container = context.get_param('refresh_container', '0') items.append((context.localize('video.rate'), diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 70f694ca4..1b2a95f1a 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -54,7 +54,7 @@ def __init__(self): self._resource_manager = None self._client = None - self._is_logged_in = False + self._logged_in = False self.yt_video = yt_video @@ -65,7 +65,7 @@ def get_wizard_steps(self, context): return [(yt_setup_wizard.process, [self, context])] def is_logged_in(self): - return self._is_logged_in + return self._logged_in @staticmethod def get_dev_config(context, addon_id, dev_configs): @@ -121,7 +121,7 @@ def get_client(self, context): items_per_page = settings.get_items_per_page() - language = settings.get_string('youtube.language', 'en-US') + language = settings.get_string('youtube.language', 'en_US') region = settings.get_string('youtube.region', 'US') api_last_origin = access_manager.get_last_origin() @@ -240,8 +240,8 @@ def get_client(self, context): self.get_resource_manager(context).clear() # in debug log the login status - self._is_logged_in = len(access_tokens) == 2 - context.log_debug('User is logged in' if self._is_logged_in else + self._logged_in = len(access_tokens) == 2 + context.log_debug('User is logged in' if self._logged_in else 'User is not logged in') if not access_tokens: @@ -268,7 +268,7 @@ def on_uri2addon(self, context, re_match): resolver = UrlResolver(context) res_url = resolver.resolve(uri) url_converter = UrlToItemConverter(flatten=True) - url_converter.add_url(res_url, self, context) + url_converter.add_url(res_url, context) items = url_converter.get_items(self, context, skip_title=True) if items: return items[0] @@ -287,7 +287,6 @@ def on_uri2addon(self, context, re_match): @RegisterProviderPath('^(?:/channel/(?P[^/]+))?/playlist/(?P[^/]+)/$') def _on_playlist(self, context, re_match): self.set_content_type(context, content.VIDEOS) - client = self.get_client(context) resource_manager = self.get_resource_manager(context) batch_id = (re_match.group('playlist_id'), @@ -362,7 +361,12 @@ def _on_channel_live(self, context, re_match): safe_search = context.get_settings().safe_search() # no caching - json_data = self.get_client(context).search(q='', search_type='video', event_type='live', channel_id=channel_id, page_token=page_token, safe_search=safe_search) + json_data = self.get_client(context).search(q='', + search_type='video', + event_type='live', + channel_id=channel_id, + page_token=page_token, + safe_search=safe_search) if not json_data: return False result.extend(v3.response_to_items(self, context, json_data)) @@ -390,7 +394,7 @@ def _on_channel(self, context, re_match): if (method == 'channel' and channel_id and channel_id.lower() == 'property' and listitem_channel_id and listitem_channel_id.lower().startswith(('mine', 'uc'))): - context.execute('Container.Update(%s)' % create_uri(['channel', listitem_channel_id])) # redirect if keymap, without redirect results in 'invalid handle -1' + context.execute('Container.Update(%s)' % create_uri(('channel', listitem_channel_id))) # redirect if keymap, without redirect results in 'invalid handle -1' if method == 'channel' and not channel_id: return False @@ -427,7 +431,7 @@ def _on_channel(self, context, re_match): if method == 'user': return False - channel_fanarts = resource_manager.get_fanarts([channel_id]) + channel_fanarts = resource_manager.get_fanarts((channel_id, )) params = context.get_params() page = params.get('page', 1) @@ -1272,13 +1276,13 @@ def on_root(self, context, re_match): if not logged_in and settings.get_bool('youtube.folder.sign.in.show', True): sign_in_item = DirectoryItem( ui.bold(localize('sign.in')), - create_uri(['sign', 'in']), + create_uri(('sign', 'in')), image='{media}/sign_in.png', action=True ) result.append(sign_in_item) - if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions.show', True): + if logged_in and settings.get_bool('youtube.folder.my_subscriptions.show', True): # my subscription # clear cache @@ -1287,16 +1291,16 @@ def on_root(self, context, re_match): my_subscriptions_item = DirectoryItem( ui.bold(localize('my_subscriptions')), - create_uri(['special', 'new_uploaded_videos_tv']), + create_uri(('special', 'new_uploaded_videos_tv')), image='{media}/new_uploads.png', ) result.append(my_subscriptions_item) - if self.is_logged_in() and settings.get_bool('youtube.folder.my_subscriptions_filtered.show', True): + if logged_in and settings.get_bool('youtube.folder.my_subscriptions_filtered.show', True): # my subscriptions filtered my_subscriptions_filtered_item = DirectoryItem( localize('my_subscriptions.filtered'), - create_uri(['special', 'new_uploaded_videos_tv_filtered']), + create_uri(('special', 'new_uploaded_videos_tv_filtered')), image='{media}/new_uploads.png', ) result.append(my_subscriptions_filtered_item) @@ -1304,12 +1308,12 @@ def on_root(self, context, re_match): access_manager = context.get_access_manager() # Recommendations - if self.is_logged_in() and settings.get_bool('youtube.folder.recommendations.show', True): + if logged_in and settings.get_bool('youtube.folder.recommendations.show', True): watch_history_playlist_id = access_manager.get_watch_history_id() if watch_history_playlist_id != 'HL': recommendations_item = DirectoryItem( localize('recommendations'), - create_uri(['special', 'recommendations']), + create_uri(('special', 'recommendations')), image='{media}/popular.png', ) result.append(recommendations_item) @@ -1318,7 +1322,7 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.popular_right_now.show', True): what_to_watch_item = DirectoryItem( localize('popular_right_now'), - create_uri(['special', 'popular_right_now']), + create_uri(('special', 'popular_right_now')), image='{media}/popular.png', ) result.append(what_to_watch_item) @@ -1351,7 +1355,7 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.my_location.show', True) and settings.get_location(): my_location_item = DirectoryItem( localize('my_location'), - create_uri(['location', 'mine']), + create_uri(('location', 'mine')), image='{media}/location.png', ) result.append(my_location_item) @@ -1360,7 +1364,7 @@ def on_root(self, context, re_match): if logged_in and settings.get_bool('youtube.folder.my_channel.show', True): my_channel_item = DirectoryItem( localize('my_channel'), - create_uri(['channel', 'mine']), + create_uri(('channel', 'mine')), image='{media}/channel.png', ) result.append(my_channel_item) @@ -1368,10 +1372,10 @@ def on_root(self, context, re_match): # watch later if settings.get_bool('youtube.folder.watch_later.show', True): playlist_id = logged_in and access_manager.get_watch_later_id() - if playlist_id and playlist_id != 'HL': + if playlist_id: watch_later_item = DirectoryItem( localize('watch_later'), - create_uri(['channel', 'mine', 'playlist', playlist_id]), + create_uri(('channel', 'mine', 'playlist', playlist_id)), image='{media}/watch_later.png', ) context_menu = [ @@ -1384,7 +1388,7 @@ def on_root(self, context, re_match): else: watch_history_item = DirectoryItem( localize('watch_later'), - create_uri([paths.WATCH_LATER, 'list']), + create_uri((paths.WATCH_LATER, 'list')), image='{media}/watch_later.png', ) result.append(watch_history_item) @@ -1396,7 +1400,7 @@ def on_root(self, context, re_match): if 'likes' in playlists: liked_videos_item = DirectoryItem( localize('video.liked'), - create_uri(['channel', 'mine', 'playlist', playlists['likes']]), + create_uri(('channel', 'mine', 'playlist', playlists['likes'])), image='{media}/likes.png', ) context_menu = [ @@ -1411,7 +1415,7 @@ def on_root(self, context, re_match): if logged_in and settings.get_bool('youtube.folder.disliked_videos.show', True): disliked_videos_item = DirectoryItem( localize('video.disliked'), - create_uri(['special', 'disliked_videos']), + create_uri(('special', 'disliked_videos')), image='{media}/dislikes.png', ) result.append(disliked_videos_item) @@ -1422,7 +1426,7 @@ def on_root(self, context, re_match): if playlist_id and playlist_id != 'HL': watch_history_item = DirectoryItem( localize('history'), - create_uri(['channel', 'mine', 'playlist', playlist_id]), + create_uri(('channel', 'mine', 'playlist', playlist_id)), image='{media}/history.png', ) context_menu = [ @@ -1444,7 +1448,7 @@ def on_root(self, context, re_match): if logged_in and settings.get_bool('youtube.folder.playlists.show', True): playlists_item = DirectoryItem( localize('playlists'), - create_uri(['channel', 'mine', 'playlists']), + create_uri(('channel', 'mine', 'playlists')), image='{media}/playlist.png', ) result.append(playlists_item) @@ -1453,7 +1457,7 @@ def on_root(self, context, re_match): if logged_in and settings.get_bool('youtube.folder.saved.playlists.show', True): playlists_item = DirectoryItem( localize('saved.playlists'), - create_uri(['special', 'saved_playlists']), + create_uri(('special', 'saved_playlists')), image='{media}/playlist.png', ) result.append(playlists_item) @@ -1462,7 +1466,7 @@ def on_root(self, context, re_match): if logged_in and settings.get_bool('youtube.folder.subscriptions.show', True): subscriptions_item = DirectoryItem( localize('subscriptions'), - create_uri(['subscriptions', 'list']), + create_uri(('subscriptions', 'list')), image='{media}/channels.png', ) result.append(subscriptions_item) @@ -1471,7 +1475,7 @@ def on_root(self, context, re_match): if logged_in and settings.get_bool('youtube.folder.browse_channels.show', True): browse_channels_item = DirectoryItem( localize('browse_channels'), - create_uri(['special', 'browse_channels']), + create_uri(('special', 'browse_channels')), image='{media}/browse_channels.png', ) result.append(browse_channels_item) @@ -1480,7 +1484,7 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.completed.live.show', True): live_events_item = DirectoryItem( localize('live.completed'), - create_uri(['special', 'completed_live']), + create_uri(('special', 'completed_live')), image='{media}/live.png', ) result.append(live_events_item) @@ -1489,7 +1493,7 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.upcoming.live.show', True): live_events_item = DirectoryItem( localize('live.upcoming'), - create_uri(['special', 'upcoming_live']), + create_uri(('special', 'upcoming_live')), image='{media}/live.png', ) result.append(live_events_item) @@ -1498,7 +1502,7 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.live.show', True): live_events_item = DirectoryItem( localize('live'), - create_uri(['special', 'live']), + create_uri(('special', 'live')), image='{media}/live.png', ) result.append(live_events_item) @@ -1507,7 +1511,7 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.switch.user.show', True): switch_user_item = DirectoryItem( localize('user.switch'), - create_uri(['users', 'switch']), + create_uri(('users', 'switch')), image='{media}/channel.png', action=True, ) @@ -1517,7 +1521,7 @@ def on_root(self, context, re_match): if logged_in and settings.get_bool('youtube.folder.sign.out.show', True): sign_out_item = DirectoryItem( localize('sign.out'), - create_uri(['sign', 'out']), + create_uri(('sign', 'out')), image='{media}/sign_out.png', action=True, ) @@ -1526,7 +1530,7 @@ def on_root(self, context, re_match): if settings.get_bool('youtube.folder.settings.show', True): settings_menu_item = DirectoryItem( localize('settings'), - create_uri(['config', 'youtube']), + create_uri(('config', 'youtube')), image='{media}/settings.png', action=True, ) @@ -1590,8 +1594,10 @@ def handle_exception(self, context, exception_to_handle): context.log_error('%s: %s' % (title, log_message)) if error == 'deleted_client': - message = context.localize('key.requirement.notification') - context.get_access_manager().update_access_token(access_token='', refresh_token='') + message = context.localize('key.requirement') + context.get_access_manager().update_access_token( + access_token='', refresh_token='' + ) ok_dialog = True if error == 'invalid_client': @@ -1605,7 +1611,9 @@ def handle_exception(self, context, exception_to_handle): if ok_dialog: context.get_ui().on_ok(title, message) else: - context.get_ui().show_notification(message, title, time_ms=message_timeout) + context.get_ui().show_notification(message, + title, + time_ms=message_timeout) return False From 9be2f829937d205385997c68b71353dcd73859e8 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:30:02 +1100 Subject: [PATCH 133/141] Update Auto remove from Watch Later for local list - Rename classes and module - Misc tidy up - Prevent potential indefinite lockup on Thread.join() --- .../kodion/context/xbmc/xbmc_context.py | 2 +- .../lib/youtube_plugin/kodion/service.py | 4 +- .../youtube_plugin/kodion/utils/__init__.py | 4 +- .../utils/{player.py => player_monitor.py} | 176 +++++++++--------- 4 files changed, 89 insertions(+), 97 deletions(-) rename resources/lib/youtube_plugin/kodion/utils/{player.py => player_monitor.py} (80%) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index b3c84e1e0..9e6e30525 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -49,6 +49,7 @@ class XbmcContext(AbstractContext): 'api.secret': 30203, 'archive': 30105, 'are_you_sure': 30703, + 'auto_remove_watch_later': 30515, 'browse_channels': 30512, 'cache.data': 30687, 'cache.function': 30557, @@ -163,7 +164,6 @@ class XbmcContext(AbstractContext): 'search.title': 30102, 'select.listen.ip': 30644, 'select_video_quality': 30010, - 'setting.auto_remove_watch_later': 30515, 'settings': 30577, 'setup.view_default': 30027, 'setup.view_videos': 30028, diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index 4bb49a8ca..d2a04d44b 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -14,7 +14,7 @@ from datetime import datetime from .context import Context -from .utils import YouTubeMonitor, YouTubePlayer +from .utils import PlayerMonitor, YouTubeMonitor from ..youtube.provider import Provider @@ -56,7 +56,7 @@ def run(): context.log_debug('YouTube service initialization...') monitor = YouTubeMonitor() - player = YouTubePlayer(provider=Provider(), context=context) + player = PlayerMonitor(provider=Provider(), context=context) # wipe add-on temp folder on updates/restarts (subtitles, and mpd files) monitor.remove_temp_dir() diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 19b0e80e7..f6878edc1 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -29,11 +29,12 @@ to_unicode, ) from .monitor import YouTubeMonitor -from .player import YouTubePlayer +from .player_monitor import PlayerMonitor from .system_version import current_system_version __all__ = ( + 'PlayerMonitor', 'create_path', 'create_uri_path', 'current_system_version', @@ -52,5 +53,4 @@ 'to_str', 'to_unicode', 'YouTubeMonitor', - 'YouTubePlayer', ) diff --git a/resources/lib/youtube_plugin/kodion/utils/player.py b/resources/lib/youtube_plugin/kodion/utils/player_monitor.py similarity index 80% rename from resources/lib/youtube_plugin/kodion/utils/player.py rename to resources/lib/youtube_plugin/kodion/utils/player_monitor.py index b9ccee0f2..e3e4f44d2 100644 --- a/resources/lib/youtube_plugin/kodion/utils/player.py +++ b/resources/lib/youtube_plugin/kodion/utils/player_monitor.py @@ -16,9 +16,9 @@ from ..compatibility import xbmc -class PlaybackMonitorThread(threading.Thread): +class PlayerMonitorThread(threading.Thread): def __init__(self, player, provider, context, playback_json): - super(PlaybackMonitorThread, self).__init__() + super(PlayerMonitorThread, self).__init__() self._stopped = threading.Event() self._ended = threading.Event() @@ -58,7 +58,6 @@ def abort_now(self): or self.stopped()) def run(self): - playing_file = self.playback_json.get('playing_file') play_count = self.playback_json.get('play_count', 0) use_remote_history = self.playback_json.get('use_remote_history', False) use_local_history = self.playback_json.get('use_local_history', False) @@ -66,46 +65,39 @@ def run(self): refresh_only = self.playback_json.get('refresh_only', False) clip = self.playback_json.get('clip', False) - player = self.player - - self._context.log_debug('PlaybackMonitorThread[{0}]: Starting' + self._context.log_debug('PlayerMonitorThread[{0}]: Starting' .format(self.video_id)) - access_manager = self._context.get_access_manager() - - settings = self._context.get_settings() - - if playback_stats is None: - playback_stats = {} - - played_time = -1.0 - - state = 'playing' - last_state = 'playing' - - np_wait_time = 0.2 - np_waited = 0.0 - p_wait_time = 0.5 - p_waited = 0.0 - - report_interval = 10.0 - first_report = True - report_url = playback_stats.get('playback_url', '') - - while not player.isPlaying() and not self._context.abort_requested(): - self._context.log_debug('Waiting for playback to start') + player = self.player - xbmc.sleep(int(np_wait_time * 1000)) - if np_waited >= 5: + wait_time = 0.2 + waited = 0.0 + while not player.isPlaying(): + if self._context.abort_requested(): + break + if waited >= 5: self.end() return - np_waited += np_wait_time + self._context.log_debug('Waiting for playback to start') + xbmc.sleep(int(wait_time * 1000)) + waited += wait_time + else: + self._context.send_notification('PlaybackStarted', { + 'video_id': self.video_id, + 'channel_id': self.channel_id, + 'status': self.video_status, + }) client = self.provider.get_client(self._context) - is_logged_in = self.provider.is_logged_in() + logged_in = self.provider.is_logged_in() + report_url = playback_stats.get('playback_url', '') + if playback_stats is None: + playback_stats = {} + state = 'playing' + last_state = 'playing' - if is_logged_in and report_url and use_remote_history: + if logged_in and report_url and use_remote_history: client.update_watch_history( self._context, self.video_id, @@ -116,21 +108,18 @@ def run(self): ) report_url = playback_stats.get('watchtime_url', '') + report_interval = 10.0 + first_report = True - plugin_play_path = 'plugin://plugin.video.youtube/play/' - video_id_param = 'video_id=%s' % self.video_id + access_manager = self._context.get_access_manager() + settings = self._context.get_settings() - notification_sent = False + video_id_param = 'video_id=%s' % self.video_id + played_time = -1.0 + wait_time = 0.5 + waited = 0.0 while not self.abort_now(): - if not notification_sent: - notification_sent = True - self._context.send_notification('PlaybackStarted', { - 'video_id': self.video_id, - 'channel_id': self.channel_id, - 'status': self.video_status, - }) - last_total_time = self.total_time last_current_time = self.current_time last_segment_start = self.segment_start @@ -138,13 +127,14 @@ def run(self): try: current_file = player.getPlayingFile() - if (current_file != playing_file and - not (current_file.startswith(plugin_play_path) and - video_id_param in current_file)) or self.stopped(): - self.stop() - break except RuntimeError: - pass + current_file = None + + if (not current_file or video_id_param not in current_file + or not self._context.is_plugin_path(current_file, 'play/') + or self.stopped()): + self.stop() + break if self.abort_now(): self.update_times(last_total_time, @@ -204,12 +194,12 @@ def run(self): last_percent_complete) break - if p_waited >= report_interval: + if waited >= report_interval: # refresh client, tokens may need refreshing - if is_logged_in: + if logged_in: self.provider.reset_client() client = self.provider.get_client(self._context) - is_logged_in = self.provider.is_logged_in() + logged_in = self.provider.is_logged_in() if self.current_time == played_time: last_state = state @@ -227,15 +217,15 @@ def run(self): last_percent_complete) break - if (is_logged_in and report_url and use_remote_history - and (first_report or p_waited >= report_interval)): + if (logged_in and report_url and use_remote_history + and (first_report or waited >= report_interval)): if first_report: first_report = False self.segment_start = 0.0 self.current_time = 0.0 self.percent_complete = 0 - p_waited = 0.0 + waited = 0.0 if self.segment_start < 0: self.segment_start = 0.0 @@ -267,11 +257,11 @@ def run(self): if self.abort_now(): break - xbmc.sleep(int(p_wait_time * 1000)) + xbmc.sleep(int(wait_time * 1000)) - p_waited += p_wait_time + waited += wait_time - if is_logged_in and report_url and use_remote_history: + if logged_in and report_url and use_remote_history: client.update_watch_history( self._context, self.video_id, @@ -297,15 +287,15 @@ def run(self): state = 'stopped' # refresh client, tokens may need refreshing - if is_logged_in: + if logged_in: self.provider.reset_client() client = self.provider.get_client(self._context) - is_logged_in = self.provider.is_logged_in() + logged_in = self.provider.is_logged_in() if self.percent_complete >= settings.get_play_count_min_percent(): play_count += 1 self.current_time = 0.0 - if is_logged_in and report_url and use_remote_history: + if logged_in and report_url and use_remote_history: client.update_watch_history( self._context, self.video_id, @@ -316,7 +306,7 @@ def run(self): ) else: - if is_logged_in and report_url and use_remote_history: + if logged_in and report_url and use_remote_history: client.update_watch_history( self._context, self.video_id, @@ -338,24 +328,26 @@ def run(self): self._context.get_playback_history().update(self.video_id, play_data) - if not refresh_only and is_logged_in: - if settings.get_bool('youtube.playlist.watchlater.autoremove', - True): - watch_later_id = access_manager.get_watch_later_id() - - if watch_later_id: - playlist_item_id = client.get_playlist_item_id_of_video_id( - playlist_id=watch_later_id, video_id=self.video_id + if refresh_only: + pass + elif settings.get_bool('youtube.playlist.watchlater.autoremove', True): + watch_later_id = logged_in and access_manager.get_watch_later_id() + if watch_later_id: + playlist_item_id = client.get_playlist_item_id_of_video_id( + playlist_id=watch_later_id, video_id=self.video_id + ) + if playlist_item_id: + _ = client.remove_video_from_playlist( + watch_later_id, playlist_item_id ) - if playlist_item_id: - json_data = client.remove_video_from_playlist( - watch_later_id, playlist_item_id - ) + else: + self._context.get_watch_later_list().remove(self.video_id) + if logged_in and not refresh_only: history_playlist_id = access_manager.get_watch_history_id() if history_playlist_id and history_playlist_id != 'HL': - json_data = client.add_video_to_playlist(history_playlist_id, - self.video_id) + _ = client.add_video_to_playlist(history_playlist_id, + self.video_id) # rate video if settings.get_bool('youtube.post.play.rate', False): @@ -395,7 +387,7 @@ def run(self): self.end() def stop(self): - self._context.log_debug('PlaybackMonitorThread[{0}]: Stop event set' + self._context.log_debug('PlayerMonitorThread[{0}]: Stop event set' .format(self.video_id)) self._stopped.set() @@ -403,7 +395,7 @@ def stopped(self): return self._stopped.is_set() def end(self): - self._context.log_debug('PlaybackMonitorThread[{0}]: End event set' + self._context.log_debug('PlayerMonitorThread[{0}]: End event set' .format(self.video_id)) self._ended.set() @@ -411,9 +403,9 @@ def ended(self): return self._ended.is_set() -class YouTubePlayer(xbmc.Player): +class PlayerMonitor(xbmc.Player): def __init__(self, *_args, **kwargs): - super(YouTubePlayer, self).__init__() + super(PlayerMonitor, self).__init__() self._context = kwargs.get('context') self.provider = kwargs.get('provider') self.ui = self._context.get_ui() @@ -428,14 +420,14 @@ def stop_threads(self): continue if not thread.stopped(): - self._context.log_debug('PlaybackMonitorThread[{0}]: stopping' + self._context.log_debug('PlayerMonitorThread[{0}]: stopping' .format(thread.video_id)) thread.stop() for thread in self.threads: if thread.stopped() and not thread.ended(): try: - thread.join() + thread.join(5) except RuntimeError: pass @@ -447,19 +439,19 @@ def cleanup_threads(self, only_ended=True): continue if thread.ended(): - self._context.log_debug('PlaybackMonitorThread[{0}]: clean up' + self._context.log_debug('PlayerMonitorThread[{0}]: clean up' .format(thread.video_id)) else: - self._context.log_debug('PlaybackMonitorThread[{0}]: stopping' + self._context.log_debug('PlayerMonitorThread[{0}]: stopping' .format(thread.video_id)) if not thread.stopped(): thread.stop() try: - thread.join() + thread.join(5) except RuntimeError: pass - self._context.log_debug('PlaybackMonitor active threads: |{0}|'.format( + self._context.log_debug('PlayerMonitor active threads: |{0}|'.format( ', '.join([thread.video_id for thread in active_threads]) )) self.threads = active_threads @@ -484,10 +476,10 @@ def onAVStarted(self): self.ui.clear_property('playback_json') self.cleanup_threads() - self.threads.append(PlaybackMonitorThread(self, - self.provider, - self._context, - playback_json)) + self.threads.append(PlayerMonitorThread(self, + self.provider, + self._context, + playback_json)) def onPlayBackEnded(self): if not self.ui.busy_dialog_active(): From d2101396964fa380e09a8c47396755757ec83e8d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:37:57 +1100 Subject: [PATCH 134/141] Rename YouTubeMonitor to ServiceMonitor --- resources/lib/youtube_plugin/kodion/service.py | 4 ++-- resources/lib/youtube_plugin/kodion/utils/__init__.py | 4 ++-- .../kodion/utils/{monitor.py => service_monitor.py} | 8 +++----- 3 files changed, 7 insertions(+), 9 deletions(-) rename resources/lib/youtube_plugin/kodion/utils/{monitor.py => service_monitor.py} (96%) diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index d2a04d44b..3e3c21aab 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -14,7 +14,7 @@ from datetime import datetime from .context import Context -from .utils import PlayerMonitor, YouTubeMonitor +from .utils import PlayerMonitor, ServiceMonitor from ..youtube.provider import Provider @@ -55,7 +55,7 @@ def run(): context.log_debug('YouTube service initialization...') - monitor = YouTubeMonitor() + monitor = ServiceMonitor() player = PlayerMonitor(provider=Provider(), context=context) # wipe add-on temp folder on updates/restarts (subtitles, and mpd files) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index f6878edc1..6485902af 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -28,13 +28,14 @@ to_str, to_unicode, ) -from .monitor import YouTubeMonitor from .player_monitor import PlayerMonitor +from .service_monitor import ServiceMonitor from .system_version import current_system_version __all__ = ( 'PlayerMonitor', + 'ServiceMonitor', 'create_path', 'create_uri_path', 'current_system_version', @@ -52,5 +53,4 @@ 'strip_html_from_text', 'to_str', 'to_unicode', - 'YouTubeMonitor', ) diff --git a/resources/lib/youtube_plugin/kodion/utils/monitor.py b/resources/lib/youtube_plugin/kodion/utils/service_monitor.py similarity index 96% rename from resources/lib/youtube_plugin/kodion/utils/monitor.py rename to resources/lib/youtube_plugin/kodion/utils/service_monitor.py index fc1495d49..28622eba6 100644 --- a/resources/lib/youtube_plugin/kodion/utils/monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/service_monitor.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals import json -import os import shutil import threading @@ -21,11 +20,10 @@ from ..settings import Settings -class YouTubeMonitor(xbmc.Monitor): +class ServiceMonitor(xbmc.Monitor): _settings = Settings(xbmcaddon.Addon(ADDON_ID)) - # noinspection PyUnusedLocal,PyMissingConstructor - def __init__(self, *args, **kwargs): + def __init__(self): settings = self._settings self._whitelist = settings.httpd_whitelist() self._old_httpd_port = self._httpd_port = int(settings.httpd_port()) @@ -36,7 +34,7 @@ def __init__(self, *args, **kwargs): self.httpd_thread = None if self.use_httpd(): self.start_httpd() - super(YouTubeMonitor, self).__init__() + super(ServiceMonitor, self).__init__() def onNotification(self, sender, method, data): if sender == ADDON_ID and method.endswith('.check_settings'): From 1c670068b72349ec1f03a210e7694cafd44f9b53 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:42:03 +1100 Subject: [PATCH 135/141] Test SQL storage REPLACE speed improvements --- .../kodion/sql_store/storage.py | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 8174b5240..b7649bf28 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -126,7 +126,13 @@ class Storage(object): 'REPLACE' ' INTO {table}' ' (key, timestamp, value, size)' - ' VALUES(?, ?, ?, ?);' + ' VALUES (?,?,?,?);' + ), + 'set_flat': ( + 'REPLACE' + ' INTO {table}' + ' (key, timestamp, value, size)' + ' VALUES {{0}};' ), } @@ -184,20 +190,23 @@ def _open(self): cursor.arraysize = 100 sql_script = [ - 'PRAGMA journal_mode = WAL;', - 'PRAGMA busy_timeout = 20000;', + 'PRAGMA busy_timeout = 1000;', 'PRAGMA read_uncommitted = TRUE;', + 'PRAGMA secure_delete = FALSE;', + 'PRAGMA synchronous = NORMAL;', + 'PRAGMA locking_mode = NORMAL;' 'PRAGMA temp_store = MEMORY;', + 'PRAGMA mmap_size = 4096000;', 'PRAGMA page_size = 4096;', - 'PRAGMA synchronous = NORMAL;', - 'PRAGMA mmap_size = 10485760;', - # 'PRAGMA locking_mode = EXCLUSIVE;' - 'PRAGMA cache_size = 500;' + 'PRAGMA cache_size = 1000;', + 'PRAGMA journal_mode = WAL;', ] statements = [] if not self._table_created: - statements.append(self._sql['create_table']) + statements.append( + self._sql['create_table'] + ) if not self._table_updated: for result in cursor.execute(self._sql['has_old_table']): @@ -308,16 +317,28 @@ def _set(self, item_id, item): self._execute(cursor, optimize_query) self._execute(cursor, self._sql['set'], values=values) - def _set_many(self, items): + def _set_many(self, items, flatten=False): now = since_epoch(datetime.now()) - values = [self._encode(*item, timestamp=now) - for item in items.items()] - optimize_query = self._optimize_item_count(len(items), defer=True) + num_items = len(items) + + if flatten: + values = [enc_part + for item in items.items() + for enc_part in self._encode(*item, timestamp=now)] + query = self._sql['set_flat'].format( + '(?,?,?,?),' * (num_items - 1) + '(?,?,?,?)' + ) + else: + values = [self._encode(*item, timestamp=now) + for item in items.items()] + query = self._sql['set'] + + optimize_query = self._optimize_item_count(num_items, defer=True) with self as (db, cursor), db: self._execute(cursor, 'BEGIN') if optimize_query: self._execute(cursor, optimize_query) - self._execute(cursor, self._sql['set'], many=True, values=values) + self._execute(cursor, query, many=not flatten, values=values) self._optimize_file_size() def clear(self, defer=False): @@ -377,12 +398,12 @@ def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, query = query.format(limit) else: num_ids = len(item_ids) - query = self._sql['get_by_key'].format(('?,' * (num_ids - 1)) + '?') + query = self._sql['get_by_key'].format('?,' * (num_ids - 1) + '?') item_ids = tuple(item_ids) + cut_off = since_epoch(datetime.now()) - seconds if seconds else 0 with self as (db, cursor), db: result = self._execute(cursor, query, item_ids) - cut_off = since_epoch(datetime.now()) - seconds if seconds else 0 if as_dict: result = { item[0]: self._decode(item[2], process, item) @@ -408,7 +429,7 @@ def _remove(self, item_id): def _remove_many(self, item_ids): num_ids = len(item_ids) - query = self._sql['remove_by_key'].format(('?,' * (num_ids - 1)) + '?') + query = self._sql['remove_by_key'].format('?,' * (num_ids - 1) + '?') with self as (db, cursor), db: self._execute(cursor, query, tuple(item_ids)) self._execute(cursor, 'VACUUM') From 645bb3eb4f84fdb7c887a54dbeaf78812a4c9b0d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:14:59 +1100 Subject: [PATCH 136/141] Simplify utils.methods.make_dirs add rm_dir - Also make code Python 2 compatible (remove exist_ok param in os.makedirs) --- .../lib/youtube_plugin/kodion/service.py | 5 +-- .../kodion/sql_store/storage.py | 3 +- .../youtube_plugin/kodion/utils/__init__.py | 2 ++ .../youtube_plugin/kodion/utils/methods.py | 28 +++++++++++----- .../kodion/utils/service_monitor.py | 33 ++----------------- .../lib/youtube_plugin/youtube/provider.py | 21 ++---------- 6 files changed, 30 insertions(+), 62 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index 3e3c21aab..b52d13010 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -14,7 +14,8 @@ from datetime import datetime from .context import Context -from .utils import PlayerMonitor, ServiceMonitor +from .constants import TEMP_PATH +from .utils import PlayerMonitor, ServiceMonitor, rm_dir from ..youtube.provider import Provider @@ -59,7 +60,7 @@ def run(): player = PlayerMonitor(provider=Provider(), context=context) # wipe add-on temp folder on updates/restarts (subtitles, and mpd files) - monitor.remove_temp_dir() + rm_dir(TEMP_PATH) # wipe function cache on updates/restarts (fix cipher related issues on update, valid for one day otherwise) try: diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index b7649bf28..d54cf1f22 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -19,6 +19,7 @@ from ..logger import log_error from ..utils.datetime_parser import since_epoch +from ..utils.methods import make_dirs class Storage(object): @@ -171,7 +172,7 @@ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): def _open(self): if not os.path.exists(self._filename): - os.makedirs(os.path.dirname(self._filename), exist_ok=True) + make_dirs(os.path.dirname(self._filename)) self.__class__._table_created = False self.__class__._table_updated = True diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 6485902af..7861ebf0a 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -22,6 +22,7 @@ loose_version, make_dirs, merge_dicts, + rm_dir, seconds_to_duration, select_stream, strip_html_from_text, @@ -48,6 +49,7 @@ 'loose_version', 'make_dirs', 'merge_dicts', + 'rm_dir', 'seconds_to_duration', 'select_stream', 'strip_html_from_text', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 3528f25c8..cb469ee47 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -14,6 +14,7 @@ import json import os import re +import shutil from datetime import timedelta from math import floor, log @@ -33,6 +34,7 @@ 'make_dirs', 'merge_dicts', 'print_items', + 'rm_dir', 'seconds_to_duration', 'select_stream', 'strip_html_from_text', @@ -231,16 +233,8 @@ def print_items(items): def make_dirs(path): if not path.endswith('/'): path = ''.join((path, '/')) - succeeded = xbmcvfs.exists(path) - - if succeeded: - return xbmcvfs.translatePath(path) - - try: - succeeded = xbmcvfs.mkdirs(path) - except OSError: - pass + succeeded = xbmcvfs.exists(path) or xbmcvfs.mkdirs(path) if succeeded: return xbmcvfs.translatePath(path) @@ -257,6 +251,22 @@ def make_dirs(path): return False +def rm_dir(path): + succeeded = (not xbmcvfs.exists(path) + or xbmcvfs.rmdir(path, force=True)) + if not succeeded: + path = xbmcvfs.translatePath(path) + try: + shutil.rmtree(path) + succeeded = not xbmcvfs.exists(path) + except OSError: + pass + + if succeeded: + return True + log_error('Failed to remove directory: {0}'.format(path)) + return False + def find_video_id(plugin_path): match = re.search(r'.*video_id=(?P[a-zA-Z0-9_\-]{11}).*', plugin_path) if match: diff --git a/resources/lib/youtube_plugin/kodion/utils/service_monitor.py b/resources/lib/youtube_plugin/kodion/utils/service_monitor.py index 28622eba6..cddab114f 100644 --- a/resources/lib/youtube_plugin/kodion/utils/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/utils/service_monitor.py @@ -10,11 +10,10 @@ from __future__ import absolute_import, division, unicode_literals import json -import shutil import threading -from ..compatibility import unquote, xbmc, xbmcaddon, xbmcvfs -from ..constants import ADDON_ID, TEMP_PATH +from ..compatibility import unquote, xbmc, xbmcaddon +from ..constants import ADDON_ID from ..logger import log_debug from ..network import get_http_server, is_httpd_live from ..settings import Settings @@ -156,31 +155,3 @@ def restart_httpd(self): def ping_httpd(self): return is_httpd_live(port=self.httpd_port()) - - @staticmethod - def remove_temp_dir(): - temp_path = TEMP_PATH - succeeded = False - - if xbmcvfs.exists(temp_path): - try: - succeeded = xbmcvfs.rmdir(temp_path, force=True) - except OSError: - pass - else: - succeeded = True - - if succeeded: - return True - - temp_path = xbmcvfs.translatePath(TEMP_PATH) - try: - shutil.rmtree(temp_path) - succeeded = not xbmcvfs.exists(temp_path) - except OSError: - pass - - if succeeded: - return True - log_debug('Failed to remove directory: {0}'.format(temp_path)) - return False diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 1b2a95f1a..c1c72e2c2 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -13,7 +13,6 @@ import json import os import re -import shutil import socket from base64 import b64decode @@ -45,7 +44,7 @@ ) from ..kodion.items import DirectoryItem, NewSearchItem, SearchItem, menu_items from ..kodion.network import get_client_ip_address, is_httpd_live -from ..kodion.utils import find_video_id, strip_html_from_text +from ..kodion.utils import find_video_id, rm_dir, strip_html_from_text class Provider(AbstractProvider): @@ -1066,23 +1065,7 @@ def maintenance_actions(self, context, re_match): return if maint_type == 'temp_files': - temp_path = _file_w_path - - if xbmcvfs.exists(temp_path): - try: - succeeded = xbmcvfs.rmdir(temp_path, force=True) - except OSError: - pass - else: - succeeded = True - - if not succeeded: - temp_path = xbmcvfs.translatePath(_file_w_path) - try: - shutil.rmtree(temp_path) - succeeded = not xbmcvfs.exists(temp_path) - except OSError: - pass + succeeded = rm_dir(_file_w_path) elif _file_w_path: succeeded = xbmcvfs.delete(_file_w_path) From 56d313e665fce79db91bdf43f7a57c1360933c63 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:17:31 +1100 Subject: [PATCH 137/141] Don't add .sqlite file extension when creating db - Correct filepath to be used when creating new instance - TODO: add filenames to constants --- .../kodion/context/abstract_context.py | 48 +++++++++++-------- .../kodion/sql_store/data_cache.py | 4 +- .../kodion/sql_store/favorite_list.py | 4 +- .../kodion/sql_store/function_cache.py | 4 +- .../kodion/sql_store/playback_history.py | 4 +- .../kodion/sql_store/search_history.py | 4 +- .../kodion/sql_store/storage.py | 14 +++--- .../kodion/sql_store/watch_later_list.py | 4 +- 8 files changed, 47 insertions(+), 39 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 69f4f7b46..4cee1fe2e 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -143,47 +143,57 @@ def get_cache_path(self): def get_playback_history(self): if not self._playback_history: uuid = self.get_access_manager().get_current_user_id() - db_file = os.path.join(self.get_data_path(), 'playback', uuid) - self._playback_history = PlaybackHistory(db_file) + filename = ''.join((uuid, '.sqlite')) + filepath = os.path.join(self.get_data_path(), 'playback', filename) + self._playback_history = PlaybackHistory(filepath) return self._playback_history def get_data_cache(self): if not self._data_cache: - max_cache_size_mb = self.get_settings().get_int(settings.CACHE_SIZE, -1) - if max_cache_size_mb <= 0: - max_cache_size_mb = 5 + cache_size = self.get_settings().get_int(settings.CACHE_SIZE, -1) + if cache_size <= 0: + cache_size = 5 else: - max_cache_size_mb /= 2.0 - self._data_cache = DataCache(os.path.join(self.get_cache_path(), 'data_cache'), - max_file_size_mb=max_cache_size_mb) + cache_size /= 2.0 + filename = 'data_cache.sqlite' + filepath = os.path.join(self.get_cache_path(), filename) + self._data_cache = DataCache(filepath, max_file_size_mb=cache_size) return self._data_cache def get_function_cache(self): if not self._function_cache: - max_cache_size_mb = self.get_settings().get_int(settings.CACHE_SIZE, -1) - if max_cache_size_mb <= 0: - max_cache_size_mb = 5 + cache_size = self.get_settings().get_int(settings.CACHE_SIZE, -1) + if cache_size <= 0: + cache_size = 5 else: - max_cache_size_mb /= 2.0 - self._function_cache = FunctionCache(os.path.join(self.get_cache_path(), 'cache'), - max_file_size_mb=max_cache_size_mb) + cache_size /= 2.0 + filename = 'cache.sqlite' + filepath = os.path.join(self.get_cache_path(), filename) + self._function_cache = FunctionCache(filepath, + max_file_size_mb=cache_size) return self._function_cache def get_search_history(self): if not self._search_history: - max_search_history_items = self.get_settings().get_int(settings.SEARCH_SIZE, 50) - self._search_history = SearchHistory(os.path.join(self.get_cache_path(), 'search'), - max_item_count=max_search_history_items) + search_size = self.get_settings().get_int(settings.SEARCH_SIZE, 50) + filename = 'search.sqlite' + filepath = os.path.join(self.get_cache_path(), filename) + self._search_history = SearchHistory(filepath, + max_item_count=search_size) return self._search_history def get_favorite_list(self): if not self._favorite_list: - self._favorite_list = FavoriteList(os.path.join(self.get_cache_path(), 'favorites')) + filename = 'favorites.sqlite' + filepath = os.path.join(self.get_cache_path(), filename) + self._favorite_list = FavoriteList(filepath) return self._favorite_list def get_watch_later_list(self): if not self._watch_later_list: - self._watch_later_list = WatchLaterList(os.path.join(self.get_cache_path(), 'watch_later')) + filename = 'watch_later.sqlite' + filepath = os.path.join(self.get_cache_path(), filename) + self._watch_later_list = WatchLaterList(filepath) return self._watch_later_list def get_access_manager(self): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py index 197ca8add..791af6f25 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/data_cache.py @@ -19,9 +19,9 @@ class DataCache(Storage): _table_updated = False _sql = {} - def __init__(self, filename, max_file_size_mb=5): + def __init__(self, filepath, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 - super(DataCache, self).__init__(filename, + super(DataCache, self).__init__(filepath, max_file_size_kb=max_file_size_kb) def get_items(self, content_ids, seconds): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py index fc84cbae0..97e81cef0 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/favorite_list.py @@ -20,8 +20,8 @@ class FavoriteList(Storage): _table_updated = False _sql = {} - def __init__(self, filename): - super(FavoriteList, self).__init__(filename) + def __init__(self, filepath): + super(FavoriteList, self).__init__(filepath) @staticmethod def _sort_item(item): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index 17e6a9682..1ac1f4fc2 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -22,9 +22,9 @@ class FunctionCache(Storage): _table_updated = False _sql = {} - def __init__(self, filename, max_file_size_mb=5): + def __init__(self, filepath, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 - super(FunctionCache, self).__init__(filename, + super(FunctionCache, self).__init__(filepath, max_file_size_kb=max_file_size_kb) self._enabled = True diff --git a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py index 03a4a9ce3..880fac4da 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/playback_history.py @@ -18,8 +18,8 @@ class PlaybackHistory(Storage): _table_updated = False _sql = {} - def __init__(self, filename): - super(PlaybackHistory, self).__init__(filename) + def __init__(self, filepath): + super(PlaybackHistory, self).__init__(filepath) def _add_last_played(self, value, item): value['last_played'] = self._convert_timestamp(item[1]) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py index 6f6784534..5aefe1591 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/search_history.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/search_history.py @@ -21,8 +21,8 @@ class SearchHistory(Storage): _table_updated = False _sql = {} - def __init__(self, filename, max_item_count=10): - super(SearchHistory, self).__init__(filename, + def __init__(self, filepath, max_item_count=10): + super(SearchHistory, self).__init__(filepath, max_item_count=max_item_count) def get_items(self): diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index d54cf1f22..83501f2bd 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -137,10 +137,8 @@ class Storage(object): ), } - def __init__(self, filename, max_item_count=-1, max_file_size_kb=-1): - self._filename = filename - if not self._filename.endswith('.sqlite'): - self._filename = ''.join((self._filename, '.sqlite')) + def __init__(self, filepath, max_item_count=-1, max_file_size_kb=-1): + self._filepath = filepath self._db = None self._cursor = None self._max_item_count = max_item_count @@ -171,13 +169,13 @@ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): self._close() def _open(self): - if not os.path.exists(self._filename): - make_dirs(os.path.dirname(self._filename)) + if not os.path.exists(self._filepath): + make_dirs(os.path.dirname(self._filepath)) self.__class__._table_created = False self.__class__._table_updated = True try: - db = sqlite3.connect(self._filename, + db = sqlite3.connect(self._filepath, check_same_thread=False, timeout=1, isolation_level=None) @@ -273,7 +271,7 @@ def _optimize_file_size(self, defer=False): return False try: - file_size_kb = (os.path.getsize(self._filename) // 1024) + file_size_kb = (os.path.getsize(self._filepath) // 1024) if file_size_kb <= self._max_file_size_kb: return False except OSError: diff --git a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py index 39274f029..b98ebdafe 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/watch_later_list.py @@ -20,8 +20,8 @@ class WatchLaterList(Storage): _table_updated = False _sql = {} - def __init__(self, filename): - super(WatchLaterList, self).__init__(filename) + def __init__(self, filepath): + super(WatchLaterList, self).__init__(filepath) def get_items(self): result = self._get_by_ids(process=from_json, values_only=True) From f2d520f90d0e7bb0c5312cb6d9a4e8509f4f6de7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:24:14 +1100 Subject: [PATCH 138/141] Simplify datetime.strptime bug workaround - Use common method in other modules - Fix for Python2 compatibility as follow up to 421ec80 --- .../youtube_plugin/kodion/items/base_item.py | 22 ++++++++- .../lib/youtube_plugin/kodion/items/utils.py | 21 ++++++--- .../lib/youtube_plugin/kodion/service.py | 47 +++---------------- .../kodion/utils/datetime_parser.py | 21 ++++----- 4 files changed, 50 insertions(+), 61 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 85c46ec3b..a919f8e68 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -62,10 +62,28 @@ def to_dict(self): def dumps(self): def _encoder(obj): if isinstance(obj, (date, datetime)): + class_name = obj.__class__.__name__ + + if 'fromisoformat' in dir(obj): + return { + '__class__': class_name, + '__isoformat__': obj.isoformat(), + } + + if class_name == 'datetime': + if obj.tzinfo: + format_string = '%Y-%m-%dT%H:%M:%S%z' + else: + format_string = '%Y-%m-%dT%H:%M:%S' + else: + format_string = '%Y-%m-%d' + return { - '__class__': obj.__class__.__name__, - '__isoformat__': obj.isoformat(), + '__class__': class_name, + '__format_string__': format_string, + '__value__': obj.strftime(format_string) } + return json.JSONEncoder().default(obj) return json.dumps(self.to_dict(), ensure_ascii=False, default=_encoder) diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index 7c985b61b..e85e7bd5c 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -17,6 +17,7 @@ from .directory_item import DirectoryItem from .image_item import ImageItem from .video_item import VideoItem +from ..utils.datetime_parser import strptime _ITEM_TYPES = { @@ -29,12 +30,20 @@ def _decoder(obj): date_in_isoformat = obj.get('__isoformat__') - if not date_in_isoformat: - return obj - - if obj['__class__'] == 'date': - return date.fromisoformat(date_in_isoformat) - return datetime.fromisoformat(date_in_isoformat) + if date_in_isoformat: + if obj['__class__'] == 'date': + return date.fromisoformat(date_in_isoformat) + return datetime.fromisoformat(date_in_isoformat) + + format_string = obj.get('__format_string__') + if format_string: + value = obj['__value__'] + value = strptime(value, format_string) + if obj['__class__'] == 'date': + return value.date() + return value + + return obj def from_json(json_data, *_args): diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index b52d13010..92752d761 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -import time from datetime import datetime from .context import Context @@ -19,39 +18,7 @@ from ..youtube.provider import Provider -def strptime(stamp, stamp_fmt): - # noinspection PyUnresolvedReferences - import _strptime - try: - time.strptime('01 01 2012', '%d %m %Y') # dummy call - except: - pass - return time.strptime(stamp, stamp_fmt) - - -def get_stamp_diff(current_stamp): - stamp_format = '%Y-%m-%d %H:%M:%S.%f' - current_datetime = datetime.now() - if not current_stamp: - return 86400 # 24 hrs - try: - stamp_datetime = datetime(*(strptime(current_stamp, stamp_format)[0:6])) - except ValueError: # current_stamp has no microseconds - stamp_format = '%Y-%m-%d %H:%M:%S' - stamp_datetime = datetime(*(strptime(current_stamp, stamp_format)[0:6])) - - time_delta = current_datetime - stamp_datetime - if time_delta: - return time_delta.total_seconds() - return 0 - - def run(): - sleep_time = 10 - ping_delay_time = 60 - ping_timestamp = None - first_run = True - context = Context() context.log_debug('YouTube service initialization...') @@ -71,19 +38,17 @@ def run(): context.get_ui().clear_property('abort_requested') + sleep_time = 10 + ping_delay = 60 + ping_time = None while not monitor.abortRequested(): - - ping_diff = get_stamp_diff(ping_timestamp) - - if (ping_timestamp is None) or (ping_diff >= ping_delay_time): - ping_timestamp = str(datetime.now()) + now = datetime.now() + if not ping_time or (ping_time - now).total_seconds() >= ping_delay: + ping_time = now if monitor.httpd and not monitor.ping_httpd(): monitor.restart_httpd() - if first_run: - first_run = False - if monitor.waitForAbort(sleep_time): break diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 83c0cf6f2..42791ccd7 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -11,8 +11,9 @@ from __future__ import absolute_import, division, unicode_literals import re -import time from datetime import date, datetime, time as dt_time, timedelta +from importlib import import_module +from sys import modules from ..exceptions import KodionException @@ -175,22 +176,18 @@ def datetime_to_since(context, dt): return ' '.join((context.format_date_short(dt), context.format_time(dt))) -def strptime(s, fmt='%Y-%m-%dT%H:%M:%S'): - # noinspection PyUnresolvedReferences - - ms_precision_required = '.' in s[-5:-1] - if ms_precision_required: +def strptime(datetime_str, fmt='%Y-%m-%dT%H:%M:%S'): + if '.' in datetime_str[-5:]: fmt.replace('%S', '%S.%f') else: fmt.replace('%S.%f', '%S') - import _strptime + if not datetime.strptime: + if '_strptime' in modules: + del modules['_strptime'] + modules['_strptime'] = import_module('_strptime') - try: - time.strptime('01 01 2012', '%d %m %Y') # dummy call - except: - pass - return datetime(*time.strptime(s, fmt)[:6]) + return datetime.strptime(datetime_str, fmt) def since_epoch(dt_object): From 8e35be451e93ea36fc58cf788024369330f01f08 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:26:37 +1100 Subject: [PATCH 139/141] sqlite.BINARY Python2 compatibility fix - BINARY is memoryview in Python3, buffer in Python2 --- resources/lib/youtube_plugin/kodion/sql_store/storage.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 83501f2bd..3508b45b3 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -337,7 +337,7 @@ def _set_many(self, items, flatten=False): self._execute(cursor, 'BEGIN') if optimize_query: self._execute(cursor, optimize_query) - self._execute(cursor, query, many=not flatten, values=values) + self._execute(cursor, query, many=(not flatten), values=values) self._optimize_file_size() def clear(self, defer=False): @@ -372,7 +372,9 @@ def _encode(key, obj, timestamp=None): blob = sqlite3.Binary(pickle.dumps( obj, protocol=pickle.HIGHEST_PROTOCOL )) - size = getattr(blob, 'nbytes', None) or blob.itemsize * len(blob) + size = getattr(blob, 'nbytes', None) + if not size: + size = int(memoryview(blob).itemsize) * len(blob) return str(key), timestamp, blob, size def _get(self, item_id, process=None, seconds=None): From 2fc0b5e7fa25f5becafea528af5f35e6f7f7bca8 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:29:29 +1100 Subject: [PATCH 140/141] Misc tidy ups --- .../kodion/network/http_server.py | 34 ++++++++----------- .../lib/youtube_plugin/kodion/service.py | 21 ++++++------ 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 0c5c9880e..327789099 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -80,8 +80,7 @@ def do_GET(self): # Strip trailing slash if present stripped_path = self.path.rstrip('/') if stripped_path != paths.PING: - log_debug('HTTPServer: GET Request uri path |{stripped_path}|' - .format(stripped_path=self.path)) + log_debug('HTTPServer: GET uri path |{path}|'.format(path=self.path)) if not self.connection_allowed(): self.send_error(403) @@ -96,25 +95,23 @@ def do_GET(self): self.wfile.write(client_json.encode('utf-8')) elif self.path.startswith(paths.MPD): - file_path = os.path.join(self.BASE_PATH, - self.path[len(paths.MPD):]) + filepath = os.path.join(self.BASE_PATH, self.path[len(paths.MPD):]) file_chunk = True - log_debug('HTTPServer: Request file path |{file_path}|' - .format(file_path=file_path)) + log_debug('HTTPServer: GET filepath |{path}|'.format(path=filepath)) try: - with open(file_path, 'rb') as f: + with open(filepath, 'rb') as f: self.send_response(200) self.send_header('Content-Type', 'application/dash+xml') self.send_header('Content-Length', - str(os.path.getsize(file_path))) + str(os.path.getsize(filepath))) self.end_headers() while file_chunk: file_chunk = f.read(self.chunk_size) if file_chunk: self.wfile.write(file_chunk) except IOError: - response = ('File Not Found: |{proxy_path}| -> |{file_path}|' - .format(proxy_path=self.path, file_path=file_path)) + response = ('File Not Found: |{path}| -> |{filepath}|' + .format(path=self.path, filepath=filepath)) self.send_error(404, response) elif api_config_enabled and stripped_path == paths.API: @@ -192,24 +189,22 @@ def do_GET(self): # noinspection PyPep8Naming def do_HEAD(self): - log_debug('HTTPServer: HEAD Request uri path |{proxy_path}|' - .format(proxy_path=self.path)) + log_debug('HTTPServer: HEAD uri path |{path}|'.format(path=self.path)) if not self.connection_allowed(): self.send_error(403) elif self.path.startswith(paths.MPD): - file_path = os.path.join(self.BASE_PATH, - self.path[len(paths.MPD):]) - if not os.path.isfile(file_path): - response = ('File Not Found: |{proxy_path}| -> |{file_path}|' - .format(proxy_path=self.path, file_path=file_path)) + filepath = os.path.join(self.BASE_PATH, self.path[len(paths.MPD):]) + if not os.path.isfile(filepath): + response = ('File Not Found: |{path}| -> |{filepath}|' + .format(path=self.path, filepath=filepath)) self.send_error(404, response) else: self.send_response(200) self.send_header('Content-Type', 'application/dash+xml') self.send_header('Content-Length', - str(os.path.getsize(file_path))) + str(os.path.getsize(filepath))) self.end_headers() else: @@ -217,8 +212,7 @@ def do_HEAD(self): # noinspection PyPep8Naming def do_POST(self): - log_debug('HTTPServer: Request uri path |{proxy_path}|' - .format(proxy_path=self.path)) + log_debug('HTTPServer: POST uri path |{path}|'.format(path=self.path)) if not self.connection_allowed(): self.send_error(403) diff --git a/resources/lib/youtube_plugin/kodion/service.py b/resources/lib/youtube_plugin/kodion/service.py index 92752d761..d549a75e1 100644 --- a/resources/lib/youtube_plugin/kodion/service.py +++ b/resources/lib/youtube_plugin/kodion/service.py @@ -20,8 +20,15 @@ def run(): context = Context() - context.log_debug('YouTube service initialization...') + context.get_ui().clear_property('abort_requested') + # wipe function cache on updates/restarts to fix cipher related issues on + # update, valid for one day otherwise + try: + context.get_function_cache().clear() + except Exception: + # prevent service failing due to cache related issues + pass monitor = ServiceMonitor() player = PlayerMonitor(provider=Provider(), context=context) @@ -29,15 +36,6 @@ def run(): # wipe add-on temp folder on updates/restarts (subtitles, and mpd files) rm_dir(TEMP_PATH) - # wipe function cache on updates/restarts (fix cipher related issues on update, valid for one day otherwise) - try: - context.get_function_cache().clear() - except: - # prevent service to failing due to cache related issues - pass - - context.get_ui().clear_property('abort_requested') - sleep_time = 10 ping_delay = 60 ping_time = None @@ -54,7 +52,8 @@ def run(): context.get_ui().set_property('abort_requested', 'true') - player.cleanup_threads(only_ended=False) # clean up any/all playback monitoring threads + # clean up any/all playback monitoring threads + player.cleanup_threads(only_ended=False) if monitor.httpd: monitor.shutdown_httpd() # shutdown http server From 56e924425776b2df339f9784208e49ab09b271bc Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:33:46 +1100 Subject: [PATCH 141/141] Beta bump - v7.0.3+beta.1 --- addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index f2b401b55..5e4a55fce 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - +