From 87e8a7f772ba11e86fb9196bc34fce5fa451c8cb Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:15:41 +1100 Subject: [PATCH 01/26] Properly display bookmarks that were created with only partial information --- .../youtube_plugin/kodion/items/base_item.py | 50 ++++++++++++++++++- .../lib/youtube_plugin/kodion/items/utils.py | 7 +-- .../lib/youtube_plugin/youtube/provider.py | 34 +++++++++++-- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 84aa969d7..fd93a9d51 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -14,7 +14,14 @@ from datetime import date, datetime from hashlib import md5 -from ..compatibility import datetime_infolabel, string_type, to_str, unescape +from ..compatibility import ( + datetime_infolabel, + parse_qsl, + string_type, + to_str, + unescape, + urlsplit, +) from ..constants import MEDIA_PATH @@ -71,6 +78,47 @@ def get_id(self): """ return md5(''.join((self._name, self._uri)).encode('utf-8')).hexdigest() + def parse_item_ids_from_uri(self): + if not self._uri: + return None + + item_ids = {} + + uri = urlsplit(self._uri) + path = uri.path + params = dict(parse_qsl(uri.query)) + + video_id = params.get('video_id') + if video_id: + item_ids['video_id'] = video_id + + channel_id = None + playlist_id = None + + while path: + part, _, next_part = path.partition('/') + if not next_part: + break + + if part == 'channel': + channel_id = next_part.partition('/')[0] + elif part == 'playlist': + playlist_id = next_part.partition('/')[0] + path = next_part + + if channel_id: + item_ids['channel_id'] = channel_id + if playlist_id: + item_ids['playlist_id'] = playlist_id + + for item_id, value in item_ids.items(): + try: + setattr(self, item_id, value) + except AttributeError: + pass + + return item_ids + def set_name(self, name): try: name = unescape(name) diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index f93ab11fa..6344d986f 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -52,12 +52,12 @@ def from_json(json_data, *args): :param json_data: :return: """ + timestamp = args[0][1] if args and args[0] and len(args[0]) == 4 else None + if isinstance(json_data, string_type): if json_data == to_str(None): # Channel bookmark that will be updated. Store timestamp for update - if args and args[0] and len(args[0]) == 4: - return args[0][1] - return None + return timestamp json_data = json.loads(json_data, object_hook=_decoder) item_type = json_data.get('type') @@ -69,5 +69,6 @@ def from_json(json_data, *args): for key, value in json_data.get('data', {}).items(): if hasattr(item, key): setattr(item, key, value) + item.set_bookmark_timestamp(timestamp) return item diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 332c050ea..addae274d 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -40,6 +40,7 @@ PATHS, ) from ..kodion.items import ( + BaseItem, DirectoryItem, NewSearchItem, SearchItem, @@ -1427,22 +1428,47 @@ def _update(new_item): yt_id = item_id callback = _update_bookmark(item_id, item) partial = True - else: + elif isinstance(item, BaseItem): callback = None partial = False + if isinstance(item, VideoItem): kind = 'youtube#video' yt_id = item.video_id else: - yt_id = item.playlist_id + yt_id = getattr(item, 'playlist_id', None) if yt_id: kind = 'youtube#playlist' else: kind = 'youtube#channel' - yt_id = item.channel_id + yt_id = getattr(item, 'channel_id', None) + else: + yt_id = None if not yt_id: - continue + if isinstance(item, BaseItem): + item_ids = item.parse_item_ids_from_uri() + to_delete = False + for kind in ('video', 'playlist', 'channel'): + yt_id = item_ids.get(kind + '_id') + if not yt_id: + continue + if yt_id == 'None': + to_delete = True + continue + kind = 'youtube#' + kind + partial = True + callback = _update_bookmark( + item_id, + item.get_bookmark_timestamp(), + ) + break + else: + if to_delete: + bookmarks_list.del_item(item_id) + continue + else: + continue item = { 'kind': kind, From 6dd9a0e9fb6d711d184642c820f0a9daf2a37bc3 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:22:13 +1100 Subject: [PATCH 02/26] Fix creation of YouTube object Items to set minimum required item id properties --- .../youtube_plugin/kodion/items/media_item.py | 25 ++++++++++++++----- .../kodion/plugin/xbmc/xbmc_plugin.py | 6 ++--- .../lib/youtube_plugin/youtube/helper/tv.py | 11 ++++++-- .../youtube/helper/url_to_item_converter.py | 21 +++++++++++----- .../youtube_plugin/youtube/helper/utils.py | 1 - .../lib/youtube_plugin/youtube/helper/v3.py | 10 +++++--- .../youtube_plugin/youtube/helper/yt_play.py | 9 ++++--- .../youtube/helper/yt_specials.py | 8 ++++-- .../lib/youtube_plugin/youtube/provider.py | 4 +++ 9 files changed, 67 insertions(+), 28 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/media_item.py b/resources/lib/youtube_plugin/kodion/items/media_item.py index 1b59fbc9c..f60820f00 100644 --- a/resources/lib/youtube_plugin/kodion/items/media_item.py +++ b/resources/lib/youtube_plugin/kodion/items/media_item.py @@ -30,7 +30,8 @@ def __init__(self, uri, image='DefaultFile.png', fanart=None, - plot=None): + plot=None, + video_id=None,): super(MediaItem, self).__init__(name, uri, image, fanart) self._aired = None self._premiered = None @@ -62,7 +63,7 @@ def __init__(self, self._upcoming = False self._vod = False - self._video_id = None + self._video_id = video_id self._channel_id = None self._subscription_id = None self._playlist_id = None @@ -338,8 +339,14 @@ def __init__(self, uri, image='DefaultAudio.png', fanart=None, - plot=None): - super(AudioItem, self).__init__(name, uri, image, fanart, plot) + plot=None, + video_id=None): + super(AudioItem, self).__init__(name, + uri, + image, + fanart, + plot, + video_id) self._album = None def set_album_name(self, album_name): @@ -364,8 +371,14 @@ def __init__(self, uri, image='DefaultVideo.png', fanart=None, - plot=None): - super(VideoItem, self).__init__(name, uri, image, fanart, plot) + plot=None, + video_id=None): + super(VideoItem, self).__init__(name, + uri, + image, + fanart, + plot, + video_id) self._directors = None self._episode = None self._imdb_id = None diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 7e54f32d3..c51c75081 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -188,9 +188,9 @@ def run(self, provider, context, focused=None): if not result: result = [ CommandItem( - context.localize('page.back'), - 'Action(ParentDir)', - context, + name=context.localize('page.back'), + command='Action(ParentDir)', + context=context, image='DefaultFolderBack.png', plot=context.localize('page.empty'), ) diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index f00bea1af..85e90f3f7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -29,7 +29,9 @@ def tv_videos_to_items(provider, context, json_data): video_id = item['id'] item_params['video_id'] = video_id video_id_dict[video_id] = VideoItem( - item['title'], context.create_uri((PATHS.PLAY,), item_params) + name=item['title'], + uri=context.create_uri((PATHS.PLAY,), item_params), + video_id=video_id, ) item_filter = context.get_settings().item_filter() @@ -90,7 +92,12 @@ def saved_playlists_to_items(provider, context, json_data): item_params, ) - playlist_item = DirectoryItem(title, item_uri, image=image) + playlist_item = DirectoryItem( + name=title, + uri=item_uri, + image=image, + playlist_id=playlist_id, + ) result.append(playlist_item) playlist_id_dict[playlist_id] = playlist_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 11acef66a..0bdc4ec51 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 @@ -92,7 +92,8 @@ def add_url(self, url, context): uri=context.create_uri( (PATHS.PLAY,), dict(new_params, video_id=video_id), - ) + ), + video_id=video_id, ) self._video_id_dict[video_id] = item @@ -100,7 +101,9 @@ def add_url(self, url, context): video_id = new_params['video_id'] item = VideoItem( - '', context.create_uri((PATHS.PLAY,), new_params) + name='', + uri=context.create_uri((PATHS.PLAY,), new_params), + video_id=video_id, ) self._video_id_dict[video_id] = item @@ -112,7 +115,9 @@ def add_url(self, url, context): return item = DirectoryItem( - '', context.create_uri(('playlist', playlist_id,), new_params), + name='', + uri=context.create_uri(('playlist', playlist_id,), new_params), + playlist_id=playlist_id, ) self._playlist_id_dict[playlist_id] = item @@ -125,9 +130,13 @@ def add_url(self, url, context): return item = VideoItem( - '', context.create_uri((PATHS.PLAY,), new_params) + name='', + uri=context.create_uri((PATHS.PLAY,), new_params), + channel_id=channel_id, ) if live else DirectoryItem( - '', context.create_uri(('channel', channel_id,), new_params) + name='', + uri=context.create_uri(('channel', channel_id,), new_params), + channel_id=channel_id, ) self._channel_id_dict[channel_id] = item @@ -154,7 +163,7 @@ def get_items(self, provider, context, skip_title=False): 'channel_ids': ','.join(self._channel_ids), }, ), - image='{media}/playlist.png', + image='{media}/channels.png', category_label=item_label, ) result.append(channels_item) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index ab3068405..dc8026a4f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -982,7 +982,6 @@ def update_video_infos(provider, context, video_id_dict, def update_play_info(provider, context, video_id, media_item, video_stream): - media_item.video_id = video_id update_video_infos(provider, context, {video_id: media_item}) settings = context.get_settings() diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 3d6212b89..9e799f82d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -123,7 +123,8 @@ def _process_list_response(provider, context, json_data, item_filter): item_uri, image=image, fanart=fanart, - plot=description) + plot=description, + video_id=item_id) video_id_dict[item_id] = item elif kind_type == 'channel': @@ -211,7 +212,8 @@ def _process_list_response(provider, context, json_data, item_filter): item_uri, image=image, fanart=fanart, - plot=description) + plot=description, + video_id=item_id) video_id_dict[item_id] = item elif kind_type == 'activity': @@ -233,7 +235,8 @@ def _process_list_response(provider, context, json_data, item_filter): item_uri, image=image, fanart=fanart, - plot=description) + plot=description, + video_id=item_id) video_id_dict[item_id] = item elif kind_type == 'commentthread': @@ -268,7 +271,6 @@ def _process_list_response(provider, context, json_data, item_filter): item.add_context_menu(**yt_item['_context_menu']) if isinstance(item, VideoItem): - item.video_id = item_id # Set track number from playlist, or set to current list length to # match "Default" (unsorted) sort order position = snippet.get('position') or len(result) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 31888ba5d..a6eeac809 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -121,10 +121,11 @@ def _play_stream(provider, context): )) stream['url'] = url - if audio_only or not video_type: - media_item = AudioItem(metadata.get('title', ''), stream['url']) - else: - media_item = VideoItem(metadata.get('title', ''), stream['url']) + media_item = (AudioItem if audio_only or not video_type else VideoItem)( + name=metadata.get('title', ''), + uri=stream['url'], + video_id=video_id, + ) use_history = not (screensaver or incognito or stream.get('live')) use_remote_history = use_history and settings.use_remote_history() diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index 52dda63e8..c19958e38 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -224,7 +224,9 @@ def _display_channels(channel_ids): channel_id_dict = {} for channel_id in channel_ids: channel_item = DirectoryItem( - '', context.create_uri(('channel', channel_id,), item_params) + name='', + uri=context.create_uri(('channel', channel_id,), item_params), + channel_id=channel_id, ) channel_id_dict[channel_id] = channel_item @@ -249,7 +251,9 @@ def _display_playlists(playlist_ids): playlist_id_dict = {} for playlist_id in playlist_ids: playlist_item = DirectoryItem( - '', context.create_uri(('playlist', playlist_id,), item_params) + name='', + uri=context.create_uri(('playlist', playlist_id,), item_params), + playlist_id=playlist_id, ) playlist_id_dict[playlist_id] = playlist_item diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index addae274d..dc0189f30 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -413,6 +413,8 @@ def on_channel_playlists(provider, context, re_match): image='{media}/playlist.png', fanart=fanart, category_label=item_label, + channel_id=channel_id, + playlist_id=uploads, ) result = [uploads] else: @@ -584,6 +586,7 @@ def on_channel(provider, context, re_match): image='{media}/playlist.png', fanart=fanart, category_label=item_label, + channel_id=channel_id, ) result.append(playlists_item) @@ -609,6 +612,7 @@ def on_channel(provider, context, re_match): image='{media}/live.png', fanart=fanart, category_label=item_label, + channel_id=channel_id, ) result.append(live_item) From 09a38f5ae04dfd14a7e80124e14055f771318d3e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:01:38 +1100 Subject: [PATCH 03/26] Standardise return type of LoginClient.refresh_token #932 --- .../youtube_plugin/kodion/json_store/access_manager.py | 2 +- .../lib/youtube_plugin/youtube/client/login_client.py | 9 +-------- resources/lib/youtube_plugin/youtube/helper/yt_login.py | 5 +---- resources/lib/youtube_plugin/youtube/provider.py | 7 ++++++- 4 files changed, 9 insertions(+), 14 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 f71467b52..0f836475e 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -455,7 +455,7 @@ def update_access_token(self, } if expiry is not None: - details['token_expires'] = ( + details['token_expires'] = time.time() + ( min(map(int, [val for val in expiry if val])) if isinstance(expiry, (list, tuple)) else int(expiry) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index ab14c4421..aa2f9f8c4 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -10,8 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -import time - from .request_client import YouTubeRequestClient from ..youtube_exceptions import ( InvalidGrant, @@ -160,12 +158,7 @@ def refresh_token(self, token_type, refresh_token=None): '{{exc}}' .format(client=client)), raise_exc=True) - - if json_data: - access_token = json_data['access_token'] - expiry = time.time() + int(json_data.get('expires_in', 3600)) - return access_token, expiry - return '', 0 + return json_data def request_access_token(self, token_type, code=None): login_type = self.TOKEN_TYPES.get(token_type) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index 7057206ff..989837ec4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -10,8 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -import time - from ..youtube_exceptions import LoginException @@ -94,8 +92,7 @@ def _do_login(token_type): if not _access_token and not _refresh_token: _expiry = 0 else: - _expiry = (int(json_data.get('expires_in', 3600)) - + time.time()) + _expiry = int(json_data.get('expires_in', 3600)) return _access_token, _expiry, _refresh_token if json_data['error'] != 'authorization_pending': diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index dc0189f30..bfbbbd50d 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -261,7 +261,12 @@ def get_client(self, context): if not value: continue - token, expiry = client.refresh_token(token_type, value) + json_data = client.refresh_token(token_type, value) + if not json_data: + continue + + token = json_data.get('access_token') + expiry = int(json_data.get('expires_in', 3600)) if token and expiry > 0: access_tokens[token_type] = token if not token_expiry or expiry < token_expiry: From d539d9e75786ced66c84c1707a57d9a0bdac16aa Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:03:17 +1100 Subject: [PATCH 04/26] Remove unused/redundant LoginClient.authenticate method --- .../youtube/client/login_client.py | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index aa2f9f8c4..5c76f6d20 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -16,7 +16,6 @@ InvalidJSON, LoginException, ) -from ...kodion.compatibility import parse_qsl from ...kodion.logger import log_debug @@ -253,49 +252,6 @@ def request_device_and_user_code(self, token_type): raise_exc=True) return json_data - def authenticate(self, username, password): - headers = {'device': '38c6ee9a82b8b10a', - 'app': 'com.google.android.youtube', - 'User-Agent': 'GoogleAuth/1.4 (GT-I9100 KTU84Q)', - 'content-type': 'application/x-www-form-urlencoded', - 'Host': 'android.clients.google.com', - 'Connection': 'keep-alive', - 'Accept-Encoding': 'gzip'} - - post_data = { - 'device_country': self._region.lower(), - 'operatorCountry': self._region.lower(), - 'lang': self._language, - '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)) - token = params.get('Auth', '') - expires = int(params.get('Expiry', -1)) - if not token or expires == -1: - raise LoginException('Failed to get token') - - return token, expires - def _get_config_type(self, client_id, client_secret=None): """used for logging""" if client_secret is None: From 07396828fdb998e3ea0aa8597a0959e4081a42b6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 12 Oct 2024 00:01:07 +1100 Subject: [PATCH 05/26] Improve updating of bookmarks and displaying unavailable bookmarked videos --- .../youtube_plugin/kodion/items/base_item.py | 34 +++++++++++++++++ .../lib/youtube_plugin/kodion/items/utils.py | 15 ++++++-- .../youtube/helper/resource_manager.py | 3 +- .../youtube_plugin/youtube/helper/utils.py | 15 ++++++-- .../lib/youtube_plugin/youtube/helper/v3.py | 37 ++++++++++-------- .../lib/youtube_plugin/youtube/provider.py | 38 ++++++++++++++----- 6 files changed, 110 insertions(+), 32 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index fd93a9d51..7d75c9ac1 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -34,6 +34,8 @@ def __init__(self, name, uri, image=None, fanart=None): self.set_name(name) self._uri = uri + self._available = True + self._callback = None self._image = '' if image: @@ -42,6 +44,7 @@ def __init__(self, name, uri, image=None, fanart=None): if fanart: self.set_fanart(fanart) + self._bookmark_id = None self._bookmark_timestamp = None self._context_menu = None self._added_utc = None @@ -144,6 +147,22 @@ def get_uri(self): """ return self._uri + @property + def available(self): + return self._available + + @available.setter + def available(self, value): + self._available = value + + @property + def callback(self): + return self._callback + + @callback.setter + def callback(self, value): + self._callback = value + def set_image(self, image): if not image: return @@ -238,6 +257,14 @@ def get_count(self): def set_count(self, count): self._count = int(count or 0) + @property + def bookmark_id(self): + return self._bookmark_id + + @bookmark_id.setter + def bookmark_id(self, value): + self._bookmark_id = value + def set_bookmark_timestamp(self, timestamp): self._bookmark_timestamp = timestamp @@ -248,6 +275,10 @@ def get_bookmark_timestamp(self): def playable(self): return self._playable + @playable.setter + def playable(self, value): + self._playable = value + def add_artist(self, artist): if artist: if self._artists is None: @@ -337,3 +368,6 @@ def encode(self, obj, nested=False): if nested: return output return super(_Encoder, self).encode(output) + + def default(self, obj): + pass diff --git a/resources/lib/youtube_plugin/kodion/items/utils.py b/resources/lib/youtube_plugin/kodion/items/utils.py index 6344d986f..7b595da42 100644 --- a/resources/lib/youtube_plugin/kodion/items/utils.py +++ b/resources/lib/youtube_plugin/kodion/items/utils.py @@ -52,12 +52,17 @@ def from_json(json_data, *args): :param json_data: :return: """ - timestamp = args[0][1] if args and args[0] and len(args[0]) == 4 else None + if args and args[0] and len(args[0]) == 4: + bookmark_id = args[0][0] + bookmark_timestamp = args[0][1] + else: + bookmark_id = None + bookmark_timestamp = None if isinstance(json_data, string_type): if json_data == to_str(None): # Channel bookmark that will be updated. Store timestamp for update - return timestamp + return bookmark_timestamp json_data = json.loads(json_data, object_hook=_decoder) item_type = json_data.get('type') @@ -69,6 +74,10 @@ def from_json(json_data, *args): for key, value in json_data.get('data', {}).items(): if hasattr(item, key): setattr(item, key, value) - item.set_bookmark_timestamp(timestamp) + + if bookmark_id: + item.bookmark_id = bookmark_id + if bookmark_timestamp: + item.set_bookmark_timestamp(bookmark_timestamp) return item diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 76b1831fe..9feec0d75 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -322,7 +322,8 @@ def get_videos(self, for yt_item in batch.get('items', []) if yt_item } - new_data = dict(dict.fromkeys(to_update, {}), **new_data) + new_data = dict(dict.fromkeys(to_update, {'_unavailable': True}), + **new_data) result.update(new_data) self.cache_data(new_data, defer=defer_cache) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index dc8026a4f..8f512db99 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -571,11 +571,20 @@ def update_video_infos(provider, context, video_id_dict, playlist_match = __RE_PLAYLIST.match(path) for video_id, yt_item in data.items(): - if not yt_item or 'snippet' not in yt_item: + if not yt_item: + continue + + media_item = video_id_dict.get(video_id) + if not media_item: + continue + + if 'snippet' not in yt_item: + if yt_item.get('_unavailable'): + media_item.playable = False + media_item.available = False continue snippet = yt_item['snippet'] - media_item = video_id_dict[video_id] media_item.set_mediatype( CONTENT.AUDIO_TYPE if isinstance(media_item, AudioItem) else @@ -1232,7 +1241,7 @@ def filter_videos(items, return [ item for item in items - if (not item.playable or ( + if ((item.callback and item.callback(item)) or not item.playable or ( (completed and item.completed) or (live and item.live and not item.upcoming) or (premieres and upcoming and item.upcoming and not item.live) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 9e799f82d..6468bed74 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -32,7 +32,7 @@ def _process_list_response(provider, context, json_data, item_filter): yt_items = json_data.get('items', []) if not yt_items: context.log_warning('v3 response: Items list is empty') - return [] + return None video_id_dict = {} channel_id_dict = {} @@ -40,7 +40,8 @@ def _process_list_response(provider, context, json_data, item_filter): playlist_item_id_dict = {} subscription_id_dict = {} - result = [] + items = [] + do_callbacks = False new_params = {} params = context.get_params() @@ -81,7 +82,6 @@ def _process_list_response(provider, context, json_data, item_filter): description = strip_html_from_text(localised_info.get('description') or snippet.get('description') or '') - # context.log_debug(f'***********\n{item_id = }, {title = }\n{yt_item = }\n***************') thumbnails = snippet.get('thumbnails') if not thumbnails: @@ -273,13 +273,14 @@ def _process_list_response(provider, context, json_data, item_filter): if isinstance(item, VideoItem): # Set track number from playlist, or set to current list length to # match "Default" (unsorted) sort order - position = snippet.get('position') or len(result) + position = snippet.get('position') or len(items) item.set_track_number(position + 1) if '_callback' in yt_item: - yt_item['_callback'](item) + item.callback = yt_item['_callback'] + do_callbacks = True - result.append(item) + items.append(item) # this will also update the channel_id_dict with the correct channel_id # for each video. @@ -437,7 +438,7 @@ def _fetch(resource): resource['thread'] = new_thread new_thread.start() - return result + return items, do_callbacks _KNOWN_RESPONSE_KINDS = { @@ -479,19 +480,23 @@ def response_to_items(provider, provider, context, json_data, item_filter ) if not result: - return result + return [] + + items, do_callbacks = result + if not items: + return items else: raise KodionException('Unknown kind: %s' % kind) - if item_filter: - result = filter_videos(result, **item_filter) + if item_filter or do_callbacks: + items = filter_videos(items, **item_filter) if sort is not None: - result.sort(key=sort, reverse=reverse) + items.sort(key=sort, reverse=reverse) # no processing of next page item if not process_next_page or params.get('hide_next_page'): - return result + return items # next page """ @@ -516,7 +521,7 @@ def response_to_items(provider, elif current_page: new_params['page_token'] = '' else: - return result + return items page_info = json_data.get('pageInfo', {}) yt_total_results = int(page_info.get('totalResults', 0)) @@ -531,7 +536,7 @@ def response_to_items(provider, next_page = 1 new_params['page'] = 1 else: - return result + return items yt_visitor_data = json_data.get('visitorData') if yt_visitor_data: @@ -547,9 +552,9 @@ def response_to_items(provider, new_params['offset'] = offset next_page_item = NextPageItem(context, new_params) - result.append(next_page_item) + items.append(next_page_item) - return result + return items def _parse_kind(item): diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index bfbbbd50d..be056ebae 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1424,21 +1424,43 @@ def on_bookmarks(provider, context, re_match): 'items': [] } - def _update_bookmark(_id, timestamp): + def _update_bookmark(context, _id, old_item): def _update(new_item): - new_item.set_bookmark_timestamp(timestamp) - bookmarks_list.update_item(_id, repr(new_item), timestamp) + if isinstance(old_item, float): + bookmark_timestamp = old_item + elif isinstance(old_item, BaseItem): + bookmark_timestamp = old_item.get_bookmark_timestamp() + else: + return + + if new_item.available: + new_item.bookmark_id = _id + new_item.set_bookmark_timestamp(bookmark_timestamp) + new_item.callback = None + bookmarks_list.update_item( + _id, + repr(new_item), + bookmark_timestamp, + ) + else: + new_item.__dict__.update(old_item.__dict__) + new_item.bookmark_id = _id + new_item.set_bookmark_timestamp(bookmark_timestamp) + new_item.available = False + new_item.playable = False + new_item.set_title(context.get_ui().color( + 'AA808080', new_item.get_title() + )) return _update for item_id, item in items.items(): + callback = _update_bookmark(context, item_id, item) if isinstance(item, float): kind = 'youtube#channel' yt_id = item_id - callback = _update_bookmark(item_id, item) partial = True elif isinstance(item, BaseItem): - callback = None partial = False if isinstance(item, VideoItem): @@ -1452,7 +1474,9 @@ def _update(new_item): kind = 'youtube#channel' yt_id = getattr(item, 'channel_id', None) else: + kind = None yt_id = None + partial = False if not yt_id: if isinstance(item, BaseItem): @@ -1467,10 +1491,6 @@ def _update(new_item): continue kind = 'youtube#' + kind partial = True - callback = _update_bookmark( - item_id, - item.get_bookmark_timestamp(), - ) break else: if to_delete: From 01336b2e430f5f8b99f29fd940e969842faa9a6f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:52:38 +1100 Subject: [PATCH 06/26] Fix curl headers not being used when set on path of setResolvedUrl listitem --- resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py | 4 +++- .../lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 3e57e76bb..b06668d50 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -483,7 +483,9 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): if (headers and uri.startswith('http') and not (is_external or settings.default_player_web_urls())): - kwargs['path'] = '|'.join((uri, headers)) + uri = '|'.join((uri, headers)) + kwargs['path'] = uri + media_item.set_uri(uri) list_item = xbmcgui.ListItem(**kwargs) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index c51c75081..2f87d04f7 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -221,6 +221,7 @@ def run(self, provider, context, focused=None): result, show_fanart=context.get_settings().fanart_selection(), ) + uri = result.get_uri() result = xbmcplugin.addDirectoryItem(self.handle, url=uri, listitem=item) From 88dfa601be7127c2caab2d32c8f4c8aa885b1591 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:54:37 +1100 Subject: [PATCH 07/26] Explicitly set http server protocol version to HTTP/1.1 --- resources/lib/youtube_plugin/kodion/network/http_server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 276a423c3..132490feb 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -51,6 +51,9 @@ def server_close(self): class RequestHandler(BaseHTTPRequestHandler, object): + protocol_version = 'HTTP/1.1' + server_version = 'plugin.video.youtube/1.0' + _context = None requests = None BASE_PATH = xbmcvfs.translatePath(TEMP_PATH) From 64d76a8d6e5a3337f0e3ab43584ac0f6669d8f29 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:57:16 +1100 Subject: [PATCH 08/26] Fix HEAD requests to MPD manifests after 74c7d04 --- .../kodion/network/http_server.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 132490feb..888969c4c 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -241,17 +241,23 @@ def do_HEAD(self): self.send_error(403) elif self.path.startswith(PATHS.MPD): - 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: + try: + file = dict(parse_qsl(urlsplit(self.path).query)).get('file') + if file: + file_path = os.path.join(self.BASE_PATH, file) + else: + file_path = None + raise IOError + + file_size = os.path.getsize(file_path) self.send_response(200) self.send_header('Content-Type', 'application/dash+xml') - self.send_header('Content-Length', - str(os.path.getsize(filepath))) + self.send_header('Content-Length', str(file_size)) self.end_headers() + except IOError: + response = ('File Not Found: |{path}| -> |{file_path}|' + .format(path=self.path, file_path=file_path)) + self.send_error(404, response) elif self.path.startswith(PATHS.REDIRECT): self.send_error(404) From 6fe83fecccfee6696417f9937ce7394a1ab782b7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:33:20 +1100 Subject: [PATCH 09/26] Rename StreamInfo._make_curl_headers to StreamInfo._make_header_string - Ensure original headers are not modified - Allow update from optional new headers - Python 2 compatible --- .../youtube_plugin/youtube/helper/stream_info.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 71e25b5ad..09d658e70 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -975,13 +975,17 @@ def _get_player_js(self): return result @staticmethod - def _make_curl_headers(headers, cookies=None): + def _make_header_string(headers, cookies=None, new_headers=None): + if cookies or new_headers: + headers = headers.copy() if cookies: headers['Cookie'] = '; '.join([ '='.join((cookie.name, cookie.value)) for cookie in cookies ]) + if new_headers: + headers.update(new_headers) # Headers used in xbmc_items.video_playback_item' - return urlencode(headers, safe='/', quote_via=quote) + return urlencode(headers) @staticmethod def _normalize_url(url): @@ -1014,7 +1018,7 @@ def _update_from_hls(self, client_name = 'web' client_data = {'json': {'videoId': self.video_id}} headers = self.build_client(client_name, client_data)['headers'] - curl_headers = self._make_curl_headers(headers, cookies=None) + curl_headers = self._make_header_string(headers) if meta_info is None: meta_info = {'video': {}, @@ -1112,7 +1116,7 @@ def _update_from_streams(self, client_name = 'web' client_data = {'json': {'videoId': self.video_id}} headers = self.build_client(client_name, client_data)['headers'] - curl_headers = self._make_curl_headers(headers, cookies=None) + curl_headers = self._make_header_string(headers) if meta_info is None: meta_info = {'video': {}, @@ -1515,7 +1519,7 @@ def load_stream_info(self, video_id): # the stream during playback. The YT player doesn't seem to use any # cookies when doing that, so for now cookies are ignored. # curl_headers = self._make_curl_headers(headers, cookies) - curl_headers = self._make_curl_headers(client['headers'], cookies=None) + curl_headers = self._make_header_string(client['headers']) microformat = (result.get('microformat', {}) .get('playerMicroformatRenderer', {})) From d2586b70676c8bd0a9603fc73ac2867d8d18ab79 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:36:07 +1100 Subject: [PATCH 10/26] Don't access match group using __getitem__ to restore Python 2 compatibility --- resources/lib/youtube_plugin/youtube/helper/stream_info.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 09d658e70..0be52fae2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -1087,9 +1087,10 @@ def _update_from_hls(self, playback_stats=playback_stats, ) if yt_format is None: + stream_info = redact_ip(match.group(1)) self._context.log_debug('Unknown itag: {itag}\n{stream}' .format(itag=itag, - stream=redact_ip(match[0]))) + stream=stream_info)) if (not yt_format or (yt_format.get('hls/video') and not yt_format.get('hls/audio'))): From db867f57ccd079ae84defd035acd1cd0a9d31b4c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:26:40 +1100 Subject: [PATCH 11/26] Further updates to standardise logging for script, plugin and service endpoints Follows 818b4a10a1ba83c1137efbb724d61537f8f634de --- resources/lib/youtube_plugin/kodion/plugin_runner.py | 10 +++++----- .../lib/youtube_plugin/kodion/script_actions.py | 12 ++++++------ .../lib/youtube_plugin/kodion/service_runner.py | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index 8dcee5dd7..c0e8b86d8 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -46,11 +46,11 @@ def run(context=_context, params[key] = '' system_version = context.get_system_version() - context.log_notice('Plugin: Running |v{version}|\n' - 'Kodi: |v{kodi}|\n' - 'Python: |v{python}|\n' - 'Path: |{path}|\n' - 'Params: |{params}|' + context.log_notice('Plugin: Running |v{version}|' + '\n\tKodi: |v{kodi}|' + '\n\tPython: |v{python}|' + '\n\tPath: |{path}|' + '\n\tParams: |{params}|' .format(version=context.get_version(), kodi=str(system_version), python=system_version.get_python_version(), diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index 403f79b92..ca83750bd 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -647,12 +647,12 @@ def run(argv): params = dict(parse_qsl(args.query)) system_version = context.get_system_version() - context.log_notice('Script: Running |v{version}|\n' - 'Kodi: |v{kodi}|\n' - 'Python: |v{python}|\n' - 'Category: |{category}|\n' - 'Action: |{action}|\n' - 'Params: |{params}|' + context.log_notice('Script: Running |v{version}|' + '\n\tKodi: |v{kodi}|' + '\n\tPython: |v{python}|' + '\n\tCategory: |{category}|' + '\n\tAction: |{action}|' + '\n\tParams: |{params}|' .format(version=context.get_version(), kodi=str(system_version), python=system_version.get_python_version(), diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index da8403d3d..4666858d7 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -30,9 +30,9 @@ def run(): provider = Provider() system_version = context.get_system_version() - context.log_notice('Service: Starting |v{version}|\n' - 'Kodi: |v{kodi}|\n' - 'Python: |v{python}|' + context.log_notice('Service: Starting |v{version}|' + '\n\tKodi: |v{kodi}|' + '\n\tPython: |v{python}|' .format(version=context.get_version(), kodi=str(system_version), python=system_version.get_python_version())) From 42e314b0ae2fed306658654eb58b96af78ffb3a3 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:49:04 +1100 Subject: [PATCH 12/26] Further updates/tidy ups to improve logging Follows 818b4a10a1ba83c1137efbb724d61537f8f634de --- .../kodion/plugin/xbmc/xbmc_plugin.py | 2 +- .../youtube/client/login_client.py | 26 ++--- .../youtube/client/request_client.py | 2 +- .../youtube_plugin/youtube/client/youtube.py | 26 ++--- .../youtube/helper/stream_info.py | 104 ++++++++++-------- .../youtube/helper/subtitles.py | 2 +- .../youtube_plugin/youtube/helper/yt_login.py | 8 +- .../youtube_plugin/youtube/helper/yt_play.py | 2 +- .../lib/youtube_plugin/youtube/provider.py | 4 +- 9 files changed, 95 insertions(+), 81 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 2f87d04f7..6fcf25b25 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -177,7 +177,7 @@ def run(self, provider, context, focused=None): except KodionException as exc: result = options = None if provider.handle_exception(context, exc): - context.log_error('XbmcRunner.run - {exc}:\n{details}'.format( + context.log_error('XbmcRunner.run - {exc!r}:\n{details}'.format( exc=exc, details=''.join(format_stack()) )) ui.on_ok('Error in ContentProvider', exc.__str__()) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 5c76f6d20..9b3549d08 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -106,7 +106,7 @@ def revoke(self, refresh_token): response_hook=LoginClient._response_hook, error_hook=LoginClient._error_hook, error_title='Logout Failed', - error_info='Revoke failed: {exc}', + error_info='Revoke failed: {exc!r}', raise_exc=True) def refresh_token(self, token_type, refresh_token=None): @@ -135,15 +135,15 @@ def refresh_token(self, token_type, refresh_token=None): 'grant_type': 'refresh_token'} config_type = self._get_config_type(client_id, client_secret) - client = (('config_type: |{config_type}|\n' - 'client_id: |{id_start}...{id_end}|\n' - 'client_secret: |{secret_start}...{secret_end}|') + client = (('\n\tconfig_type: |{config_type}|' + '\n\tclient_id: |{id_start}...{id_end}|' + '\n\tclient_secret: |{secret_start}...{secret_end}|') .format(config_type=config_type, id_start=client_id[:3], id_end=client_id[-5:], secret_start=client_secret[:3], secret_end=client_secret[-3:])) - log_debug('Refresh token\n{0}'.format(client)) + log_debug('Refresh token:{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -154,7 +154,7 @@ def refresh_token(self, token_type, refresh_token=None): error_title='Login Failed', error_info=('Refresh token failed\n' '{client}:\n' - '{{exc}}' + '{{exc!r}}' .format(client=client)), raise_exc=True) return json_data @@ -185,15 +185,15 @@ def request_access_token(self, token_type, code=None): 'grant_type': 'http://oauth.net/grant_type/device/1.0'} config_type = self._get_config_type(client_id, client_secret) - client = (('config_type: |{config_type}|\n' - 'client_id: |{id_start}...{id_end}|\n' - 'client_secret: |{secret_start}...{secret_end}|') + client = (('\n\tconfig_type: |{config_type}|' + '\n\tclient_id: |{id_start}...{id_end}|' + '\n\tclient_secret: |{secret_start}...{secret_end}|') .format(config_type=config_type, id_start=client_id[:3], id_end=client_id[-5:], secret_start=client_secret[:3], secret_end=client_secret[-3:])) - log_debug('Requesting access token\n{0}'.format(client)) + log_debug('Requesting access token:{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -231,12 +231,12 @@ def request_device_and_user_code(self, token_type): 'scope': 'https://www.googleapis.com/auth/youtube'} config_type = self._get_config_type(client_id) - client = (('config_type: |{config_type}|\n' - 'client_id: |{id_start}...{id_end}|') + client = (('\n\tconfig_type: |{config_type}|' + '\n\tclient_id: |{id_start}...{id_end}|') .format(config_type=config_type, id_start=client_id[:3], id_end=client_id[-5:])) - log_debug('Requesting device and user code\n{0}'.format(client)) + log_debug('Requesting device and user code:{0}'.format(client)) json_data = self.request(self.DEVICE_CODE_URL, method='POST', diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 8709aabd1..38a66b238 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -294,7 +294,7 @@ class YouTubeRequestClient(BaseRequestsClass): }, 'params': { 'key': ValueError, - 'prettyPrint': 'false' + 'prettyPrint': False, }, }, } diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 005896c01..295d2f926 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -115,7 +115,7 @@ class YouTube(LoginClient): }, 'params': { 'key': None, - 'prettyPrint': 'false' + 'prettyPrint': False, }, }, } @@ -1944,8 +1944,8 @@ def _perform(_playlist_idx, _page_token, _offset, _result): def _response_hook(self, **kwargs): response = kwargs['response'] - self._context.log_debug('API response: |{0.status_code}|\n' - 'headers: |{0.headers}|'.format(response)) + self._context.log_debug('API response: |{0.status_code}|' + '\n\theaders: |{0.headers}|'.format(response)) if response.status_code == 204 and 'no_content' in kwargs: return True try: @@ -2005,9 +2005,9 @@ def _error_hook(self, **kwargs): title, time_ms=timeout) - info = ('API error: {reason}\n' - 'exc: |{exc!r}|\n' - 'message: |{message}|') + info = ('API error: {reason}' + '\n\texc: |{exc!r}|' + '\n\tmessage: |{message}|') details = {'reason': reason, 'message': message} return '', info, details, data, False, exception @@ -2085,13 +2085,13 @@ def api_request(self, log_headers = None context = self._context - context.log_debug('API request:\n' - 'version: |{version}|\n' - 'method: |{method}|\n' - 'path: |{path}|\n' - 'params: |{params}|\n' - 'post_data: |{data}|\n' - 'headers: |{headers}|' + context.log_debug('API request:' + '\n\tversion: |{version}|' + '\n\tmethod: |{method}|' + '\n\tpath: |{path}|' + '\n\tparams: |{params}|' + '\n\tpost_data: |{data}|' + '\n\theaders: |{headers}|' .format(version=version, method=method, path=path, diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 0be52fae2..02fd3c76c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -766,18 +766,18 @@ def _error_hook(**kwargs): exception = None if not json_data or 'error' not in json_data: - info = ('exc: |{exc}|\n' - 'video_id: {video_id}, client: {client}, auth: {auth}') + info = ('exc: |{exc!r}|' + '\n\tvideo_id: {video_id}, client: {client}, auth: {auth}') return None, info, kwargs, data, None, exception details = json_data['error'] reason = details.get('errors', [{}])[0].get('reason', 'Unknown') message = details.get('message', 'Unknown error') - info = ('exc: |{exc}|\n' - 'reason: {reason}\n' - 'message: |{message}|\n' - 'video_id: {video_id}, client: {client}, auth: {auth}') + info = ('exc: |{exc!r}|' + '\n\treason: |{reason}|' + '\n\tmessage: |{message}|' + '\n\tvideo_id: {video_id}, client: {client}, auth: {auth}') kwargs['message'] = message kwargs['reason'] = reason return None, info, kwargs, data, None, exception @@ -1029,12 +1029,14 @@ def _update_from_hls(self, if playback_stats is None: playback_stats = {} - settings = self._context.get_settings() + context = self._context + settings = context.get_settings() if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() + log_debug = context.log_debug for url in urls: result = self.request( @@ -1088,9 +1090,8 @@ def _update_from_hls(self, ) if yt_format is None: stream_info = redact_ip(match.group(1)) - self._context.log_debug('Unknown itag: {itag}\n{stream}' - .format(itag=itag, - stream=stream_info)) + log_debug('Unknown itag: {itag}\n{stream}' + .format(itag=itag, stream=stream_info)) if (not yt_format or (yt_format.get('hls/video') and not yt_format.get('hls/audio'))): @@ -1128,12 +1129,14 @@ def _update_from_streams(self, if playback_stats is None: playback_stats = {} - settings = self._context.get_settings() + context = self._context + settings = context.get_settings() if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() + log_debug = context.log_debug for stream_map in streams: itag = str(stream_map['itag']) @@ -1172,9 +1175,8 @@ def _update_from_streams(self, stream_map['conn'] = redact_ip(conn) if stream: stream_map['stream'] = redact_ip(stream) - self._context.log_debug('Unknown itag: {itag}\n{stream}'.format( - itag=itag, stream=stream_map, - )) + log_debug('Unknown itag: {itag}\n{stream}' + .format(itag=itag, stream=stream_map)) if (not yt_format or (yt_format.get('dash/video') and not yt_format.get('dash/audio'))): @@ -1230,8 +1232,9 @@ def _process_signature_cipher(self, stream_map): signature = self._cipher.get_signature(encrypted_signature) except Exception as exc: self._context.log_error('VideoInfo._process_signature_cipher - ' - 'failed to extract URL from |{sig}|\n' - '{exc}:\n{details}'.format( + 'failed to extract URL from |{sig}|' + '\n\texc: |{exc!r}|' + '\n\tdetails: |{details}|'.format( sig=encrypted_signature, exc=exc, details=''.join(format_stack()) @@ -1341,7 +1344,8 @@ def _get_error_details(self, playability_status, details=None): def load_stream_info(self, video_id): self.video_id = video_id - settings = self._context.get_settings() + context = self._context + settings = context.get_settings() age_gate_enabled = settings.age_gate() audio_only = self._audio_only ask_for_quality = self._ask_for_quality @@ -1362,6 +1366,9 @@ def load_stream_info(self, video_id): video_info_url = 'https://www.youtube.com/youtubei/v1/player' + log_debug = context.log_debug + log_warning = context.log_warning + abort_reasons = { 'country', 'not available', @@ -1441,7 +1448,7 @@ def load_stream_info(self, video_id): 'ERROR', 'UNPLAYABLE', }: - self._context.log_warning( + log_warning( 'Failed to retrieve video info - ' 'video_id: {0}, client: {1}, auth: {2},\n' 'status: {3}, reason: {4}'.format( @@ -1461,7 +1468,7 @@ def load_stream_info(self, video_id): abort = True break else: - self._context.log_debug( + log_debug( 'Unknown playabilityStatus in player response:\n|{0}|' .format(playability) ) @@ -1470,7 +1477,7 @@ def load_stream_info(self, video_id): break if status == 'OK': - self._context.log_debug( + log_debug( 'Retrieved video info - ' 'video_id: {0}, client: {1}, auth: {2}'.format( video_id, @@ -1645,7 +1652,7 @@ def load_stream_info(self, video_id): playback_stats, ) - subtitles = Subtitles(self._context, video_id) + subtitles = Subtitles(context, video_id) query_subtitles = client.get('_query_subtitles') if (not is_live or live_dvr) and ( query_subtitles is True @@ -1726,11 +1733,12 @@ def load_stream_info(self, video_id): elif default_lang['is_asr']: title.append(' [ASR]') + localize = context.localize for _prop in ('multi_lang', 'multi_audio'): if not main_stream.get(_prop): continue _prop = 'stream.' + _prop - title.extend((' [', self._context.localize(_prop), ']')) + title.extend((' [', localize(_prop), ']')) if len(title) > 1: yt_format['title'] = ''.join(yt_format['title']) @@ -1756,11 +1764,12 @@ def load_stream_info(self, video_id): return stream_list.values() def _process_stream_data(self, stream_data, default_lang_code='und'): - _settings = self._context.get_settings() + context = self._context + settings = context.get_settings() audio_only = self._audio_only - qualities = _settings.mpd_video_qualities() - isa_capabilities = self._context.inputstream_adaptive_capabilities() - stream_features = _settings.stream_features() + qualities = settings.mpd_video_qualities() + isa_capabilities = context.inputstream_adaptive_capabilities() + 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 @@ -1768,7 +1777,8 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): fps_map = (self.INTEGER_FPS_SCALE if 'no_frac_fr_hint' in stream_features else self.FRACTIONAL_FPS_SCALE) - stream_select = _settings.stream_select() + stream_select = settings.stream_select() + localize = context.localize audio_data = {} video_data = {} @@ -1834,18 +1844,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('stream.original') + label = localize('stream.original') elif role_type == 3: role = 'dub' - label = self._context.localize('stream.dubbed') + label = localize('stream.dubbed') elif role_type == 2: role = 'description' - label = self._context.localize('stream.descriptive') + label = 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('stream.alternate') + label = localize('stream.alternate') mime_group = ''.join(( mime_type, '_', language_code, '.', role_str, @@ -1865,12 +1875,12 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): role = 'main' role_type = 4 role_str = '4' - label = self._context.localize('stream.original') + label = localize('stream.original') mime_group = mime_type sample_rate = int(stream.get('audioSampleRate', '0'), 10) height = width = fps = frame_rate = hdr = None - language = self._context.get_language_name(language_code) + language = context.get_language_name(language_code) label = '{0} ({1} kbps)'.format(label, bitrate // 1000) if channels > 2 or 'auto' not in stream_select: quality_group = ''.join(( @@ -1992,7 +2002,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): data[mime_group][itag] = data[quality_group][itag] = details if not video_data and not audio_only: - self._context.log_debug('Generate MPD: No video mime-types found') + context.log_debug('Generate MPD: No video mime-types found') return None, None def _stream_sort(stream): @@ -2047,9 +2057,12 @@ def _generate_mpd_manifest(self, if not video_data or not audio_data: return None, None + context = self._context + log_error = context.log_error + if not self.BASE_PATH: - self._context.log_error('VideoInfo._generate_mpd_manifest - ' - 'unable to access temp directory') + log_error('VideoInfo._generate_mpd_manifest - ' + 'unable to access temp directory') return None, None def _filter_group(previous_group, previous_stream, item): @@ -2096,11 +2109,12 @@ def _filter_group(previous_group, previous_stream, item): ) return skip_group - _settings = self._context.get_settings() - stream_features = _settings.stream_features() + settings = context.get_settings() + stream_features = settings.stream_features() do_filter = 'filter' in stream_features frame_rate_hint = 'no_fr_hint' not in stream_features - stream_select = _settings.stream_select() + stream_select = settings.stream_select() + localize = context.localize main_stream = { 'audio': audio_data[0][1][0], @@ -2147,7 +2161,7 @@ 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('stream.automatic'), + or localize('stream.automatic'), stream['label'] ) if stream == main_stream[media_type]: @@ -2281,7 +2295,7 @@ def _filter_group(previous_group, previous_stream, item): set_id += 1 if subs_data: - translation_lang = self._context.localize('subtitles.translation') + translation_lang = localize('subtitles.translation') for lang_id, subtitle in subs_data.items(): lang_code = subtitle['lang'] label = language = subtitle['language'] @@ -2342,14 +2356,14 @@ def _filter_group(previous_group, previous_stream, item): with xbmcvfs.File(filepath, 'w') as mpd_file: success = mpd_file.write(output) except (IOError, OSError): - self._context.log_error('VideoInfo._generate_mpd_manifest - ' - 'file write failed for: {file}' - .format(file=filepath)) + log_error('VideoInfo._generate_mpd_manifest - ' + 'file write failed for: {file}' + .format(file=filepath)) success = False if success: return urlunsplit(( 'http', - get_connect_address(self._context, as_netloc=True), + get_connect_address(context, as_netloc=True), PATHS.MPD, urlencode({'file': filename}), '', diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 741ed0e47..cdad8fe61 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -394,7 +394,7 @@ def _get_url(self, track, lang=None): response = BaseRequestsClass(context=self._context).request( subtitle_url, headers=self.headers, - error_info=('Subtitles._get_url - GET failed for: {lang}: {{exc}}' + error_info=('Subtitles._get_url - GET failed for: {lang}: {{exc!r}}' .format(lang=lang)) ) response = response and response.text diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index 989837ec4..b7495139b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -128,10 +128,10 @@ def _do_login(token_type): refresh_token = None context.log_debug('YouTube Login:' - 'Type: |{0}|\n' - 'Access token: |{1}|\n' - 'Refresh token: |{2}|\n' - 'Expires: |{3}|' + '\n\tType: |{0}|' + '\n\tAccess token: |{1}|' + '\n\tRefresh token: |{2}|' + '\n\tExpires: |{3}|' .format(token, bool(access_token), bool(refresh_token), diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index a6eeac809..7b00d305f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -73,7 +73,7 @@ def _play_stream(provider, context): audio_only, settings.use_mpd_videos()) except YouTubeException as exc: - context.log_error('yt_play.play_video - {exc}:\n{details}'.format( + context.log_error('yt_play.play_video - {exc!r}:\n{details}'.format( exc=exc, details=''.join(format_stack()) )) ui.show_notification(message=exc.get_message()) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index be056ebae..1c56286f2 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -134,8 +134,8 @@ def get_dev_config(context, addon_id, dev_configs): dev_main = None if not dev_main: - context.log_error('Invalid developer config: |{dev_config}|\n' - 'expected: |{{' + context.log_error('Invalid developer config: |{dev_config}|' + '\n\texpected: |{{' ' "origin": ADDON_ID,' ' "main": {{' ' "system": SYSTEM_NAME,' From 7d70432d4494392c9fa4ab9bd3e543f2d7dba306 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:27:07 +1100 Subject: [PATCH 13/26] Rename StreamInfo._make_header_string to StreamInfo._prepare_headers Follow up to 6fe83fecccfee6696417f9937ce7394a1ab782b7 --- .../youtube_plugin/youtube/helper/stream_info.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 02fd3c76c..258bfa5e3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -975,7 +975,7 @@ def _get_player_js(self): return result @staticmethod - def _make_header_string(headers, cookies=None, new_headers=None): + def _prepare_headers(headers, to_string=True, cookies=None, new_headers=None): if cookies or new_headers: headers = headers.copy() if cookies: @@ -984,8 +984,10 @@ def _make_header_string(headers, cookies=None, new_headers=None): ]) if new_headers: headers.update(new_headers) - # Headers used in xbmc_items.video_playback_item' - return urlencode(headers) + if to_string: + # Headers used in xbmc_items.video_playback_item + return urlencode(headers) + return headers @staticmethod def _normalize_url(url): @@ -1018,7 +1020,7 @@ def _update_from_hls(self, client_name = 'web' client_data = {'json': {'videoId': self.video_id}} headers = self.build_client(client_name, client_data)['headers'] - curl_headers = self._make_header_string(headers) + curl_headers = self._prepare_headers(headers) if meta_info is None: meta_info = {'video': {}, @@ -1118,7 +1120,7 @@ def _update_from_streams(self, client_name = 'web' client_data = {'json': {'videoId': self.video_id}} headers = self.build_client(client_name, client_data)['headers'] - curl_headers = self._make_header_string(headers) + curl_headers = self._prepare_headers(headers) if meta_info is None: meta_info = {'video': {}, @@ -1527,7 +1529,7 @@ def load_stream_info(self, video_id): # the stream during playback. The YT player doesn't seem to use any # cookies when doing that, so for now cookies are ignored. # curl_headers = self._make_curl_headers(headers, cookies) - curl_headers = self._make_header_string(client['headers']) + curl_headers = self._prepare_headers(client['headers']) microformat = (result.get('microformat', {}) .get('playerMicroformatRenderer', {})) From 8f76befeb53655a49cd83a01b18b54f15fa20ce4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:30:35 +1100 Subject: [PATCH 14/26] Properly distinguish between VP9 and VP9.2 with HDR info --- .../youtube_plugin/kodion/context/xbmc/xbmc_context.py | 1 + .../lib/youtube_plugin/youtube/helper/stream_info.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 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 c1023005d..a05d4a4dc 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -683,6 +683,7 @@ def use_inputstream_adaptive(self, prompt=False): 'av01': loose_version('20.3.0'), 'vp8': False, 'vp9': loose_version('2.3.14'), + 'vp9.2': loose_version('2.4.0'), } def inputstream_adaptive_capabilities(self, capability=None): diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 258bfa5e3..9d7ab213a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -662,8 +662,8 @@ class StreamInfo(YouTubeRequestClient): QUALITY_FACTOR = { # video - order based on comparative compression ratio 'av01': 1, + 'vp9.2': 0.75, 'vp9': 0.75, - 'vp09': 0.75, 'vp8': 0.55, 'vp08': 0.55, 'avc1': 0.5, @@ -1816,8 +1816,10 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): codec = re.match(r'codecs="([a-z0-9]+([.\-][0-9](?="))?)', codecs) if codec: codec = codec.group(1) - if codec.startswith(('vp9', 'vp09')): + if codec.startswith('vp9'): codec = 'vp9' + elif codec.startswith('vp09'): + codec = 'vp9.2' elif codec.startswith('dts'): codec = 'dts' if codec not in isa_capabilities: @@ -1903,7 +1905,8 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): if fps > 30 and not allow_hfr: continue - hdr = 'HDR' in stream.get('qualityLabel', '') + hdr = ('colorInfo' in stream + or 'HDR' in stream.get('qualityLabel', '')) if hdr and not allow_hdr: continue From 54cdb9c5a0c8d91f19066639e454300e1a1881db Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:33:25 +1100 Subject: [PATCH 15/26] Fix kodion.utils.methods.redact_ip not defined in __all__ --- resources/lib/youtube_plugin/kodion/utils/methods.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index bdb55f6fa..38eac1d92 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -32,6 +32,7 @@ 'make_dirs', 'merge_dicts', 'print_items', + 'redact_ip', 'rm_dir', 'seconds_to_duration', 'select_stream', From e73d7044bca63533566ea97b28e5b399f3c0410b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:37:53 +1100 Subject: [PATCH 16/26] Add new kodion.utils.methods.entity_escape method --- .../youtube_plugin/kodion/utils/__init__.py | 2 ++ .../youtube_plugin/kodion/utils/methods.py | 12 +++++++++ .../youtube/helper/stream_info.py | 26 ++++--------------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 659ffb1de..8820cfd01 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -13,6 +13,7 @@ from . import datetime_parser from .methods import ( duration_to_seconds, + entity_escape, find_video_id, friendly_number, get_kodi_setting_bool, @@ -37,6 +38,7 @@ 'current_system_version', 'datetime_parser', 'duration_to_seconds', + 'entity_escape', 'find_video_id', 'friendly_number', 'get_kodi_setting_bool', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 38eac1d92..6847c5d97 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -23,6 +23,7 @@ __all__ = ( 'duration_to_seconds', + 'entity_escape', 'find_video_id', 'friendly_number', 'get_kodi_setting_bool', @@ -316,3 +317,14 @@ def wait(timeout=None): def redact_ip(url): return re.sub(r'([?&/])ip([=/])[^?&/]+', r'\g<1>ip\g<2>', url) + + +def entity_escape(input, + entities=str.maketrans({ + '&': '&', + '"': '"', + '<': '<', + '>': '>', + '\'': ''', + })): + return input.translate(entities) diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 9d7ab213a..f411a1c68 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -35,7 +35,7 @@ ) from ...kodion.constants import PATHS, TEMP_PATH from ...kodion.network import get_connect_address -from ...kodion.utils import make_dirs, redact_ip +from ...kodion.utils import entity_escape, make_dirs, redact_ip class StreamInfo(YouTubeRequestClient): @@ -1965,14 +1965,10 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): url = unquote(url) primary_url, secondary_url = self._process_url_params(url) - primary_url = (primary_url.replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) details = { 'mimeType': mime_type, - 'baseUrl': primary_url, + 'baseUrl': entity_escape(primary_url), 'mediaType': media_type, 'container': container, 'codecs': codecs, @@ -1999,11 +1995,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): 'channels': channels, } if secondary_url: - secondary_url = (secondary_url.replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) - details['baseUrlSecondary'] = secondary_url + details['baseUrlSecondary'] = entity_escape(secondary_url) data[mime_group][itag] = data[quality_group][itag] = details if not video_data and not audio_only: @@ -2222,11 +2214,7 @@ def _filter_group(previous_group, previous_stream, item): )) if license_url: - license_url = (license_url - .replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) + license_url = entity_escape(license_url) output.extend(( '\t\t\t", ">")) + url = entity_escape(unquote(subtitle['url'])) output.extend(( '\t\t Date: Thu, 24 Oct 2024 11:41:32 +1100 Subject: [PATCH 17/26] Misc tidy ups --- .../kodion/constants/__init__.py | 2 + .../kodion/context/abstract_context.py | 4 +- .../kodion/items/xbmc/xbmc_items.py | 7 +- .../kodion/monitors/service_monitor.py | 10 +++ .../kodion/plugin/xbmc/xbmc_plugin.py | 15 ++-- .../youtube/helper/stream_info.py | 16 ++-- .../youtube/helper/url_to_item_converter.py | 3 +- .../youtube_plugin/youtube/helper/yt_play.py | 27 ++++++- .../lib/youtube_plugin/youtube/provider.py | 74 +++++++++---------- 9 files changed, 96 insertions(+), 62 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index b0431d4fe..327bf5db1 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -65,6 +65,7 @@ PLAY_FORCE_AUDIO = 'audio_only' PLAY_PROMPT_QUALITY = 'ask_for_quality' PLAY_PROMPT_SUBTITLES = 'prompt_for_subtitles' +PLAY_STRM = 'strm' PLAY_TIMESHIFT = 'timeshift' PLAY_WITH = 'play_with' @@ -124,6 +125,7 @@ 'PLAY_FORCE_AUDIO', 'PLAY_PROMPT_QUALITY', 'PLAY_PROMPT_SUBTITLES', + 'PLAY_STRM', 'PLAY_TIMESHIFT', 'PLAY_WITH', diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 5b150e511..7f1db2a7c 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -19,6 +19,7 @@ PLAY_FORCE_AUDIO, PLAY_PROMPT_QUALITY, PLAY_PROMPT_SUBTITLES, + PLAY_STRM, PLAY_TIMESHIFT, PLAY_WITH, VALUE_FROM_STR, @@ -45,6 +46,7 @@ class AbstractContext(object): PLAY_FORCE_AUDIO, PLAY_PROMPT_SUBTITLES, PLAY_PROMPT_QUALITY, + PLAY_STRM, PLAY_TIMESHIFT, PLAY_WITH, 'confirmed', @@ -58,10 +60,8 @@ class AbstractContext(object): 'incognito', 'location', 'logged_in', - 'play', 'resume', 'screensaver', - 'strm', 'window_return', } _INT_PARAMS = { diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index b06668d50..8cf9e388f 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -25,6 +25,7 @@ PLAYLISTITEM_ID, PLAYLIST_ID, PLAY_COUNT, + PLAY_STRM, PLAY_TIMESHIFT, PLAY_WITH, SUBSCRIPTION_ID, @@ -398,10 +399,12 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): context.log_debug('Converting %s |%s|' % (media_item.__class__.__name__, redact_ip(uri))) + params = context.get_params() settings = context.get_settings() ui = context.get_ui() + is_external = ui.get_property(PLAY_WITH) - is_strm = context.get_param('strm') + is_strm = params.get(PLAY_STRM) mime_type = None if is_strm: @@ -510,7 +513,7 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): if media_item.subtitles: list_item.setSubtitles(media_item.subtitles) - resume = context.get_param('resume') + resume = params.get('resume') set_info(list_item, media_item, props, resume=resume) return list_item diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index fea592596..01cd50892 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -87,20 +87,26 @@ def refresh_container(self, force=False): def onNotification(self, sender, method, data): if sender != ADDON_ID: return + group, separator, event = method.partition('.') + if event == WAKEUP: if not isinstance(data, dict): data = json.loads(data) if not data: return + target = data.get('target') + if target == PLUGIN_WAKEUP: self.interrupt = True + elif target == SERVER_WAKEUP: if not self.httpd and self.httpd_required(): self.start_httpd() if self.httpd_sleep_allowed: self.httpd_sleep_allowed = None + elif target == CHECK_SETTINGS: state = data.get('state') if state == 'defer': @@ -109,16 +115,20 @@ def onNotification(self, sender, method, data): self._settings_state = state self.onSettingsChanged() self._settings_state = None + if data.get('response_required'): self.set_property(WAKEUP, target) + elif event == REFRESH_CONTAINER: self.refresh_container() + elif event == CONTAINER_FOCUS: if data: data = json.loads(data) if not data or not self.is_plugin_container(check_all=True): return xbmc.executebuiltin('SetFocus({0},{1},absolute)'.format(*data)) + elif event == RELOAD_ACCESS_MANAGER: self._context.reload_access_manager() self.refresh_container() diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 6fcf25b25..01926f861 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -64,10 +64,9 @@ class XbmcPlugin(AbstractPlugin): def __init__(self): super(XbmcPlugin, self).__init__() - self.handle = None def run(self, provider, context, focused=None): - self.handle = context.get_handle() + handle = context.get_handle() ui = context.get_ui() route = ui.pop_property(REROUTE_PATH) @@ -81,7 +80,7 @@ def run(self, provider, context, focused=None): break xbmcplugin.endOfDirectory( - self.handle, + handle, succeeded=False, ) @@ -211,6 +210,8 @@ def run(self, provider, context, focused=None): if options.get(provider.RESULT_FORCE_RESOLVE): result = result[0] + else: + result = None if result and result.__class__.__name__ in self._PLAY_ITEM_MAP: uri = result.get_uri() @@ -222,7 +223,7 @@ def run(self, provider, context, focused=None): show_fanart=context.get_settings().fanart_selection(), ) uri = result.get_uri() - result = xbmcplugin.addDirectoryItem(self.handle, + result = xbmcplugin.addDirectoryItem(handle, url=uri, listitem=item) if route: @@ -230,7 +231,7 @@ def run(self, provider, context, focused=None): playlist_player.play_item(item=uri, listitem=item) else: context.wakeup(SERVER_WAKEUP, timeout=5) - xbmcplugin.setResolvedUrl(self.handle, + xbmcplugin.setResolvedUrl(handle, succeeded=result, listitem=item) @@ -257,7 +258,7 @@ def run(self, provider, context, focused=None): if item_count: context.apply_content() succeeded = xbmcplugin.addDirectoryItems( - self.handle, items, item_count + handle, items, item_count ) cache_to_disc = options.get(provider.RESULT_CACHE_TO_DISC, True) update_listing = options.get(provider.RESULT_UPDATE_LISTING, False) @@ -269,7 +270,7 @@ def run(self, provider, context, focused=None): update_listing = True xbmcplugin.endOfDirectory( - self.handle, + handle, succeeded=succeeded, updateListing=update_listing, cacheToDisc=cache_to_disc, diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index f411a1c68..6d24431d7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -2229,7 +2229,7 @@ def _filter_group(previous_group, previous_stream, item): num_streams = len(streams) if media_type == 'audio': - output.extend((( + output.extend([( '\t\t\t\n' '\t\t\t\n' ).format( - quality=(idx + 1), priority=(num_streams - idx), **stream - ) for idx, stream in enumerate(streams))) + quality=(idx + 1), + priority=(num_streams - idx), + **stream + ) for idx, stream in enumerate(streams)]) elif media_type == 'video': - output.extend((( + output.extend([( '\t\t\t\n' '\t\t\t\n' ).format( - quality=(idx + 1), priority=(num_streams - idx), **stream - ) for idx, stream in enumerate(streams))) + quality=(idx + 1), + priority=(num_streams - idx), + **stream + ) for idx, stream in enumerate(streams)]) output.append('\t\t\n') set_id += 1 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 0bdc4ec51..8dc19b7eb 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 @@ -178,8 +178,7 @@ def get_items(self, provider, context, skip_title=False): (PATHS.PLAY,), { 'playlist_ids': ','.join(self._playlist_ids), - 'play': True, - 'order': 'default', + 'order': 'normal', }, ), playable=True, diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 7b00d305f..a6f3682ee 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -197,7 +197,7 @@ def _play_playlist(provider, context): text='{wait} {current}/{total}'.format( wait=context.localize('please_wait'), current=0, - total=total + total=total, ) ) @@ -214,7 +214,7 @@ def _play_playlist(provider, context): text='{wait} {current}/{total}'.format( wait=context.localize('please_wait'), current=len(videos), - total=total + total=total, ) ) @@ -305,6 +305,27 @@ def _play_channel_live(provider, context): def process(provider, context, **_kwargs): + """ + Plays a video, playlist, or channel live stream. + + Video: + plugin://plugin.video.youtube/play/?video_id= + + * VIDEO_ID: YouTube Video ID + + Playlist: + plugin://plugin.video.youtube/play/?playlist_id=[&order=][&action=] + + * PLAYLIST_ID: YouTube Playlist ID + * ORDER: [ask(default)|normal|reverse|shuffle] optional playlist order + * ACTION: [list|play|queue|None(default)] optional action to perform + + Channel live streams: + plugin://plugin.video.youtube/play/?channel_id=[&live=X] + + * CHANNEL_ID: YouTube Channel ID + * X: optional index of live stream to play if channel has multiple live streams. 1 (default) for first live stream + """ ui = context.get_ui() params = context.get_params() @@ -346,7 +367,7 @@ def process(provider, context, **_kwargs): if context.get_handle() == -1: context.execute('PlayMedia({0})'.format( - context.create_uri(('play',), params) + context.create_uri((PATHS.PLAY,), params) )) return False diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 1c56286f2..0aa94c239 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -347,21 +347,24 @@ def on_uri2addon(provider, context, uri=None, **_kwargs): return False - """ - Lists the videos of a playlist. - path : '/channel/(?P[^/]+)/playlist/(?P[^/]+)/' - or - path : '/playlist/(?P[^/]+)/' - channel_id : ['mine'|] - playlist_id: - """ - @AbstractProvider.register_path( r'^(?:/channel/(?P[^/]+))?' r'/playlist/(?P[^/]+)/?$' ) @staticmethod def on_playlist(provider, context, re_match): + """ + Lists the videos of a playlist. + + plugin://plugin.video.youtube/channel//playlist/ + + or + + plugin://plugin.video.youtube/playlist/ + + * CHANNEL_ID: ['mine'|YouTube Channel ID] + * PLAYLIST_ID: YouTube Playlist ID + """ context.set_content(CONTENT.VIDEO_CONTENT) resource_manager = provider.get_resource_manager(context) @@ -374,17 +377,18 @@ def on_playlist(provider, context, re_match): result = v3.response_to_items(provider, context, json_data[batch_id]) return result - """ - Lists all playlists of a channel. - path : '/channel/(?P[^/]+)/playlists/' - channel_id: - """ - @AbstractProvider.register_path( r'^/channel/(?P[^/]+)' r'/playlists/?$') @staticmethod def on_channel_playlists(provider, context, re_match): + """ + Lists all playlists of a channel. + + plugin://plugin.video.youtube/channel//playlists/ + + * CHANNEL_ID: YouTube Channel ID + """ context.set_content(CONTENT.LIST_CONTENT) channel_id = re_match.group('channel_id') @@ -436,17 +440,18 @@ def on_channel_playlists(provider, context, re_match): result.extend(v3.response_to_items(provider, context, json_data)) return result - """ - List live streams for channel. - path : '/channel/(?P[^/]+)/live/' - channel_id: - """ - @AbstractProvider.register_path( r'^/channel/(?P[^/]+)' r'/live/?$') @staticmethod def on_channel_live(provider, context, re_match): + """ + List live streams for channel. + + plugin://plugin.video.youtube/channel//live + + * CHANNEL_ID: YouTube Channel ID + """ context.set_content(CONTENT.VIDEO_CONTENT) result = [] @@ -479,17 +484,19 @@ def on_channel_live(provider, context, re_match): return result - """ - Lists a playlist folder and all uploaded videos of a channel. - path :'/channel|handle|user/(?P)[^/]+/' - channel_id: - """ - @AbstractProvider.register_path( r'^/(?P(channel|handle|user))' r'/(?P[^/]+)/?$') @staticmethod def on_channel(provider, context, re_match): + """ + Lists a playlist folder and all uploaded videos of a channel. + + plugin://plugin.video.youtube// + + * ID_TYPE: channel|handle|user + * ID: YouTube ID + """ listitem_channel_id = context.get_listitem_property(CHANNEL_ID) client = provider.get_client(context) @@ -698,19 +705,6 @@ def on_my_location(context, **_kwargs): return result - """ - Plays a video, playlist, or channel live stream. - Video: '/play/?video_id=XXXXXX' - - Playlist: '/play/?playlist_id=XXXXXX[&order=ORDER][&action=ACTION]' - ORDER: [normal(default)|reverse|shuffle] optional playlist ordering - ACTION: [list|play|queue|None(default)] optional action to perform - - Channel live streams: '/play/?channel_id=UCXXXXXX[&live=X] - X: optional index of live stream to play if channel has multiple live - streams. 1 (default) for first live stream - """ - @AbstractProvider.register_path('^/users/(?P[^/]+)/?$') @staticmethod def on_users(re_match, **_kwargs): From 292fb94645ba4b7198787f9955f078f847451364 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:56:28 +1100 Subject: [PATCH 18/26] Fix http server not running when script shows client IP --- resources/lib/youtube_plugin/kodion/script_actions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index ca83750bd..4a6f64a7b 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -16,6 +16,7 @@ from .constants import ( DATA_PATH, RELOAD_ACCESS_MANAGER, + SERVER_WAKEUP, TEMP_PATH, WAIT_END_FLAG, ) @@ -289,6 +290,7 @@ def _config_actions(context, action, *_args): settings.httpd_listen(addresses[selected_address]) elif action == 'show_client_ip': + context.wakeup(SERVER_WAKEUP, timeout=5) if httpd_status(context): client_ip = get_client_ip_address(context) if client_ip: From 0388d14c38dda78fb338a837137c09c8bbe492b5 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:02:56 +1100 Subject: [PATCH 19/26] Add View all and Shuffle context menu items for playlists --- .../resource.language.en_gb/strings.po | 2 +- .../kodion/context/xbmc/xbmc_context.py | 1 + .../youtube_plugin/kodion/items/menu_items.py | 61 ++++++++++++++----- .../youtube_plugin/youtube/helper/utils.py | 15 +++-- .../youtube_plugin/youtube/helper/yt_play.py | 2 + .../lib/youtube_plugin/youtube/provider.py | 35 ++++++++--- 6 files changed, 88 insertions(+), 28 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index b9b3ba928..d00ce3398 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -554,7 +554,7 @@ msgid "None" msgstr "" msgctxt "#30562" -msgid "" +msgid "View all" msgstr "" msgctxt "#30563" 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 a05d4a4dc..1c5651747 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -154,6 +154,7 @@ class XbmcContext(AbstractContext): 'playlist.progress.updating': 30536, 'playlist.removed_from': 30715, 'playlist.select': 30521, + 'playlist.view.all': 30562, 'playlists': 30501, 'please_wait': 30119, 'prompt': 30566, diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 0654b81d1..98b2069ee 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -109,27 +109,58 @@ def queue_video(context): ) -def play_all_from_playlist(context, playlist_id, video_id=''): - if video_id: - return ( - context.localize('playlist.play.from_here'), - context.create_uri( - (PATHS.PLAY,), - { - 'playlist_id': playlist_id, - 'video_id': video_id, - 'play': True, - }, - run=True, - ), - ) +def play_playlist(context, playlist_id): return ( context.localize('playlist.play.all'), context.create_uri( (PATHS.PLAY,), { 'playlist_id': playlist_id, - 'play': True, + 'order': 'ask', + }, + run=True, + ), + ) + + +def play_playlist_from(context, playlist_id, video_id): + return ( + context.localize('playlist.play.from_here'), + context.create_uri( + (PATHS.PLAY,), + { + 'playlist_id': playlist_id, + 'video_id': video_id, + }, + run=True, + ), + ) + + +def view_playlist(context, playlist_id): + return ( + context.localize('playlist.view.all'), + context.create_uri( + (PATHS.ROUTE, PATHS.PLAY,), + { + 'playlist_id': playlist_id, + 'order': 'normal', + 'action': 'list', + }, + run=True, + ), + ) + + +def shuffle_playlist(context, playlist_id): + return ( + context.localize('playlist.play.shuffle'), + context.create_uri( + (PATHS.ROUTE, PATHS.PLAY,), + { + 'playlist_id': playlist_id, + 'order': 'random', + 'action': 'list', }, run=True, ), diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 8f512db99..50a3c0cdb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -439,9 +439,16 @@ def update_playlist_infos(provider, context, playlist_id_dict, # play all videos of the playlist context_menu = [ - menu_items.play_all_from_playlist( + menu_items.play_playlist( context, playlist_id ), + menu_items.view_playlist( + context, playlist_id + ), + menu_items.shuffle_playlist( + context, playlist_id + ), + menu_items.separator(), menu_items.bookmark_add( context, playlist_item ) if not in_bookmarks_list and channel_id != 'mine' else None, @@ -844,12 +851,12 @@ def update_video_infos(provider, context, video_id_dict, playlist_channel_id = playlist_match.group('channel_id') context_menu.extend(( - menu_items.play_all_from_playlist( + menu_items.play_playlist_from( context, playlist_id, video_id ), - menu_items.play_all_from_playlist( + menu_items.play_playlist( context, playlist_id - ) + ), )) # add 'Watch Later' only if we are not in my 'Watch Later' list diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index a6f3682ee..705cb4536 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -19,6 +19,7 @@ from ...kodion.compatibility import urlencode, urlunsplit from ...kodion.constants import ( BUSY_FLAG, + CONTENT, PATHS, PLAYBACK_INIT, PLAYER_DATA, @@ -241,6 +242,7 @@ def _play_playlist(provider, context): random.shuffle(videos) if action == 'list': + context.set_content(CONTENT.VIDEO_CONTENT) return videos # clear the playlist diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 0aa94c239..6dea82cf1 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1222,9 +1222,15 @@ def on_root(provider, context, re_match): image='{media}/watch_later.png', ) context_menu = [ - menu_items.play_all_from_playlist( + menu_items.play_playlist( context, watch_later_id - ) + ), + menu_items.view_playlist( + context, watch_later_id + ), + menu_items.shuffle_playlist( + context, watch_later_id + ), ] watch_later_item.add_context_menu(context_menu) result.append(watch_later_item) @@ -1241,15 +1247,22 @@ def on_root(provider, context, re_match): resource_manager = provider.get_resource_manager(context) playlists = resource_manager.get_related_playlists('mine') if playlists and 'likes' in playlists: + liked_list_id = playlists['likes'] liked_videos_item = DirectoryItem( localize('video.liked'), - create_uri(('channel', 'mine', 'playlist', playlists['likes'])), + create_uri(('channel', 'mine', 'playlist', liked_list_id)), image='{media}/likes.png', ) context_menu = [ - menu_items.play_all_from_playlist( - context, playlists['likes'] - ) + menu_items.play_playlist( + context, liked_list_id + ), + menu_items.view_playlist( + context, liked_list_id + ), + menu_items.shuffle_playlist( + context, liked_list_id + ), ] liked_videos_item.add_context_menu(context_menu) result.append(liked_videos_item) @@ -1272,9 +1285,15 @@ def on_root(provider, context, re_match): image='{media}/history.png', ) context_menu = [ - menu_items.play_all_from_playlist( + menu_items.play_playlist( context, history_id - ) + ), + menu_items.view_playlist( + context, history_id + ), + menu_items.shuffle_playlist( + context, history_id + ), ] watch_history_item.add_context_menu(context_menu) result.append(watch_history_item) From 2351a4a50b1c78e5504d4dcd32db0aa002cda309 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:18:40 +1100 Subject: [PATCH 20/26] Dont ask for playlist order when playing from specific video --- resources/lib/youtube_plugin/youtube/helper/yt_play.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 705cb4536..fd39bddbd 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -180,6 +180,7 @@ def _play_playlist(provider, context): playlist_ids = params.get('playlist_ids') if not playlist_ids: playlist_ids = [params.get('playlist_id')] + video_id = params.get('video_id') resource_manager = provider.get_resource_manager(context) ui = context.get_ui() @@ -223,8 +224,10 @@ def _play_playlist(provider, context): return False # select order - order = params.get('order', '') - if not order: + order = params.get('order') + if not order and not video_id: + order = 'ask' + if order == 'ask': order_list = ('default', 'reverse', 'shuffle') items = [(context.localize('playlist.play.%s' % order), order) for order in order_list] @@ -250,7 +253,6 @@ def _play_playlist(provider, context): playlist_player.unshuffle() # check if we have a video as starting point for the playlist - video_id = params.get('video_id') playlist_position = None if video_id else 0 # add videos to playlist for idx, video in enumerate(videos): From 82c499a77b08b18f8076bf88d82f4d8d05d22089 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 26 Oct 2024 22:35:14 +1100 Subject: [PATCH 21/26] Add kodion.logger.Logger mixin class and kodion.logging.enabled plugin settings - New setting forces debug logging on - Simplify settings change monitoring - Workaround being unable to check debug logging enabled from advancedsettings.xml - Workaround debug logging being disabled by FireOS #938 --- .../lib/youtube_plugin/kodion/__init__.py | 1 - .../kodion/constants/const_settings.py | 2 + .../kodion/context/abstract_context.py | 22 +---- resources/lib/youtube_plugin/kodion/debug.py | 6 +- .../kodion/json_store/json_store.py | 46 ++++------ resources/lib/youtube_plugin/kodion/logger.py | 81 ++++++++++------- .../kodion/monitors/service_monitor.py | 90 +++++++++++-------- .../kodion/network/http_server.py | 41 +++++---- .../youtube_plugin/kodion/network/ip_api.py | 10 +-- .../youtube_plugin/kodion/network/requests.py | 6 +- .../kodion/plugin/xbmc/xbmc_plugin.py | 8 +- .../youtube_plugin/kodion/plugin_runner.py | 25 ++++-- .../kodion/settings/abstract_settings.py | 5 ++ .../settings/xbmc/xbmc_plugin_settings.py | 80 ++++++++--------- .../kodion/sql_store/storage.py | 10 +-- .../kodion/utils/datetime_parser.py | 6 +- .../youtube_plugin/kodion/utils/methods.py | 6 +- .../youtube/client/login_client.py | 7 +- .../youtube/helper/ratebypass/ratebypass.py | 32 +++---- resources/settings.xml | 23 +++-- 20 files changed, 258 insertions(+), 249 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/__init__.py b/resources/lib/youtube_plugin/kodion/__init__.py index cb913bc4a..9b3fdb304 100644 --- a/resources/lib/youtube_plugin/kodion/__init__.py +++ b/resources/lib/youtube_plugin/kodion/__init__.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -from . import logger from .abstract_provider import ( # Abstract provider for implementation by the user AbstractProvider, diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index ed8ffef57..3f12718ee 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -90,3 +90,5 @@ HTTPD_LISTEN = 'kodion.http.listen' # (str) HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (str) HTTPD_IDLE_SLEEP = 'youtube.http.idle_sleep' # (bool) + +LOGGING_ENABLED = 'kodion.logging.enabled' # (bool) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 7f1db2a7c..add2755ff 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -12,7 +12,7 @@ import os -from .. import logger +from ..logger import Logger from ..compatibility import parse_qsl, quote, to_str, urlencode, urlsplit from ..constants import ( PATHS, @@ -37,7 +37,7 @@ from ..utils import current_system_version -class AbstractContext(object): +class AbstractContext(Logger): _initialized = False _addon = None _settings = None @@ -430,24 +430,6 @@ def set_content(self, content_type, sub_type=None, category_label=None): def add_sort_method(self, *sort_methods): raise NotImplementedError() - def log(self, text, log_level=logger.NOTICE): - logger.log(text, log_level, self.get_id()) - - def log_warning(self, text): - self.log(text, logger.WARNING) - - def log_error(self, text): - self.log(text, logger.ERROR) - - def log_notice(self, text): - self.log(text, logger.NOTICE) - - def log_debug(self, text): - self.log(text, logger.DEBUG) - - def log_info(self, text): - self.log(text, logger.INFO) - def clone(self, new_path=None, new_params=None): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index 076836681..f99a5ad54 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -13,7 +13,7 @@ import atexit import os -from .logger import log_debug +from .logger import Logger def debug_here(host='localhost'): @@ -145,7 +145,7 @@ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): if not self._enabled: return - log_debug('Profiling stats: {0}'.format(self.get_stats( + Logger.log_debug('Profiling stats: {0}'.format(self.get_stats( num_lines=self._num_lines, print_callees=self._print_callees, reuse=self._reuse, @@ -270,7 +270,7 @@ def get_stats(self, return output def print_stats(self): - log_debug('Profiling stats: {0}'.format(self.get_stats( + Logger.log_debug('Profiling stats: {0}'.format(self.get_stats( num_lines=self._num_lines, print_callees=self._print_callees, reuse=self._reuse, 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 baede56d4..f74adff56 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -14,18 +14,18 @@ from io import open from ..constants import DATA_PATH -from ..logger import log_debug, log_error +from ..logger import Logger from ..utils import make_dirs, merge_dicts, to_unicode -class JSONStore(object): +class JSONStore(Logger): BASE_PATH = make_dirs(DATA_PATH) 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.log_error('JSONStore.__init__ - temp directory not available') self.filename = None self._data = {} @@ -42,13 +42,11 @@ def save(self, data, update=False, process=None): if update: data = merge_dicts(self._data, data) if data == self._data: - log_debug('JSONStore.save - data unchanged:\n|{filename}|'.format( - filename=self.filename - )) + self.log_debug('JSONStore.save - data unchanged:\n' + '|{filename}|'.format(filename=self.filename)) return - log_debug('JSONStore.save - saving:\n|{filename}|'.format( - filename=self.filename - )) + self.log_debug('JSONStore.save - saving:\n' + '|{filename}|'.format(filename=self.filename)) try: if not data: raise ValueError @@ -60,23 +58,20 @@ 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 - access error:\n|{filename}|'.format( - filename=self.filename - )) + self.log_error('JSONStore.save - access error:\n' + '|{filename}|'.format(filename=self.filename)) return except (TypeError, ValueError): - log_error('JSONStore.save - invalid data:\n|{data}|'.format( - data=data - )) + self.log_error('JSONStore.save - invalid data:\n' + '|{data}|'.format(data=data)) 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 - )) + self.log_debug('JSONStore.load - loading:\n' + '|{filename}|'.format(filename=self.filename)) try: with open(self.filename, mode='r', encoding='utf-8') as jsonfile: data = jsonfile.read() @@ -85,13 +80,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 - access error:\n|{filename}|'.format( - filename=self.filename - )) + self.log_error('JSONStore.load - access error:\n' + '|{filename}|'.format(filename=self.filename)) except (TypeError, ValueError): - log_error('JSONStore.load - invalid data:\n|{data}|'.format( - data=data - )) + self.log_error('JSONStore.load - invalid data:\n' + '|{data}|'.format(data=data)) def get_data(self, process=None): try: @@ -100,9 +93,8 @@ def get_data(self, process=None): _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.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, ensure_ascii=False)) return process(_data) if process is not None else _data diff --git a/resources/lib/youtube_plugin/kodion/logger.py b/resources/lib/youtube_plugin/kodion/logger.py index b3968f2d6..063ede6ac 100644 --- a/resources/lib/youtube_plugin/kodion/logger.py +++ b/resources/lib/youtube_plugin/kodion/logger.py @@ -10,42 +10,55 @@ from __future__ import absolute_import, division, unicode_literals -from .compatibility import xbmc, xbmcaddon +from .compatibility import xbmc from .constants import ADDON_ID -DEBUG = xbmc.LOGDEBUG -INFO = xbmc.LOGINFO -NOTICE = xbmc.LOGNOTICE -WARNING = xbmc.LOGWARNING -ERROR = xbmc.LOGERROR -FATAL = xbmc.LOGFATAL -SEVERE = xbmc.LOGSEVERE -NONE = xbmc.LOGNONE -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): - log(text, DEBUG, addon_id) - - -def log_info(text, addon_id=ADDON_ID): - log(text, INFO, addon_id) - - -def log_notice(text, addon_id=ADDON_ID): - log(text, NOTICE, addon_id) - - -def log_warning(text, addon_id=ADDON_ID): - log(text, WARNING, addon_id) - - -def log_error(text, addon_id=ADDON_ID): - log(text, ERROR, addon_id) +class Logger(object): + LOGDEBUG = xbmc.LOGDEBUG + LOGINFO = xbmc.LOGINFO + LOGNOTICE = xbmc.LOGNOTICE + LOGWARNING = xbmc.LOGWARNING + LOGERROR = xbmc.LOGERROR + LOGFATAL = xbmc.LOGFATAL + LOGSEVERE = xbmc.LOGSEVERE + LOGNONE = xbmc.LOGNONE + + @staticmethod + def log(text, log_level=LOGDEBUG, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=log_level) + + @staticmethod + def log_debug(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGDEBUG) + + @staticmethod + def log_info(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGINFO) + + @staticmethod + def log_notice(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGNOTICE) + + @staticmethod + def log_warning(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGWARNING) + + @staticmethod + def log_error(text, addon_id=ADDON_ID): + log_line = '[%s] %s' % (addon_id, text) + xbmc.log(msg=log_line, level=Logger.LOGERROR) + + @staticmethod + def debug_log(on=False, off=True): + if on: + Logger.LOGDEBUG = Logger.LOGNOTICE + elif off: + Logger.LOGDEBUG = xbmc.LOGDEBUG diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index 01cd50892..1d502d6cd 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -23,35 +23,33 @@ SERVER_WAKEUP, WAKEUP, ) -from ..logger import log_debug from ..network import get_connect_address, get_http_server, httpd_status class ServiceMonitor(xbmc.Monitor): _settings_changes = 0 - _settings_state = None + _settings_collect = False get_idle_time = xbmc.getGlobalIdleTime def __init__(self, context): self._context = context - settings = context.get_settings() - self._httpd_address, self._httpd_port = get_connect_address(context) - self._old_httpd_address = self._httpd_address - self._old_httpd_port = self._httpd_port - self._whitelist = settings.httpd_whitelist() + self._httpd_address = None + self._httpd_port = None + self._whitelist = None + self._old_httpd_address = None + self._old_httpd_port = None + self._use_httpd = None self.httpd = None self.httpd_thread = None - self.httpd_sleep_allowed = settings.httpd_sleep_allowed() + self.httpd_sleep_allowed = True self.system_idle = False self.refresh = False self.interrupt = False - self._use_httpd = None - if self.httpd_required(settings): - self.start_httpd() + self.onSettingsChanged(force=True) super(ServiceMonitor, self).__init__() @@ -110,11 +108,9 @@ def onNotification(self, sender, method, data): elif target == CHECK_SETTINGS: state = data.get('state') if state == 'defer': - self._settings_state = state + self._settings_collect = True elif state == 'process': - self._settings_state = state - self.onSettingsChanged() - self._settings_state = None + self.onSettingsChanged(force=True) if data.get('response_required'): self.set_property(WAKEUP, target) @@ -147,26 +143,38 @@ def onDPMSDeactivated(self): self.system_idle = False self.interrupt = True - def onSettingsChanged(self): - self._settings_changes += 1 - if self._settings_state == 'defer': - return - changes = self._settings_changes - if self._settings_state != 'process': + def onSettingsChanged(self, force=False): + context = self._context + + if force: + self._settings_collect = False + self._settings_changes = 0 + else: + self._settings_changes += 1 + if self._settings_collect: + return + + total = self._settings_changes self.waitForAbort(1) - if changes != self._settings_changes: + if total != self._settings_changes: return - log_debug('onSettingsChanged: {0} change(s)'.format(changes)) - self._settings_changes = 0 - settings = self._context.get_settings(refresh=True) + context.log_debug('onSettingsChanged: {0} change(s)'.format(total)) + self._settings_changes = 0 + + settings = context.get_settings(refresh=True) + if settings.logging_enabled(): + context.debug_log(on=True) + else: + context.debug_log(off=True) + self.set_property(CHECK_SETTINGS) self.refresh_container() httpd_started = bool(self.httpd) httpd_restart = False - address, port = get_connect_address(self._context) + address, port = get_connect_address(context) if port != self._httpd_port: self._old_httpd_port = self._httpd_port self._httpd_port = port @@ -201,12 +209,14 @@ def start_httpd(self): if self.httpd: return - log_debug('HTTPServer: Starting |{ip}:{port}|' - .format(ip=self._httpd_address, port=self._httpd_port)) + context = self._context + context.log_debug('HTTPServer: Starting |{ip}:{port}|' + .format(ip=self._httpd_address, + port=self._httpd_port)) self.httpd_address_sync() self.httpd = get_http_server(address=self._httpd_address, port=self._httpd_port, - context=self._context) + context=context) if not self.httpd: return @@ -214,16 +224,17 @@ def start_httpd(self): self.httpd_thread.start() address = self.httpd.socket.getsockname() - log_debug('HTTPServer: Listening on |{ip}:{port}|' - .format(ip=address[0], port=address[1])) + context.log_debug('HTTPServer: Listening on |{ip}:{port}|' + .format(ip=address[0], + port=address[1])) def shutdown_httpd(self, sleep=False): if self.httpd: if sleep and self.httpd_required(while_sleeping=True): return - log_debug('HTTPServer: Shutting down |{ip}:{port}|' - .format(ip=self._old_httpd_address, - port=self._old_httpd_port)) + self._context.log_debug('HTTPServer: Shutting down |{ip}:{port}|' + .format(ip=self._old_httpd_address, + port=self._old_httpd_port)) self.httpd_address_sync() self.httpd.shutdown() self.httpd.server_close() @@ -232,11 +243,12 @@ def shutdown_httpd(self, sleep=False): self.httpd = None def restart_httpd(self): - 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._context.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() diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 888969c4c..236619f30 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -34,7 +34,6 @@ PATHS, TEMP_PATH, ) -from ..logger import log_debug, log_error from ..utils import redact_ip, validate_ip_address, wait @@ -91,24 +90,25 @@ def connection_allowed(self): log_lines.append('Whitelisted: |%s|' % str(conn_allowed)) if not conn_allowed: - log_debug('HTTPServer: Connection from |{client_ip| not allowed' - .format(client_ip=client_ip)) + self._context.log_debug('HTTPServer: Connection blocked from' + ' |{client_ip|' + .format(client_ip=client_ip)) elif self.path != PATHS.PING: - log_debug(' '.join(log_lines)) + self._context.log_debug(' '.join(log_lines)) return conn_allowed # noinspection PyPep8Naming def do_GET(self): - settings = self._context.get_settings() - localize = self._context.localize + context = self._context + settings = context.get_settings() + localize = context.localize api_config_enabled = settings.api_config_page() # Strip trailing slash if present stripped_path = self.path.rstrip('/') if stripped_path != PATHS.PING: - log_debug('HTTPServer: GET |{path}|'.format( - path=redact_ip(self.path) - )) + context.log_debug('HTTPServer: GET |{path}|' + .format(path=redact_ip(self.path))) if not self.connection_allowed(): self.send_error(403) @@ -235,7 +235,8 @@ def do_GET(self): # noinspection PyPep8Naming def do_HEAD(self): - log_debug('HTTPServer: HEAD |{path}|'.format(path=self.path)) + self._context.log_debug('HTTPServer: HEAD |{path}|' + .format(path=self.path)) if not self.connection_allowed(): self.send_error(403) @@ -267,7 +268,8 @@ def do_HEAD(self): # noinspection PyPep8Naming def do_POST(self): - log_debug('HTTPServer: POST |{path}|'.format(path=self.path)) + self._context.log_debug('HTTPServer: POST |{path}|' + .format(path=self.path)) if not self.connection_allowed(): self.send_error(403) @@ -317,8 +319,9 @@ def do_POST(self): re.MULTILINE) if match: authorized_types = match.group('authorized_types').split(',') - log_debug('HTTPServer: Found authorized formats |{auth_fmts}|' - .format(auth_fmts=authorized_types)) + self._context.log_debug('HTTPServer: Found authorized formats' + ' |{auth_fmts}|' + .format(auth_fmts=authorized_types)) fmt_to_px = { 'SD': (1280 * 528) - 1, @@ -589,8 +592,10 @@ def get_http_server(address, port, context): server = HTTPServer((address, port), RequestHandler) return server except socket.error as exc: - log_error('HTTPServer: Failed to start |{address}:{port}| |{response}|' - .format(address=address, port=port, response=exc)) + context.log_error('HTTPServer: Failed to start\n' + 'Address: |{address}:{port}|\n' + 'Response: |{response}|' + .format(address=address, port=port, response=exc)) xbmcgui.Dialog().notification(context.get_name(), str(exc), context.get_icon(), @@ -615,9 +620,9 @@ def httpd_status(context): if result == 204: return True - log_debug('HTTPServer: Ping |{netloc}| - |{response}|' - .format(netloc=netloc, - response=result or 'failed')) + context.log_debug('HTTPServer: Ping |{netloc}| - |{response}|' + .format(netloc=netloc, + response=result or 'failed')) return False diff --git a/resources/lib/youtube_plugin/kodion/network/ip_api.py b/resources/lib/youtube_plugin/kodion/network/ip_api.py index 853ee216e..e3f9bc94b 100644 --- a/resources/lib/youtube_plugin/kodion/network/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/network/ip_api.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, unicode_literals from .requests import BaseRequestsClass -from .. import logger class Locator(BaseRequestsClass): @@ -32,9 +31,10 @@ def locate_requester(self): def success(self): successful = self.response().get('status', 'fail') == 'success' if successful: - logger.log_debug('Location request was successful') + self.log_debug('Location request was successful') else: - logger.log_error(self.response().get('message', 'Location request failed with no error message')) + msg = 'Location request failed with no error message' + self.log_error(self.response().get('message') or msg) return successful def coordinates(self): @@ -44,7 +44,7 @@ def coordinates(self): lat = self._response.get('lat') lon = self._response.get('lon') if lat is None or lon is None: - logger.log_error('No coordinates returned') + self.log_error('No coordinates returned') return None - logger.log_debug('Coordinates found') + self.log_debug('Coordinates found') return {'lat': lat, 'lon': lon} diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index deb19940a..52cd9dfdf 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -19,7 +19,7 @@ from requests.utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths from urllib3.util.ssl_ import create_urllib3_context -from ..logger import log_error +from ..logger import Logger __all__ = ( @@ -63,7 +63,7 @@ def cert_verify(self, conn, url, verify, cert): return super(SSLHTTPAdapter, self).cert_verify(conn, url, verify, cert) -class BaseRequestsClass(object): +class BaseRequestsClass(Logger): _session = Session() _session.mount('https://', SSLHTTPAdapter( pool_maxsize=10, @@ -198,7 +198,7 @@ def request(self, url, method='GET', ) ) - log_error('\n'.join([part for part in [ + self.log_error('\n'.join([part for part in [ error_title, error_info, response_text, stack_trace ] if part])) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 01926f861..05a6c2a9a 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -16,7 +16,6 @@ from ...compatibility import xbmcplugin from ...constants import ( BUSY_FLAG, - CHECK_SETTINGS, CONTAINER_FOCUS, CONTAINER_ID, CONTAINER_POSITION, @@ -153,12 +152,7 @@ def run(self, provider, context, focused=None): if ui.pop_property(RELOAD_ACCESS_MANAGER): context.reload_access_manager() - if ui.pop_property(CHECK_SETTINGS): - provider.reset_client() - settings = context.get_settings(refresh=True) - else: - settings = context.get_settings() - + settings = context.get_settings() if settings.setup_wizard_enabled(): provider.run_wizard(context) diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index c0e8b86d8..b009e19af 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -10,7 +10,9 @@ from __future__ import absolute_import, division, unicode_literals +from .constants import CHECK_SETTINGS from .context import XbmcContext +from .debug import Profiler from .plugin import XbmcPlugin from ..youtube import Provider @@ -20,21 +22,26 @@ _context = XbmcContext() _plugin = XbmcPlugin() _provider = Provider() - -_profiler = _context.get_infobool('System.GetBool(debug.showloginfo)') -_profiler = True -if _profiler: - from .debug import Profiler - - _profiler = Profiler(enabled=False, print_callees=False, num_lines=20) +_profiler = Profiler(enabled=False, print_callees=False, num_lines=20) def run(context=_context, plugin=_plugin, provider=_provider, profiler=_profiler): - if profiler: + + if context.get_ui().pop_property(CHECK_SETTINGS): + provider.reset_client() + settings = context.get_settings(refresh=True) + else: + settings = context.get_settings() + + debug = settings.logging_enabled() + if debug: + context.debug_log(on=True) profiler.enable(flush=True) + else: + context.debug_log(off=True) current_uri = context.get_uri() context.init() @@ -59,5 +66,5 @@ def run(context=_context, plugin.run(provider, context, focused=(current_uri == new_uri)) - if profiler: + if debug: profiler.print_stats() diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 81dbc3812..fa47c1a41 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -15,6 +15,7 @@ from ..constants import SETTINGS from ..utils import ( current_system_version, + get_kodi_setting_bool, get_kodi_setting_value, validate_ip_address, ) @@ -653,3 +654,7 @@ def get_label_color(self, label_part): def get_channel_name_aliases(self): return frozenset(self.get_string_list(SETTINGS.CHANNEL_NAME_ALIASES)) + + def logging_enabled(self): + return (self.get_bool(SETTINGS.LOGGING_ENABLED, False) + or get_kodi_setting_bool('debug.showloginfo')) 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 d8e82c413..6065af900 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 @@ -15,8 +15,7 @@ from ..abstract_settings import AbstractSettings from ...compatibility import xbmcaddon from ...constants import ADDON_ID, VALUE_FROM_STR -from ...logger import log_debug -from ...utils.methods import get_kodi_setting_bool +from ...logger import Logger from ...utils.system_version import current_system_version @@ -94,7 +93,7 @@ def ref(self): del self._ref -class XbmcPluginSettings(AbstractSettings): +class XbmcPluginSettings(AbstractSettings, Logger): _instances = set() _proxy = None @@ -119,7 +118,6 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): else: fill = False - self._echo = get_kodi_setting_bool('debug.showloginfo') self._cache = {} if current_system_version.compatible(21): self._proxy = SettingsProxy(xbmc_addon.getSettings()) @@ -134,6 +132,8 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): self.__class__._instances.add(xbmc_addon) self._proxy = SettingsProxy(xbmc_addon) + self._echo = self.logging_enabled() + def get_bool(self, setting, default=None, echo=None): if setting in self._cache: return self._cache[setting] @@ -154,11 +154,10 @@ def get_bool(self, setting, default=None, echo=None): 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.log_debug('Get |{setting}|: {value} (bool, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) self._cache[setting] = value return value @@ -174,11 +173,10 @@ def set_bool(self, setting, value, echo=None): error = exc 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' - )) + self.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): @@ -203,11 +201,10 @@ def get_int(self, setting, default=-1, process=None, echo=None): 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.log_debug('Get |{setting}|: {value} (int, {status})' + .format(setting=setting, + value=value, + status=error if error else 'success')) self._cache[setting] = value return value @@ -223,11 +220,10 @@ def set_int(self, setting, value, echo=None): error = exc 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' - )) + self.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): @@ -250,11 +246,10 @@ def get_string(self, setting, default='', echo=None): echo = '...'.join((value[:3], value[-3:])) else: echo = value - log_debug('Get |{setting}|: "{echo}" (str, {status})'.format( - setting=setting, - echo=echo, - status=error if error else 'success' - )) + self.log_debug('Get |{setting}|: "{echo}" (str, {status})' + .format(setting=setting, + echo=echo, + status=error if error else 'success')) self._cache[setting] = value return value @@ -278,11 +273,10 @@ def set_string(self, setting, value, echo=None): echo = '...'.join((value[:3], value[-3:])) else: echo = value - log_debug('Set |{setting}|: "{echo}" (str, {status})'.format( - setting=setting, - echo=echo, - status=error if error else 'success' - )) + self.log_debug('Set |{setting}|: "{echo}" (str, {status})' + .format(setting=setting, + echo=echo, + status=error if error else 'success')) return not error def get_string_list(self, setting, default=None, echo=None): @@ -299,11 +293,10 @@ def get_string_list(self, setting, default=None, echo=None): value = default 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.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 @@ -319,9 +312,8 @@ def set_string_list(self, setting, value, echo=None): error = exc 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' - )) + self.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/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index 1b49ec05e..099b22509 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -17,7 +17,7 @@ from threading import Lock from traceback import format_stack -from ..logger import log_warning, log_error +from ..logger import Logger from ..utils.datetime_parser import fromtimestamp, since_epoch from ..utils.methods import make_dirs @@ -236,10 +236,10 @@ def _open(self): exc=exc, details=''.join(format_stack()) ) if isinstance(exc, sqlite3.OperationalError): - log_warning(msg) + Logger.log_warning(msg) time.sleep(0.1) else: - log_error(msg) + Logger.log_error(msg) return False else: @@ -313,10 +313,10 @@ def _execute(cursor, query, values=None, many=False, script=False): exc=exc, details=''.join(format_stack()) ) if isinstance(exc, sqlite3.OperationalError): - log_warning(msg) + Logger.log_warning(msg) time.sleep(0.1) else: - log_error(msg) + Logger.log_error(msg) return [] return [] diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index b09097f73..8b8d7e346 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -17,7 +17,7 @@ from threading import Condition, Lock from ..exceptions import KodionException -from ..logger import log_error +from ..logger import Logger try: from datetime import timezone @@ -283,8 +283,8 @@ def strptime(datetime_str, fmt=None): if strptime.reloaded.acquire(False): _strptime = import_module('_strptime') modules['_strptime'] = _strptime - log_error('Python strptime bug workaround - ' - 'https://github.com/python/cpython/issues/71587') + Logger.log_error('Python strptime bug workaround - ' + 'https://github.com/python/cpython/issues/71587') strptime.reloaded.notify_all() strptime.reloaded.release() else: diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 6847c5d97..029180cbb 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -18,7 +18,7 @@ from math import floor, log from ..compatibility import byte_string_type, string_type, xbmc, xbmcvfs -from ..logger import log_error +from ..logger import Logger __all__ = ( @@ -168,7 +168,7 @@ def make_dirs(path): if succeeded: return path - log_error('Failed to create directory: |{0}|'.format(path)) + Logger.log_error('Failed to create directory: |{0}|'.format(path)) return False @@ -188,7 +188,7 @@ def rm_dir(path): if succeeded: return True - log_error('Failed to remove directory: {0}'.format(path)) + Logger.log_error('Failed to remove directory: {0}'.format(path)) return False diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 9b3549d08..23f1fdf2b 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -16,7 +16,6 @@ InvalidJSON, LoginException, ) -from ...kodion.logger import log_debug class LoginClient(YouTubeRequestClient): @@ -143,7 +142,7 @@ def refresh_token(self, token_type, refresh_token=None): id_end=client_id[-5:], secret_start=client_secret[:3], secret_end=client_secret[-3:])) - log_debug('Refresh token:{0}'.format(client)) + self.log_debug('Refresh token:{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -193,7 +192,7 @@ def request_access_token(self, token_type, code=None): id_end=client_id[-5:], secret_start=client_secret[:3], secret_end=client_secret[-3:])) - log_debug('Requesting access token:{0}'.format(client)) + self.log_debug('Requesting access token:{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -236,7 +235,7 @@ def request_device_and_user_code(self, token_type): .format(config_type=config_type, id_start=client_id[:3], id_end=client_id[-5:])) - log_debug('Requesting device and user code:{0}'.format(client)) + self.log_debug('Requesting device and user code:{0}'.format(client)) json_data = self.request(self.DEVICE_CODE_URL, method='POST', diff --git a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py index d5c4e7473..876d6b43a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py +++ b/resources/lib/youtube_plugin/youtube/helper/ratebypass/ratebypass.py @@ -14,9 +14,9 @@ import re try: - from ....kodion import logger + from ....kodion.logger import Logger except: - class logger(object): + class Logger(object): @staticmethod def log_debug(txt): print(txt) @@ -259,17 +259,17 @@ def get_throttling_function_code(js): # This pattern is only present in the throttling function code. fiduciary_index = js.find('enhanced_except_') if fiduciary_index == -1: - logger.log_debug('ratebypass: fiduciary_index not found') + Logger.log_debug('ratebypass: fiduciary_index not found') return None start_index = js.rfind('=function(', 0, fiduciary_index) if start_index == -1: - logger.log_debug('ratebypass: function code start not found') + Logger.log_debug('ratebypass: function code start not found') return None end_index = js.find('};', fiduciary_index) if end_index == -1: - logger.log_debug('ratebypass: function code end not found') + Logger.log_debug('ratebypass: function code end not found') return None return js[start_index:end_index].replace('\n', '') @@ -294,7 +294,7 @@ def get_throttling_plan_gen(raw_code): plan_start_pattern = 'try{' plan_start_index = raw_code.find(plan_start_pattern) if plan_start_index == -1: - logger.log_debug('ratebypass: command block start not found') + Logger.log_debug('ratebypass: command block start not found') raise Exception() else: # Skip the whole start pattern, it's not needed. @@ -302,7 +302,7 @@ def get_throttling_plan_gen(raw_code): plan_end_index = raw_code.find('}', plan_start_index) if plan_end_index == -1: - logger.log_debug('ratebypass: command block end not found') + Logger.log_debug('ratebypass: command block end not found') raise Exception() plan_code = raw_code[plan_start_index:plan_end_index] @@ -365,14 +365,14 @@ def get_throttling_function_array(cls, mutable_n_list, raw_code): array_start_pattern = ",c=[" array_start_index = raw_code.find(array_start_pattern) if array_start_index == -1: - logger.log_debug('ratebypass: "c" array pattern not found') + Logger.log_debug('ratebypass: "c" array pattern not found') raise Exception() else: array_start_index += len(array_start_pattern) array_end_index = raw_code.rfind('];') if array_end_index == -1: - logger.log_debug('ratebypass: "c" array end not found') + Logger.log_debug('ratebypass: "c" array end not found') raise Exception() array_code = raw_code[array_start_index:array_end_index] @@ -404,7 +404,7 @@ def get_throttling_function_array(cls, mutable_n_list, raw_code): found = True break else: - logger.log_debug('ratebypass: mapping function not yet ' + Logger.log_debug('ratebypass: mapping function not yet ' 'listed: {unknown}'.format(unknown=el)) if found: continue @@ -428,7 +428,7 @@ def calculate_n(self, mutable_n_list): video stream URL. """ if self.calculated_n: - logger.log_debug('`n` already calculated: {calculated_n}. returning early...' + Logger.log_debug('`n` already calculated: {calculated_n}. returning early...' .format(calculated_n=self.calculated_n)) return self.calculated_n @@ -436,7 +436,7 @@ def calculate_n(self, mutable_n_list): return None initial_n_string = ''.join(mutable_n_list) - logger.log_debug('Attempting to calculate `n` from initial: {initial_n}' + Logger.log_debug('Attempting to calculate `n` from initial: {initial_n}' .format(initial_n=initial_n_string)) # For each step in the plan, get the first item of the step as the @@ -449,8 +449,8 @@ def calculate_n(self, mutable_n_list): for step in self.get_throttling_plan_gen(self.throttling_function_code): curr_func = throttling_array[int(step[0])] if not callable(curr_func): - logger.log_debug('{curr_func} is not callable.'.format(curr_func=curr_func)) - logger.log_debug('Throttling array:\n{throttling_array}\n' + Logger.log_debug('{curr_func} is not callable.'.format(curr_func=curr_func)) + Logger.log_debug('Throttling array:\n{throttling_array}\n' .format(throttling_array=throttling_array)) return None @@ -462,10 +462,10 @@ def calculate_n(self, mutable_n_list): second_arg = throttling_array[int(step[2])] curr_func(first_arg, second_arg) except: - logger.log_debug('Error calculating new `n`') + Logger.log_debug('Error calculating new `n`') return None self.calculated_n = ''.join(mutable_n_list) - logger.log_debug('Calculated `n`: {calculated_n}' + Logger.log_debug('Calculated `n`: {calculated_n}' .format(calculated_n=self.calculated_n)) return self.calculated_n diff --git a/resources/settings.xml b/resources/settings.xml index 3d27f3270..d11565eb7 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -593,7 +593,7 @@ - + 0 false @@ -647,7 +647,7 @@ - + 0 true @@ -721,7 +721,7 @@ - + 0 true @@ -757,7 +757,7 @@ - + 0 true @@ -857,7 +857,7 @@ - + 0 @@ -917,7 +917,7 @@ - + 0 90 @@ -941,7 +941,7 @@ - + 0 true @@ -1057,7 +1057,7 @@ - + 0 0.0.0.0 @@ -1110,6 +1110,13 @@ + + + 0 + false + + + From a42753f6dcf3b2bfda621f9a5b80eaa506f9ffc8 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 27 Oct 2024 22:06:12 +1100 Subject: [PATCH 22/26] Store stream and MediaItem headers as dict --- resources/lib/youtube_plugin/kodion/items/media_item.py | 6 ++++-- .../lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py | 4 ++-- resources/lib/youtube_plugin/youtube/helper/stream_info.py | 5 +---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/media_item.py b/resources/lib/youtube_plugin/kodion/items/media_item.py index f60820f00..addaae4d9 100644 --- a/resources/lib/youtube_plugin/kodion/items/media_item.py +++ b/resources/lib/youtube_plugin/kodion/items/media_item.py @@ -14,7 +14,7 @@ from datetime import date from . import BaseItem -from ..compatibility import datetime_infolabel, to_str, unescape +from ..compatibility import datetime_infolabel, to_str, unescape, urlencode from ..constants import CONTENT from ..utils import duration_to_seconds, seconds_to_duration @@ -218,7 +218,9 @@ def get_track_number(self): def set_headers(self, value): self._headers = value - def get_headers(self): + def get_headers(self, as_string=False): + if as_string: + return urlencode(self._headers) return self._headers def set_license_key(self, url): diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 8cf9e388f..44ab20b2b 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -467,7 +467,7 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): 'ssl_verify_peer': False, }) - headers = media_item.get_headers() + headers = media_item.get_headers(as_string=True) if headers: props['inputstream.adaptive.manifest_headers'] = headers props['inputstream.adaptive.stream_headers'] = headers @@ -482,7 +482,7 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): mime_type = uri.split('mime=', 1)[1].split('&', 1)[0] mime_type = mime_type.replace('%2F', '/') - headers = media_item.get_headers() + headers = media_item.get_headers(as_string=True) if (headers and uri.startswith('http') and not (is_external or settings.default_player_web_urls())): diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 6d24431d7..9f6a1f857 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -975,7 +975,7 @@ def _get_player_js(self): return result @staticmethod - def _prepare_headers(headers, to_string=True, cookies=None, new_headers=None): + def _prepare_headers(headers, cookies=None, new_headers=None): if cookies or new_headers: headers = headers.copy() if cookies: @@ -984,9 +984,6 @@ def _prepare_headers(headers, to_string=True, cookies=None, new_headers=None): ]) if new_headers: headers.update(new_headers) - if to_string: - # Headers used in xbmc_items.video_playback_item - return urlencode(headers) return headers @staticmethod From 7dae24cb1aaa5ccf951e7522af03169235d71c89 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 27 Oct 2024 22:11:56 +1100 Subject: [PATCH 23/26] Ensure connection is closed on HTTPServer redirect - Avoid Kodi VideoPlayer timeout when trying to close - Follow up to 88dfa601be7127c2caab2d32c8f4c8aa885b1591 - HTTP/1.1 default to keep-alive --- resources/lib/youtube_plugin/kodion/network/http_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 236619f30..2542f6f5e 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -226,6 +226,7 @@ def do_GET(self): wait(1) self.send_response(301) self.send_header('Location', url) + self.send_header('Connection', 'close') self.end_headers() else: self.send_error(501) From 2f3ce1e83f8873976eecdcf2821cd0f6ac6c1e81 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 27 Oct 2024 23:16:50 +1100 Subject: [PATCH 24/26] Move kodion.utils.methods.entity_escape to kodion.comptibility.entity_escape - For Python2 compatibility - Also use correct escape sequence for single quotes - Follow up to e73d7044bca63533566ea97b28e5b399f3c0410b --- .../kodion/compatibility/__init__.py | 28 +++++++++++++++++++ .../youtube_plugin/kodion/utils/__init__.py | 2 -- .../youtube_plugin/kodion/utils/methods.py | 12 -------- .../youtube/helper/stream_info.py | 3 +- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py index 8e702ce5e..58c08a8ba 100644 --- a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py +++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -13,6 +13,7 @@ 'byte_string_type', 'cpu_count', 'datetime_infolabel', + 'entity_escape', 'parse_qs', 'parse_qsl', 'quote', @@ -54,12 +55,25 @@ import xbmcplugin import xbmcvfs + xbmc.LOGNOTICE = xbmc.LOGINFO xbmc.LOGSEVERE = xbmc.LOGFATAL string_type = str byte_string_type = bytes to_str = str + + + def entity_escape(text, + entities=str.maketrans({ + '&': '&', + '"': '"', + '<': '<', + '>': '>', + '\'': ''', + })): + return text.translate(entities) + # Compatibility shims for Kodi v18 and Python v2.7 except ImportError: from BaseHTTPServer import BaseHTTPRequestHandler @@ -130,11 +144,25 @@ def _file_closer(*args, **kwargs): string_type = basestring byte_string_type = (bytes, str) + def to_str(value): if isinstance(value, unicode): return value.encode('utf-8') return str(value) + + def entity_escape(text, + entities={ + '&': '&', + '"': '"', + '<': '<', + '>': '>', + '\'': ''', + }): + for key, value in entities.viewitems(): + text = text.replace(key, value) + return text + # Kodi v20+ if hasattr(xbmcgui.ListItem, 'setDateTime'): def datetime_infolabel(datetime_obj, *_args, **_kwargs): diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index 8820cfd01..659ffb1de 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -13,7 +13,6 @@ from . import datetime_parser from .methods import ( duration_to_seconds, - entity_escape, find_video_id, friendly_number, get_kodi_setting_bool, @@ -38,7 +37,6 @@ 'current_system_version', 'datetime_parser', 'duration_to_seconds', - 'entity_escape', 'find_video_id', 'friendly_number', 'get_kodi_setting_bool', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 029180cbb..0730c5374 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -23,7 +23,6 @@ __all__ = ( 'duration_to_seconds', - 'entity_escape', 'find_video_id', 'friendly_number', 'get_kodi_setting_bool', @@ -317,14 +316,3 @@ def wait(timeout=None): def redact_ip(url): return re.sub(r'([?&/])ip([=/])[^?&/]+', r'\g<1>ip\g<2>', url) - - -def entity_escape(input, - entities=str.maketrans({ - '&': '&', - '"': '"', - '<': '<', - '>': '>', - '\'': ''', - })): - return input.translate(entities) diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 9f6a1f857..408a6353e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -23,6 +23,7 @@ from ..client.request_client import YouTubeRequestClient from ..youtube_exceptions import InvalidJSON, YouTubeException from ...kodion.compatibility import ( + entity_escape, parse_qs, quote, unescape, @@ -35,7 +36,7 @@ ) from ...kodion.constants import PATHS, TEMP_PATH from ...kodion.network import get_connect_address -from ...kodion.utils import entity_escape, make_dirs, redact_ip +from ...kodion.utils import make_dirs, redact_ip class StreamInfo(YouTubeRequestClient): From 4ca960b4035abfdf0f0d9de728e2d4c1ca99c39b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:13:13 +1100 Subject: [PATCH 25/26] Add context menu items for Uploads playlist Follow up to 0388d14c38dda78fb338a837137c09c8bbe492b5 --- .../lib/youtube_plugin/youtube/provider.py | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 6dea82cf1..3cfc5c987 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -410,21 +410,57 @@ def on_channel_playlists(provider, context, re_match): ).get(channel_id) playlists = resource_manager.get_related_playlists(channel_id) - uploads = playlists.get('uploads') - if uploads: + playlist_id = playlists.get('uploads') + if playlist_id: item_label = context.localize('uploads') uploads = DirectoryItem( context.get_ui().bold(item_label), context.create_uri( - ('channel', channel_id, 'playlist', uploads), + ('channel', channel_id, 'playlist', playlist_id), new_params, ), image='{media}/playlist.png', fanart=fanart, category_label=item_label, channel_id=channel_id, - playlist_id=uploads, + playlist_id=playlist_id, ) + + context_menu = [ + menu_items.play_playlist( + context, playlist_id + ), + menu_items.view_playlist( + context, playlist_id + ), + menu_items.shuffle_playlist( + context, playlist_id + ), + menu_items.separator(), + menu_items.bookmark_add( + context, uploads + ) if channel_id != 'mine' else None, + ] + + if channel_id != 'mine': + if provider.is_logged_in: + # subscribe to the channel via the playlist item + context_menu.append( + menu_items.subscribe_to_channel( + context, channel_id, + ) + ) + context_menu.append( + # bookmark channel of the playlist + menu_items.bookmark_add_channel( + context, channel_id, + ) + ) + + if context_menu: + context_menu.append(menu_items.separator()) + uploads.add_context_menu(context_menu) + result = [uploads] else: result = False From b6f68377334d547365e89e7b7971d23983ebfa6e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 27 Oct 2024 22:24:18 +1100 Subject: [PATCH 26/26] Version bump v7.1.1+beta.2 --- addon.xml | 2 +- changelog.txt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index c76fee920..16419135b 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 7f6f6f6d4..d779cb404 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,22 @@ +## v7.1.1+beta.2 +### Fixed +- Standardise return type of LoginClient.refresh_token #932 +- Fix curl headers not being used when set on path of setResolvedUrl listitem +- Fix HEAD requests to MPD manifests +- Fix various Python2 incompatible changes +- Properly distinguish between VP9 and VP9.2 with HDR info +- Fix http server not running when script shows client IP + +### Changed +- Improve display and update of bookmarks +- Explicitly set http server protocol version to HTTP/1.1 +- Improve logging + +### New +- Add View all and Shuffle context menu items for playlists +- New setting to enable debug logging for addon + - Setting > Advanced > Logging > Enable debug logging + ## v7.1.1+beta.1 ### Fixed - Fix http server not listening on any interface if listen IP is 0.0.0.0 #927