From 97f91709412b17ad11412a467dd530c140aa7a3d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 14 Jul 2024 03:43:12 +1000 Subject: [PATCH 01/21] Update kodion.network.requests for potentially breaking future script.module.requests change --- .../youtube_plugin/kodion/network/requests.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 4bb0e4644..fd73165a6 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -15,6 +15,8 @@ from requests import Session from requests.adapters import HTTPAdapter, Retry from requests.exceptions import InvalidJSONError, RequestException +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 ..settings import XbmcPluginSettings @@ -26,8 +28,20 @@ ) +class SSLHTTPAdapter(HTTPAdapter): + _ssl_context = create_urllib3_context() + _ssl_context.load_verify_locations( + capath=extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH) + ) + + def init_poolmanager(self, *args, **kwargs): + kwargs['ssl_context'] = self._ssl_context + super(SSLHTTPAdapter, self).init_poolmanager(*args, **kwargs) + + class BaseRequestsClass(object): - _http_adapter = HTTPAdapter( + _session = Session() + _session.mount('https://', SSLHTTPAdapter( pool_maxsize=10, pool_block=True, max_retries=Retry( @@ -36,10 +50,7 @@ class BaseRequestsClass(object): status_forcelist={500, 502, 503, 504}, allowed_methods=None, ) - ) - - _session = Session() - _session.mount('https://', _http_adapter) + )) atexit.register(_session.close) def __init__(self, exc_type=None): From 3c26dabad57d5653e374231f616de6dd125aa942 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 14 Jul 2024 04:28:15 +1000 Subject: [PATCH 02/21] Pass XbmcContext instance to BaseRequestsClass to get settings TODO: Update stored values when settings change --- .../lib/youtube_plugin/kodion/network/http_server.py | 10 ++++++++-- .../lib/youtube_plugin/kodion/network/ip_api.py | 4 ++-- .../lib/youtube_plugin/kodion/network/requests.py | 6 ++---- .../kodion/settings/abstract_settings.py | 3 ++- .../youtube_plugin/youtube/client/login_client.py | 3 --- .../youtube_plugin/youtube/client/request_client.py | 12 ++++++++++-- .../lib/youtube_plugin/youtube/client/youtube.py | 2 +- .../lib/youtube_plugin/youtube/helper/subtitles.py | 2 +- .../youtube_plugin/youtube/helper/url_resolver.py | 2 +- .../lib/youtube_plugin/youtube/helper/video_info.py | 2 +- .../youtube_plugin/youtube/helper/yt_setup_wizard.py | 2 +- 11 files changed, 29 insertions(+), 19 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 9e71befc1..e11141b2c 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -43,9 +43,9 @@ def server_close(self): self.socket.close() -class RequestHandler(BaseHTTPRequestHandler, object): +class RequestHandler(BaseHTTPRequestHandler): _context = None - requests = BaseRequestsClass() + requests = None BASE_PATH = xbmcvfs.translatePath(TEMP_PATH) chunk_size = 1024 * 64 local_ranges = ( @@ -58,6 +58,8 @@ class RequestHandler(BaseHTTPRequestHandler, object): ) def __init__(self, *args, **kwargs): + if not RequestHandler.requests: + RequestHandler.requests = BaseRequestsClass(context=self._context) self.whitelist_ips = self._context.get_settings().httpd_whitelist() super(RequestHandler, self).__init__(*args, **kwargs) @@ -580,6 +582,8 @@ def httpd_status(context): url = 'http://{address}:{port}{path}'.format(address=address, port=port, path=PATHS.PING) + if not RequestHandler.requests: + RequestHandler.requests = BaseRequestsClass(context=context) response = RequestHandler.requests.request(url) result = response and response.status_code if result == 204: @@ -598,6 +602,8 @@ def get_client_ip_address(context): url = 'http://{address}:{port}{path}'.format(address=address, port=port, path=PATHS.IP) + if not RequestHandler.requests: + RequestHandler.requests = BaseRequestsClass(context=context) response = RequestHandler.requests.request(url) if response and response.status_code == 200: response_json = response.json() diff --git a/resources/lib/youtube_plugin/kodion/network/ip_api.py b/resources/lib/youtube_plugin/kodion/network/ip_api.py index 4c6ea9062..853ee216e 100644 --- a/resources/lib/youtube_plugin/kodion/network/ip_api.py +++ b/resources/lib/youtube_plugin/kodion/network/ip_api.py @@ -15,11 +15,11 @@ class Locator(BaseRequestsClass): - def __init__(self): + def __init__(self, context): self._base_url = 'http://ip-api.com' self._response = {} - super(Locator, self).__init__() + super(Locator, self).__init__(context=context) def response(self): return self._response diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index fd73165a6..f735a6bea 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -19,7 +19,6 @@ from urllib3.util.ssl_ import create_urllib3_context from ..logger import log_error -from ..settings import XbmcPluginSettings __all__ = ( @@ -53,11 +52,10 @@ class BaseRequestsClass(object): )) atexit.register(_session.close) - def __init__(self, exc_type=None): - settings = XbmcPluginSettings() + def __init__(self, context, exc_type=None): + settings = context.get_settings() self._verify = settings.verify_ssl() self._timeout = settings.get_timeout() - settings.flush(flush_all=False) if isinstance(exc_type, tuple): self._default_exc = (RequestException,) + exc_type diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index f2ea47f27..3af3346fd 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -195,9 +195,10 @@ def age_gate(self): return self.get_bool(SETTINGS.AGE_GATE, True) def verify_ssl(self): - verify = self.get_bool(SETTINGS.VERIFY_SSL, False) if sys.version_info <= (2, 7, 9): verify = False + else: + verify = self.get_bool(SETTINGS.VERIFY_SSL, True) return verify def get_timeout(self): diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 7a17a6c15..0293c65b4 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -80,9 +80,6 @@ def _error_hook(**kwargs): return None, None, None, json_data, False, InvalidGrant(json_data) return None, None, None, json_data, False, LoginException(json_data) - def verify(self): - return self._verify - def set_access_token(self, access_token=''): self._access_token = access_token diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index bb68cfe65..20f820723 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -288,7 +288,12 @@ class YouTubeRequestClient(BaseRequestsClass): }, } - def __init__(self, language=None, region=None, exc_type=None, **_kwargs): + def __init__(self, + context, + language=None, + region=None, + exc_type=None, + **_kwargs): common_client = self.CLIENTS['_common']['json']['context']['client'] # the default language is always en_US (like YouTube on the WEB) language = language.replace('-', '_') if language else 'en_US' @@ -302,7 +307,10 @@ def __init__(self, language=None, region=None, exc_type=None, **_kwargs): else: exc_type = (YouTubeException,) - super(YouTubeRequestClient, self).__init__(exc_type=exc_type) + super(YouTubeRequestClient, self).__init__( + context=context, + exc_type=exc_type, + ) @classmethod def json_traverse(cls, json_data, path, default=None): diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 9a4eaf9fa..5c38c314b 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -133,7 +133,7 @@ def __init__(self, context, **kwargs): if 'items_per_page' in kwargs: self._max_results = kwargs.pop('items_per_page') - super(YouTube, self).__init__(**kwargs) + super(YouTube, self).__init__(context=context, **kwargs) def get_max_results(self): return self._max_results diff --git a/resources/lib/youtube_plugin/youtube/helper/subtitles.py b/resources/lib/youtube_plugin/youtube/helper/subtitles.py index 1a03455ee..741ed0e47 100644 --- a/resources/lib/youtube_plugin/youtube/helper/subtitles.py +++ b/resources/lib/youtube_plugin/youtube/helper/subtitles.py @@ -391,7 +391,7 @@ def _get_url(self, track, lang=None): if not download: return subtitle_url, self.FORMATS[sub_format]['mime_type'] - response = BaseRequestsClass().request( + response = BaseRequestsClass(context=self._context).request( subtitle_url, headers=self.headers, error_info=('Subtitles._get_url - GET failed for: {lang}: {{exc}}' diff --git a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py index 4c95a4173..96fe526e8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/url_resolver.py +++ b/resources/lib/youtube_plugin/youtube/helper/url_resolver.py @@ -43,7 +43,7 @@ class AbstractResolver(BaseRequestsClass): def __init__(self, context): self._context = context - super(AbstractResolver, self).__init__() + super(AbstractResolver, self).__init__(context=context) def supports_url(self, url, url_components): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index bd3147abb..9b67fc672 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -713,7 +713,7 @@ def __init__(self, context, access_token='', **kwargs): 'android_embedded', ) - super(VideoInfo, self).__init__(**kwargs) + super(VideoInfo, self).__init__(context=context, **kwargs) @staticmethod def _response_hook_json(**kwargs): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 77f042c90..755b749ff 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -296,7 +296,7 @@ def process_geo_location(context, step, steps, **_kwargs): (localize('setup_wizard.prompt') % localize('setup_wizard.prompt.my_location')) ): - locator = Locator() + locator = Locator(context) locator.locate_requester() coords = locator.coordinates() if coords: From 3494a29e896b416ecbc7b117aa5510bf9713e3ed Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 14 Jul 2024 08:59:10 +1000 Subject: [PATCH 03/21] Update debug.Profiler to print a configurable number of lines and callees --- resources/lib/youtube_plugin/kodion/debug.py | 29 +++++++++++++++---- .../youtube_plugin/kodion/plugin_runner.py | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index 6898b32ec..ddd8ad333 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -42,6 +42,8 @@ class Profiler(object): __slots__ = ( '__weakref__', '_enabled', + '_num_lines', + '_print_callees', '_profiler', '_reuse', '_timer', @@ -115,9 +117,13 @@ def __init__(self, enabled=True, lazy=True, name=__name__, + num_lines=20, + print_callees=False, reuse=False, timer=None): self._enabled = enabled + self._num_lines = num_lines + self._print_callees = print_callees self._profiler = None self._reuse = reuse self._timer = timer @@ -140,7 +146,9 @@ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): return log_debug('Profiling stats: {0}'.format(self.get_stats( - reuse=self._reuse + num_lines=self._num_lines, + print_callees=self._print_callees, + reuse=self._reuse, ))) if not self._reuse: self.tear_down() @@ -218,7 +226,11 @@ def enable(self, flush=False): else: self._profiler.enable() - def get_stats(self, flush=True, reuse=False): + def get_stats(self, + flush=True, + num_lines=20, + print_callees=False, + reuse=False): if not (self._enabled and self._profiler): return None @@ -226,10 +238,15 @@ def get_stats(self, flush=True, reuse=False): output_stream = self._StringIO() try: - self._Stats( + stats = self._Stats( self._profiler, stream=output_stream - ).strip_dirs().sort_stats('cumulative', 'time').print_stats(20) + ) + stats.strip_dirs().sort_stats('cumulative', 'time') + if print_callees: + stats.print_callees(num_lines) + else: + stats.print_stats(num_lines) output = output_stream.getvalue() # Occurs when no stats were able to be generated from profiler except TypeError: @@ -245,7 +262,9 @@ def get_stats(self, flush=True, reuse=False): def print_stats(self): log_debug('Profiling stats: {0}'.format(self.get_stats( - reuse=self._reuse + num_lines=self._num_lines, + print_callees=self._print_callees, + reuse=self._reuse, ))) def tear_down(self): diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index ae617563f..66e361603 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -28,7 +28,7 @@ if _profiler: from .debug import Profiler - _profiler = Profiler(enabled=False) + _profiler = Profiler(enabled=False, print_callees=False, num_lines=20) def run(context=_context, From 0d4711b4c4d6e2671f2b9d410de50776fd579d60 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 14 Jul 2024 21:06:17 +1000 Subject: [PATCH 04/21] Disable jump to page in related video listings --- resources/lib/youtube_plugin/kodion/items/next_page_item.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index 19d12f0eb..32d50c0f5 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -24,7 +24,8 @@ def __init__(self, context, params, image=None, fanart=None): items_per_page = params.get('items_per_page', 50) can_jump = ('next_page_token' not in params and not path.startswith(('/channel', - '/special/recommendations'))) + '/special/recommendations', + '/special/related_videos'))) if 'page_token' not in params and can_jump: params['page_token'] = self.create_page_token(page, items_per_page) From 16404cd7ec0b0387c83a30de1c160cbcddd4f4bd Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 15 Jul 2024 06:33:17 +1000 Subject: [PATCH 05/21] Fix dummy next pages in related video listings --- resources/lib/youtube_plugin/youtube/client/youtube.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 5c38c314b..0b140cce6 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1283,7 +1283,9 @@ def get_related_videos(self, max_results=remaining, **kwargs ) - if continuation and 'nextPageToken' in continuation: + if not continuation: + break + if 'nextPageToken' in continuation: page_token = continuation['nextPageToken'] else: page_token = '' From c69783282e4bdb7e32e7a16cf169860de282e746 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 15 Jul 2024 06:46:09 +1000 Subject: [PATCH 06/21] Enable loop to first page on last page of manually paginated listings (My Subscriptions) --- .../lib/youtube_plugin/youtube/client/youtube.py | 5 +++++ .../lib/youtube_plugin/youtube/helper/v3.py | 16 ++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 0b140cce6..e3b14a219 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1458,6 +1458,10 @@ def get_my_subscriptions(self, v3_response = { 'kind': 'youtube#videoListResponse', 'items': [], + 'pageInfo': { + 'totalResults': 0, + 'resultsPerPage': self._max_results, + }, } cache = self._context.get_feed_history() @@ -1819,6 +1823,7 @@ def _threaded_fetch(kwargs, else: return None + v3_response['pageInfo']['totalResults'] = totals['num'] v3_response['items'] = items return v3_response diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 4f16e5151..241faba59 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -463,8 +463,8 @@ def response_to_items(provider, This should work for up to ~2000 entries. """ params = context.get_params() - current_page = params.get('page', 1) - next_page = current_page + 1 + current_page = params.get('page') + next_page = current_page + 1 if current_page else 2 new_params = dict(params, page=next_page) yt_next_page_token = json_data.get('nextPageToken') @@ -472,8 +472,14 @@ def response_to_items(provider, new_params['page_token'] = '' elif yt_next_page_token: new_params['page_token'] = yt_next_page_token - elif 'page_token' in new_params: - del new_params['page_token'] + else: + if 'page_token' in new_params: + del new_params['page_token'] + elif current_page: + new_params['page_token'] = '' + else: + return result + page_info = json_data.get('pageInfo', {}) yt_total_results = int(page_info.get('totalResults', 0)) yt_results_per_page = int(page_info.get('resultsPerPage', 50)) @@ -488,8 +494,6 @@ def response_to_items(provider, new_params['page'] = 1 else: return result - else: - return result yt_visitor_data = json_data.get('visitorData') if yt_visitor_data: From 05dce3ea30298de574a5a35bf5b892195ffef95d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 17 Jul 2024 07:44:39 +1000 Subject: [PATCH 07/21] Minimise unnecessary function calls and attr lookups in Kodi 19+ --- .../settings/xbmc/xbmc_plugin_settings.py | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) 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 af586fe1d..ba22a9731 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 @@ -22,67 +22,76 @@ class SettingsProxy(object): def __init__(self, instance): - self._ref = instance + self.ref = instance if current_system_version.compatible(21, 0): def get_bool(self, *args, **kwargs): - return xbmcaddon.Settings.getBool(self.ref(), *args, **kwargs) + return self.ref.getBool(*args, **kwargs) def set_bool(self, *args, **kwargs): - return xbmcaddon.Settings.setBool(self.ref(), *args, **kwargs) + return self.ref.setBool(*args, **kwargs) def get_int(self, *args, **kwargs): - return xbmcaddon.Settings.getInt(self.ref(), *args, **kwargs) + return self.ref.getInt(*args, **kwargs) def set_int(self, *args, **kwargs): - return xbmcaddon.Settings.setInt(self.ref(), *args, **kwargs) + return self.ref.setInt(*args, **kwargs) def get_str(self, *args, **kwargs): - return xbmcaddon.Settings.getString(self.ref(), *args, **kwargs) + return self.ref.getString(*args, **kwargs) def set_str(self, *args, **kwargs): - return xbmcaddon.Settings.setString(self.ref(), *args, **kwargs) + return self.ref.setString(*args, **kwargs) def get_str_list(self, *args, **kwargs): - return xbmcaddon.Settings.getStringList(self.ref(), *args, **kwargs) + return self.ref.getStringList(*args, **kwargs) def set_str_list(self, *args, **kwargs): - return xbmcaddon.Settings.setStringList(self.ref(), *args, **kwargs) + return self.ref.setStringList(*args, **kwargs) - def ref(self): - return self._ref else: def get_bool(self, *args, **kwargs): - return xbmcaddon.Addon.getSettingBool(self.ref(), *args, **kwargs) + return self.ref.getSettingBool(*args, **kwargs) def set_bool(self, *args, **kwargs): - return xbmcaddon.Addon.setSettingBool(self.ref(), *args, **kwargs) + return self.ref.setSettingBool(*args, **kwargs) def get_int(self, *args, **kwargs): - return xbmcaddon.Addon.getSettingInt(self.ref(), *args, **kwargs) + return self.ref.getSettingInt(*args, **kwargs) def set_int(self, *args, **kwargs): - return xbmcaddon.Addon.setSettingInt(self.ref(), *args, **kwargs) + return self.ref.setSettingInt(*args, **kwargs) def get_str(self, *args, **kwargs): - return xbmcaddon.Addon.getSettingString(self.ref(), *args, **kwargs) + return self.ref.getSettingString(*args, **kwargs) def set_str(self, *args, **kwargs): - return xbmcaddon.Addon.setSettingString(self.ref(), *args, **kwargs) + return self.ref.setSettingString(*args, **kwargs) def get_str_list(self, setting): - return xbmcaddon.Addon.getSetting(self.ref(), setting).split(',') + return self.ref.getSetting(setting).split(',') def set_str_list(self, setting, value): value = ','.join(value) - return xbmcaddon.Addon.setSetting(self.ref(), setting, value) + return self.ref.setSetting(setting, value) - if current_system_version.compatible(19, 0): + if not current_system_version.compatible(19, 0): + @property def ref(self): - return self._ref - else: + if self._ref: + return self._ref() + return None + + @ref.setter + def ref(self, value): + if value: + self._ref = ref(value) + else: + self._ref = None + + @ref.deleter def ref(self): - return self._ref() + del self._ref class XbmcPluginSettings(AbstractSettings): @@ -98,10 +107,12 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): xbmc_addon = xbmcaddon.Addon(ADDON_ID) else: if self.__class__._instances: - if not flush_all: - self.__class__._instances.discard(self._proxy.ref()) - else: + if flush_all: self.__class__._instances.clear() + else: + self.__class__._instances.discard(self._proxy.ref) + del self._proxy.ref + self._proxy.ref = None del self._proxy self._proxy = None return @@ -118,12 +129,10 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): # don't actually return anything... # Ignore return value until bug is fixed in Kodi self._check_set = False - elif current_system_version.compatible(19, 0): - self._proxy = SettingsProxy(xbmc_addon) else: - if fill: + if fill and not current_system_version.compatible(19, 0): self.__class__._instances.add(xbmc_addon) - self._proxy = SettingsProxy(ref(xbmc_addon)) + self._proxy = SettingsProxy(xbmc_addon) def get_bool(self, setting, default=None, echo=None): if setting in self._cache: From 3dc1f08e10e7b7cf646d317b44d27e2a8d7243e1 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:40:16 +1000 Subject: [PATCH 08/21] Update version checks for Kodi v22 Piers --- resources/lib/youtube_plugin/kodion/utils/system_version.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py index de4f500f1..b306abac1 100644 --- a/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -42,7 +42,9 @@ def __init__(self, version=None, releasename=None, appname=None): self._version = (1, 0) # Frodo self._appname = 'Unknown Application' - if self._version >= (21, 0): + if self._version >= (22, 0): + self._releasename = 'Piers' + elif self._version >= (21, 0): self._releasename = 'Omega' elif self._version >= (20, 0): self._releasename = 'Nexus' From ee14c78b0824f6d47bc7eef246c75a7a028e9f28 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:03:37 +1000 Subject: [PATCH 09/21] Attempt to workaround issue with getting system idle time on Xbox #839 --- .../kodion/monitors/service_monitor.py | 16 +++ .../youtube_plugin/kodion/service_runner.py | 129 +++++++++--------- 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index 9b7ef586e..a704d9573 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -29,6 +29,7 @@ class ServiceMonitor(xbmc.Monitor): _settings_changes = 0 _settings_state = None + get_idle_time = xbmc.getGlobalIdleTime def __init__(self, context): self._context = context @@ -43,6 +44,7 @@ def __init__(self, context): self.httpd_thread = None self.httpd_sleep_allowed = True + self.system_idle = False self.refresh = False self.interrupt = False @@ -114,6 +116,20 @@ def onNotification(self, sender, method, data): self._context.reload_access_manager() self.refresh_container() + def onScreensaverActivated(self): + self.system_idle = True + + def onScreensaverDeactivated(self): + self.system_idle = False + self.interrupt = True + + def onDPMSActivated(self): + self.system_idle = True + + def onDPMSDeactivated(self): + self.system_idle = False + self.interrupt = True + def onSettingsChanged(self): self._settings_changes += 1 if self._settings_state == 'defer': diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index 3d0405998..1e3efa730 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -32,7 +32,6 @@ def run(): provider = Provider() - get_infobool = context.get_infobool get_listitem_info = context.get_listitem_info get_listitem_property = context.get_listitem_property @@ -51,61 +50,86 @@ def run(): # wipe add-on temp folder on updates/restarts (subtitles, and mpd files) rm_dir(TEMP_PATH) - plugin_sleeping = False - plugin_sleep_timeout = httpd_sleep_timeout = 0 - ping_period = 60 - loop_num = sub_loop_num = 0 - restart_attempts = 0 + loop_period = 10 + loop_period_ms = loop_period * 1000 + + httpd_idle_time_ms = 0 + httpd_idle_timeout_ms = 30000 + httpd_ping_period_ms = 60000 + httpd_restart_attempts = 0 + httpd_max_restarts = 5 + + plugin_is_idle = False + plugin_idle_time_ms = 0 + plugin_idle_timeout_ms = 30000 + + active_interval_ms = 100 + idle_interval_ms = 1000 + video_id = None container = monitor.is_plugin_container() + while not monitor.abortRequested(): - idle = get_infobool('System.IdleTime(10)') + is_idle = monitor.system_idle or monitor.get_idle_time() >= loop_period - if idle: - if plugin_sleep_timeout >= 30: - plugin_sleep_timeout = 0 - if not plugin_sleeping: - plugin_sleeping = set_property(PLUGIN_SLEEPING) + if is_idle: + if plugin_idle_time_ms >= plugin_idle_timeout_ms: + plugin_idle_time_ms = 0 + if not plugin_is_idle: + plugin_is_idle = set_property(PLUGIN_SLEEPING) else: - plugin_sleep_timeout = 0 - if plugin_sleeping: - plugin_sleeping = clear_property(PLUGIN_SLEEPING) + plugin_idle_time_ms = 0 + if plugin_is_idle: + plugin_is_idle = clear_property(PLUGIN_SLEEPING) if not monitor.httpd: - httpd_sleep_timeout = 0 - elif idle: + httpd_idle_time_ms = 0 + elif is_idle: if monitor.httpd_sleep_allowed: - if httpd_sleep_timeout >= 30: + if httpd_idle_time_ms >= httpd_idle_timeout_ms: + httpd_idle_time_ms = 0 monitor.shutdown_httpd(sleep=True) else: if pop_property(SERVER_POST_START): monitor.httpd_sleep_allowed = True - httpd_sleep_timeout = 0 + httpd_idle_time_ms = 0 else: - if httpd_sleep_timeout >= ping_period: - httpd_sleep_timeout = 0 + if httpd_idle_time_ms >= httpd_ping_period_ms: + httpd_idle_time_ms = 0 if monitor.ping_httpd(): - restart_attempts = 0 - elif restart_attempts < 5: + httpd_restart_attempts = 0 + elif httpd_restart_attempts < httpd_max_restarts: monitor.restart_httpd() - restart_attempts += 1 + httpd_restart_attempts += 1 else: monitor.shutdown_httpd() + check_item = not plugin_is_idle and container['is_plugin'] + if check_item: + wait_interval_ms = active_interval_ms + else: + wait_interval_ms = idle_interval_ms + wait_interval = wait_interval_ms / 1000 + wait_time_ms = 0 + while not monitor.abortRequested(): - if container['is_plugin']: - wait_interval = 0.1 - if loop_num < 1: - loop_num = 1 - if sub_loop_num < 1: - sub_loop_num = 10 - - if monitor.refresh and all(container.values()): - monitor.refresh_container(force=True) - monitor.refresh = False - break - monitor.interrupt = False + if monitor.refresh and all(container.values()): + monitor.refresh_container(force=True) + monitor.refresh = False + break + if monitor.interrupt: + monitor.interrupt = False + container = monitor.is_plugin_container() + if check_item != container['is_plugin']: + check_item = not check_item + if check_item: + wait_interval_ms = active_interval_ms + else: + wait_interval_ms = idle_interval_ms + wait_interval = wait_interval_ms / 1000 + + if check_item: new_video_id = get_listitem_property(VIDEO_ID) if new_video_id: if video_id != new_video_id: @@ -114,34 +138,15 @@ def run(): elif video_id and get_listitem_info('Label'): video_id = None clear_property(VIDEO_ID) - else: - wait_interval = 1 - if loop_num < 1: - loop_num = 2 - if sub_loop_num < 1: - sub_loop_num = 5 - - if not plugin_sleeping: - plugin_sleeping = set_property(PLUGIN_SLEEPING) - - if sub_loop_num > 1: - sub_loop_num -= 1 - if monitor.interrupt: - container = monitor.is_plugin_container() - monitor.interrupt = False - else: - container = monitor.is_plugin_container() - sub_loop_num = 0 - loop_num -= 1 - if not wait_interval or container['is_plugin']: - wait_interval = 0.1 + elif not plugin_is_idle and not container['is_plugin']: + plugin_is_idle = set_property(PLUGIN_SLEEPING) - if wait_interval: - monitor.waitForAbort(wait_interval) - httpd_sleep_timeout += wait_interval - plugin_sleep_timeout += wait_interval + monitor.waitForAbort(wait_interval) + wait_time_ms += wait_interval_ms + httpd_idle_time_ms += wait_interval_ms + plugin_idle_time_ms += wait_interval_ms - if loop_num <= 0: + if wait_time_ms >= loop_period_ms: break else: break From 5ba18132157aab8fb27b07bdda9b5ece787c41af Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 22 Jul 2024 20:41:37 +1000 Subject: [PATCH 10/21] Respect disable certificate verification setting with cURL in ISA #841 --- .../lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py | 3 +++ .../lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) 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 c534c915c..9b260c22b 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -678,6 +678,9 @@ def use_inputstream_adaptive(self, prompt=False): 'live': loose_version('2.0.12'), 'drm': loose_version('2.2.12'), 'ttml': loose_version('20.0.0'), + # properties + 'config_prop': loose_version('21.4.11'), + 'manifest_config_prop': loose_version('21.4.5'), # audio codecs 'vorbis': loose_version('2.3.14'), # unknown when Opus audio support was implemented 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 08c350fa5..2ab8a60fa 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -417,10 +417,15 @@ def video_playback_item(context, video_item, show_fanart=None, **_kwargs): props[inputstream_property] = 'inputstream.adaptive' if current_system_version.compatible(21, 0): - if video_item.live: + isa_capabilities = context.inputstream_adaptive_capabilities() + if video_item.live and isa_capabilities['manifest_config_prop']: props['inputstream.adaptive.manifest_config'] = dumps({ 'timeshift_bufferlimit': 4 * 60 * 60, }) + if not settings.verify_ssl() and isa_capabilities['config_prop']: + props['inputstream.adaptive.config'] = dumps({ + 'ssl_verify_peer': False, + }) else: props['inputstream.adaptive.manifest_type'] = manifest_type From 1a280e1bcded5bd7c9c1f9d5b1ce172e624218e4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Jul 2024 07:51:59 +1000 Subject: [PATCH 11/21] Misc tidy ups --- .../kodion/context/abstract_context.py | 2 +- .../kodion/context/xbmc/xbmc_context.py | 6 ++++-- .../youtube_plugin/kodion/items/xbmc/xbmc_items.py | 8 ++++---- .../lib/youtube_plugin/youtube/helper/video_info.py | 12 +++++++++--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 359355858..1fba2f4f0 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -472,5 +472,5 @@ def get_listitem_info(detail_name): def tear_down(self): pass - def wakeup(self): + def wakeup(self, target, timeout=None): raise NotImplementedError() 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 9b260c22b..aad9572a9 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -644,7 +644,8 @@ def set_addon_enabled(self, addon_id, enabled=True): error.get('message', 'unknown'))) return False - def send_notification(self, method, data=True): + @staticmethod + def send_notification(method, data=True): jsonrpc(method='JSONRPC.NotifyAll', params={'sender': ADDON_ID, 'message': method, @@ -675,8 +676,9 @@ def use_inputstream_adaptive(self, prompt=False): # - any Falsy value to exclude capability regardless of version # - True to include capability regardless of version _ISA_CAPABILITIES = { - 'live': loose_version('2.0.12'), + # functionality 'drm': loose_version('2.2.12'), + 'live': loose_version('2.0.12'), 'ttml': loose_version('20.0.0'), # properties 'config_prop': loose_version('21.4.11'), 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 2ab8a60fa..ce4d5aa47 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -411,10 +411,10 @@ def video_playback_item(context, video_item, show_fanart=None, **_kwargs): manifest_type = 'hls' mime_type = 'application/x-mpegURL' - inputstream_property = ('inputstream' - if current_system_version.compatible(19, 0) else - 'inputstreamaddon') - props[inputstream_property] = 'inputstream.adaptive' + if current_system_version.compatible(19, 0): + props['inputstream'] = 'inputstream.adaptive' + else: + props['inputstreamaddon'] = 'inputstream.adaptive' if current_system_version.compatible(21, 0): isa_capabilities = context.inputstream_adaptive_capabilities() diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 9b67fc672..dbbc1ab43 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1350,9 +1350,15 @@ def _get_video_info(self): status = playability_status.get('status', '').upper() reason = playability_status.get('reason', '') - if status in {'', 'AGE_CHECK_REQUIRED', 'UNPLAYABLE', - 'CONTENT_CHECK_REQUIRED', 'LOGIN_REQUIRED', - 'AGE_VERIFICATION_REQUIRED', 'ERROR'}: + if status in { + '', + 'AGE_CHECK_REQUIRED', + 'AGE_VERIFICATION_REQUIRED', + 'CONTENT_CHECK_REQUIRED', + 'ERROR', + 'LOGIN_REQUIRED', + 'UNPLAYABLE', + }: if (playability_status.get('desktopLegacyAgeGateReason') and _settings.age_gate()): break From 2824308f744fd1ff42a3dcbd0e8feb12a455f757 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Jul 2024 08:17:20 +1000 Subject: [PATCH 12/21] Better handle unknown errors in player request responses #845 --- .../youtube/helper/video_info.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index dbbc1ab43..35abc94df 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1315,7 +1315,7 @@ def _get_video_info(self): while 1: for client_name in self._prioritised_clients: - if status and status != 'OK': + if status is not None: self._context.log_warning( 'Failed to retrieve video info - ' 'video_id: {0}, client: {1}, auth: {2},\n' @@ -1350,11 +1350,19 @@ def _get_video_info(self): status = playability_status.get('status', '').upper() reason = playability_status.get('reason', '') - if status in { + if video_details and video_id != video_details.get('videoId'): + status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' + reason = 'Watch on the latest version of YouTube' + continue + + if status == 'OK': + break + elif status in { '', 'AGE_CHECK_REQUIRED', 'AGE_VERIFICATION_REQUIRED', 'CONTENT_CHECK_REQUIRED', + 'CONTENT_NOT_AVAILABLE_IN_THIS_APP', 'ERROR', 'LOGIN_REQUIRED', 'UNPLAYABLE', @@ -1392,9 +1400,11 @@ def _get_video_info(self): if url and url.startswith('//support.google.com/youtube/answer/12318250'): status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' continue - if video_details and video_id != video_details.get('videoId'): - status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' - reason = 'Watch on the latest version of YouTube' + else: + self._context.log_debug( + 'Unknown playabilityStatus in player response:\n|{0}|' + .format(playability_status) + ) continue break # Only attempt to remove Authorization header if clients iterable From 3c612464cffcd7c850a74443e309478c90a509b2 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Jul 2024 08:19:26 +1000 Subject: [PATCH 13/21] Change XbmcContext.wakeup logging to display time in ms --- .../kodion/context/xbmc/xbmc_context.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index aad9572a9..b9741e577 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -784,21 +784,22 @@ def wakeup(self, target, timeout=None): pop_property = self.get_ui().pop_property no_timeout = timeout < 0 - remaining = timeout - wait_period = 0.1 + remaining = timeout = timeout * 1000 + wait_period_ms = 100 + wait_period = wait_period_ms / 1000 while no_timeout or remaining > 0: awake = pop_property(WAKEUP) if awake: if awake == target: - self.log_debug('Wakeup |{0}| in {1}s' + self.log_debug('Wakeup |{0}| in {1}ms' .format(awake, timeout - remaining)) else: - self.log_error('Wakeup |{0}| in {1}s - expected |{2}|' + self.log_error('Wakeup |{0}| in {1}ms - expected |{2}|' .format(awake, timeout - remaining, target)) break wait(wait_period) - remaining -= wait_period + remaining -= wait_period_ms else: - self.log_error('Wakeup |{0}| timed out in {1}s' + self.log_error('Wakeup |{0}| timed out in {1}ms' .format(target, timeout)) From 4967dda7d60c7b35ad238807daabe005e7d2ec3d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:59:13 +1000 Subject: [PATCH 14/21] Rename video_info module to stream_info - Format stream title when stream fetched, rather than looping through all streams - Allow for specific preferred client to be programmatically used for player requests - Fix not resetting failed request status when next client is not used - Allow creating audio only mpd manifests (currently disabled) --- .../youtube_plugin/youtube/client/youtube.py | 64 ++---------- .../helper/{video_info.py => stream_info.py} | 98 +++++++++++++------ .../youtube_plugin/youtube/helper/yt_play.py | 35 ++++--- resources/lib/youtube_resolver.py | 2 +- 4 files changed, 93 insertions(+), 106 deletions(-) rename resources/lib/youtube_plugin/youtube/helper/{video_info.py => stream_info.py} (96%) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index e3b14a219..b33a085d0 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -18,7 +18,7 @@ from random import randint from .login_client import LoginClient -from ..helper.video_info import VideoInfo +from ..helper.stream_info import StreamInfo from ..youtube_exceptions import InvalidJSON, YouTubeException from ...kodion.compatibility import cpu_count, string_type, to_str from ...kodion.utils import ( @@ -195,63 +195,11 @@ def update_watch_history(self, context, video_id, url, status=None): self.request(url, params=params, headers=headers, error_msg='Failed to update watch history') - def get_video_streams(self, context, video_id): - video_info = VideoInfo(context, access_token=self._access_token_tv, - language=self._language) - - video_streams = video_info.load_stream_infos(video_id) - - # update title - for video_stream in video_streams: - title = '%s (%s)' % ( - context.get_ui().bold(video_stream['title']), - video_stream['container'] - ) - - if 'audio' in video_stream and 'video' in video_stream: - if (video_stream['audio']['bitrate'] > 0 - and video_stream['video']['codec'] - and video_stream['audio']['codec']): - title = '%s (%s; %s / %s@%d)' % ( - context.get_ui().bold(video_stream['title']), - video_stream['container'], - video_stream['video']['codec'], - video_stream['audio']['codec'], - video_stream['audio']['bitrate'] - ) - - elif (video_stream['video']['codec'] - and video_stream['audio']['codec']): - title = '%s (%s; %s / %s)' % ( - context.get_ui().bold(video_stream['title']), - video_stream['container'], - video_stream['video']['codec'], - video_stream['audio']['codec'] - ) - elif 'audio' in video_stream and 'video' not in video_stream: - if (video_stream['audio']['codec'] - and video_stream['audio']['bitrate'] > 0): - title = '%s (%s; %s@%d)' % ( - context.get_ui().bold(video_stream['title']), - video_stream['container'], - video_stream['audio']['codec'], - video_stream['audio']['bitrate'] - ) - - elif 'audio' in video_stream or 'video' in video_stream: - codec = video_stream.get('audio', {}).get('codec') - if not codec: - codec = video_stream.get('video', {}).get('codec') - if codec: - title = '%s (%s; %s)' % ( - context.get_ui().bold(video_stream['title']), - video_stream['container'], - codec - ) - - video_stream['title'] = title - - return video_streams + def get_streams(self, context, video_id, audio_only=False): + return StreamInfo(context, + access_token=self._access_token_tv, + audio_only=audio_only, + language=self._language).load_stream_infos(video_id) def remove_playlist(self, playlist_id, **kwargs): params = {'id': playlist_id, diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py similarity index 96% rename from resources/lib/youtube_plugin/youtube/helper/video_info.py rename to resources/lib/youtube_plugin/youtube/helper/stream_info.py index 35abc94df..b282b2a3a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -37,7 +37,7 @@ from ...kodion.utils import make_dirs, redact_ip_from_url -class VideoInfo(YouTubeRequestClient): +class StreamInfo(YouTubeRequestClient): BASE_PATH = make_dirs(TEMP_PATH) FORMAT = { @@ -660,17 +660,24 @@ class VideoInfo(YouTubeRequestClient): 'dtse': 1.3, } - def __init__(self, context, access_token='', **kwargs): + def __init__(self, + context, + access_token='', + clients=None, + audio_only=False, + **kwargs): self.video_id = None self._context = context self._language_base = kwargs.get('language', 'en_US')[0:2] self._access_token = access_token + self._audio_only = audio_only self._player_js = None self._calculate_n = True self._cipher = None self._selected_client = None client_selection = context.get_settings().client_selection() + self._prioritised_clients = clients if clients else () # Default client selection uses the Android or iOS client as the first # option to ensure that the age gate setting is enforced, regardless of @@ -680,7 +687,7 @@ def __init__(self, context, access_token='', **kwargs): # Prefer iOS client to access premium streams, however other stream # types are limited if client_selection == 1: - self._prioritised_clients = ( + self._prioritised_clients += ( 'ios', 'android', 'android_youtube_tv', @@ -691,7 +698,7 @@ def __init__(self, context, access_token='', **kwargs): # Alternate #2 # Prefer use of non-adaptive formats. elif client_selection == 2: - self._prioritised_clients = ( + self._prioritised_clients += ( 'media_connect_frontend', 'android', 'android_youtube_tv', @@ -704,7 +711,7 @@ def __init__(self, context, access_token='', **kwargs): # Some restricted videos require additional requests for subtitles # Fallback to iOS, media connect, and embedded clients else: - self._prioritised_clients = ( + self._prioritised_clients += ( 'android', 'android_youtube_tv', 'android_testsuite', @@ -713,7 +720,7 @@ def __init__(self, context, access_token='', **kwargs): 'android_embedded', ) - super(VideoInfo, self).__init__(context=context, **kwargs) + super(StreamInfo, self).__init__(context=context, **kwargs) @staticmethod def _response_hook_json(**kwargs): @@ -790,12 +797,17 @@ def _get_stream_format(self, itag, info=None, max_height=None, **kwargs): yt_format = yt_format.copy() manual_sort = yt_format.get('sort', 0) + av_label = [yt_format['container']] if info: - video_info = info.get('video') or {} - yt_format['title'] = video_info.get('label', '') - yt_format['video']['codec'] = video_info.get('codec', '') - yt_format['video']['height'] = video_info.get('height', 0) + if 'video' in yt_format: + if self._audio_only: + del yt_format['video'] + else: + video_info = info['video'] or {} + yt_format['title'] = video_info.get('label', '') + yt_format['video']['codec'] = video_info.get('codec', '') + yt_format['video']['height'] = video_info.get('height', 0) audio_info = info.get('audio') or {} yt_format['audio']['codec'] = audio_info.get('codec', '') @@ -806,19 +818,24 @@ def _get_stream_format(self, itag, info=None, max_height=None, **kwargs): video_height = video_info.get('height', 0) if max_height and video_height > max_height: return False - video_sort = ( - video_height - * self.QUALITY_FACTOR.get(video_info.get('codec'), 1) - ) + codec = video_info.get('codec') + if codec: + video_sort = video_height * self.QUALITY_FACTOR.get(codec, 1) + av_label.append(codec) + else: + video_sort = video_height else: video_sort = -1 audio_info = yt_format.get('audio') if audio_info: - audio_sort = ( - audio_info.get('bitrate', 0) - * self.QUALITY_FACTOR.get(audio_info.get('codec'), 1) - ) + codec = audio_info.get('codec') + bitrate = audio_info.get('bitrate', 0) + audio_sort = bitrate * self.QUALITY_FACTOR.get(codec, 1) + if bitrate: + av_label.append('@'.join((codec, str(bitrate)))) + elif codec: + av_label.append(codec) else: audio_sort = 0 @@ -827,14 +844,22 @@ def _get_stream_format(self, itag, info=None, max_height=None, **kwargs): video_sort, audio_sort, ] + if kwargs: kwargs.update(yt_format) - return kwargs + yt_format = kwargs + + yt_format['title'] = ''.join(( + self._context.get_ui().bold(yt_format['title']), + ' (', + ' / '.join(av_label), + ')' + )) return yt_format def load_stream_infos(self, video_id): self.video_id = video_id - return self._get_video_info() + return self._get_stream_info() def _get_player_page(self, client_name='web', embed=False): if embed: @@ -973,8 +998,12 @@ def _normalize_url(url): url = urljoin('https://www.youtube.com', url) return url - def _load_hls_manifest(self, url, is_live=False, meta_info=None, - headers=None, playback_stats=None): + def _load_hls_manifest(self, + url, + is_live=False, + meta_info=None, + headers=None, + playback_stats=None): if not url: return [] @@ -1294,7 +1323,7 @@ def _get_error_details(self, playability_status, details=None): return result['simpleText'] return None - def _get_video_info(self): + def _get_stream_info(self): video_info_url = 'https://www.youtube.com/youtubei/v1/player' _settings = self._context.get_settings() @@ -1329,6 +1358,7 @@ def _get_video_info(self): ) client = self.build_client(client_name, client_data) if not client: + status = None continue result = self.request( @@ -1581,7 +1611,10 @@ def _get_video_info(self): if 'hlsManifestUrl' in streaming_data: stream_list.extend(self._load_hls_manifest( streaming_data['hlsManifestUrl'], - is_live, meta_info, client['headers'], playback_stats + is_live, + meta_info, + client['headers'], + playback_stats, )) subtitles = Subtitles(self._context, video_id) @@ -1631,7 +1664,7 @@ def _get_video_info(self): subs_data = None # extract adaptive streams and create MPEG-DASH manifest - if adaptive_fmts: + if adaptive_fmts and not self._audio_only: video_data, audio_data = self._process_stream_data( adaptive_fmts, default_lang['default'] @@ -1689,6 +1722,7 @@ def _get_video_info(self): def _process_stream_data(self, stream_data, default_lang_code='und'): _settings = self._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() @@ -1806,7 +1840,8 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): ) else: quality_group = mime_group - + elif audio_only: + continue else: data = video_data # Could use "zxx" language code for @@ -1913,7 +1948,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): details['baseUrlSecondary'] = secondary_url data[mime_group][itag] = data[quality_group][itag] = details - if not video_data: + if not video_data and not audio_only: self._context.log_debug('Generate MPD: No video mime-types found') return None, None @@ -1962,6 +1997,7 @@ def _generate_mpd_manifest(self, audio_data, subs_data, license_url): + # if (not video_data and not self._audio_only) or not audio_data: if not video_data or not audio_data: return None, None @@ -2021,11 +2057,15 @@ def _filter_group(previous_group, previous_stream, item): stream_select = _settings.stream_select() main_stream = { - 'video': video_data[0][1][0], 'audio': audio_data[0][1][0], 'multi_audio': False, 'multi_lang': False, } + if video_data: + main_stream['video'] = video_data[0][1][0] + duration = main_stream['video']['duration'] + else: + duration = main_stream['audio']['duration'] output = [ '\n' @@ -2034,7 +2074,7 @@ def _filter_group(previous_group, previous_stream, item): ' xmlns:xlink="http://www.w3.org/1999/xlink"' ' xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"' ' minBufferTime="PT1.5S"' - ' mediaPresentationDuration="PT', str(main_stream['video']['duration']), 'S"' + ' mediaPresentationDuration="PT', str(duration), 'S"' ' type="static"' ' profiles="urn:mpeg:dash:profile:isoff-main:2011"' '>\n' diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 00bcb41e1..8b5b246fa 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -33,7 +33,7 @@ from ...kodion.utils import find_video_id, select_stream -def _play_video(provider, context): +def _play_stream(provider, context): ui = context.get_ui() params = context.get_params() video_id = params.get('video_id') @@ -51,7 +51,7 @@ def _play_video(provider, context): is_external = ui.get_property(PLAY_WITH) if ((is_external and settings.alternative_player_web_urls()) or settings.default_player_web_urls()): - video_stream = { + stream = { 'url': 'https://youtu.be/{0}'.format(video_id), } else: @@ -61,11 +61,10 @@ def _play_video(provider, context): audio_only = None if ui.pop_property(PLAY_FORCE_AUDIO): - ask_for_quality = False audio_only = True try: - video_streams = client.get_video_streams(context, video_id) + streams = client.get_streams(context, video_id, audio_only) except YouTubeException as exc: context.log_error('yt_play.play_video - {exc}:\n{details}'.format( exc=exc, details=''.join(format_stack()) @@ -73,24 +72,24 @@ def _play_video(provider, context): ui.show_notification(message=exc.get_message()) return False - if not video_streams: + if not streams: message = context.localize('error.no_video_streams_found') ui.show_notification(message, time_ms=5000) return False - video_stream = select_stream( + stream = select_stream( context, - video_streams, + streams, ask_for_quality=ask_for_quality, audio_only=audio_only, use_adaptive_formats=(not is_external or settings.alternative_player_adaptive()), ) - if video_stream is None: + if stream is None: return False - video_type = video_stream.get('video') + video_type = stream.get('video') if video_type and video_type.get('rtmpe'): message = context.localize('error.rtmpe_not_supported') ui.show_notification(message, time_ms=5000) @@ -104,7 +103,7 @@ def _play_video(provider, context): v3, video_id) - metadata = video_stream.get('meta', {}) + metadata = stream.get('meta', {}) video_details = metadata.get('video', {}) if is_external: @@ -112,18 +111,18 @@ def _play_video(provider, context): 'http', get_connect_address(context=context, as_netloc=True), PATHS.REDIRECT, - urlencode({'url': video_stream['url']}), + urlencode({'url': stream['url']}), '', )) - video_stream['url'] = url - video_item = VideoItem(video_details.get('title', ''), video_stream['url']) + stream['url'] = url + video_item = VideoItem(video_details.get('title', ''), stream['url']) - use_history = not (screensaver or incognito or video_stream.get('live')) + use_history = not (screensaver or incognito or stream.get('live')) use_remote_history = use_history and settings.use_remote_history() use_play_data = use_history and settings.use_local_history() utils.update_play_info(provider, context, video_id, video_item, - video_stream, use_play_data=use_play_data) + stream, use_play_data=use_play_data) seek_time = 0.0 if params.get('resume') else params.get('seek', 0.0) start_time = params.get('start', 0.0) @@ -137,7 +136,7 @@ def _play_video(provider, context): # video_item.set_duration_from_seconds(end_time) play_count = use_play_data and video_item.get_play_count() or 0 - playback_stats = video_stream.get('playback_stats') + playback_stats = stream.get('playback_stats') playback_data = { 'video_id': video_id, @@ -337,9 +336,9 @@ def process(provider, context, **_kwargs): context.execute('Action(Play)') return False context.wakeup(SERVER_WAKEUP, timeout=5) - video = _play_video(provider, context) + video_item = _play_stream(provider, context) ui.set_property(SERVER_POST_START) - return video + return video_item if playlist_id or 'playlist_ids' in params: return _play_playlist(provider, context) diff --git a/resources/lib/youtube_resolver.py b/resources/lib/youtube_resolver.py index fd7648f01..72eb0ae43 100644 --- a/resources/lib/youtube_resolver.py +++ b/resources/lib/youtube_resolver.py @@ -53,7 +53,7 @@ def resolve(video_id, sort=True, addon_id=None): break if matched_id: - streams = client.get_video_streams(context=context, video_id=matched_id) + streams = client.get_streams(context=context, video_id=matched_id) if sort and streams: streams = sorted(streams, key=lambda x: x.get('sort', (0, 0))) From e396150f84929e958b79f5191d692d03f8d41169 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Jul 2024 06:00:45 +1000 Subject: [PATCH 15/21] Change Item channel/subscription/playlist id attribute getter/setter to property --- .../youtube_plugin/kodion/items/base_item.py | 5 +--- .../kodion/items/directory_item.py | 26 ++++++++++++------- .../youtube_plugin/kodion/items/video_item.py | 24 +++++++++++------ .../kodion/items/xbmc/xbmc_items.py | 14 +++++----- .../youtube_plugin/youtube/helper/utils.py | 10 +++---- 5 files changed, 45 insertions(+), 34 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 383250278..c193e28bd 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -19,13 +19,10 @@ class BaseItem(object): - VERSION = 3 - + _version = 3 _playable = False def __init__(self, name, uri, image=None, fanart=None): - self._version = BaseItem.VERSION - self._name = None self.set_name(name) diff --git a/resources/lib/youtube_plugin/kodion/items/directory_item.py b/resources/lib/youtube_plugin/kodion/items/directory_item.py index e5a33ef15..a4cab88be 100644 --- a/resources/lib/youtube_plugin/kodion/items/directory_item.py +++ b/resources/lib/youtube_plugin/kodion/items/directory_item.py @@ -85,24 +85,30 @@ def set_action(self, value): if isinstance(value, bool): self._is_action = value - def set_subscription_id(self, value): - self._subscription_id = value - - def get_subscription_id(self): + @property + def subscription_id(self): return self._subscription_id - def set_channel_id(self, value): - self._channel_id = value + @subscription_id.setter + def subscription_id(self, value): + self._subscription_id = value - def get_channel_id(self): + @property + def channel_id(self): return self._channel_id - def set_playlist_id(self, value): - self._playlist_id = value + @channel_id.setter + def channel_id(self, value): + self._channel_id = value - def get_playlist_id(self): + @property + def playlist_id(self): return self._playlist_id + @playlist_id.setter + def playlist_id(self, value): + self._playlist_id = value + @property def next_page(self): return self._next_page diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 8f592f7ed..acf320bfd 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -374,28 +374,36 @@ def video_id(self): def video_id(self, value): self._video_id = value - def get_channel_id(self): + @property + def channel_id(self): return self._channel_id - def set_channel_id(self, value): + @channel_id.setter + def channel_id(self, value): self._channel_id = value - def get_subscription_id(self): + @property + def subscription_id(self): return self._subscription_id - def set_subscription_id(self, value): + @subscription_id.setter + def subscription_id(self, value): self._subscription_id = value - def get_playlist_id(self): + @property + def playlist_id(self): return self._playlist_id - def set_playlist_id(self, value): + @playlist_id.setter + def playlist_id(self, value): self._playlist_id = value - def get_playlist_item_id(self): + @property + def playlist_item_id(self): return self._playlist_item_id - def set_playlist_item_id(self, value): + @playlist_item_id.setter + def playlist_item_id(self, value): self._playlist_item_id = value def get_code(self): 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 ce4d5aa47..a286e3c8d 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -537,17 +537,17 @@ def directory_listitem(context, directory_item, show_fanart=None, **_kwargs): else: special_sort = 'top' - prop_value = directory_item.get_subscription_id() + prop_value = directory_item.subscription_id if prop_value: special_sort = None props[SUBSCRIPTION_ID] = prop_value - prop_value = directory_item.get_channel_id() + prop_value = directory_item.channel_id if prop_value: special_sort = None props[CHANNEL_ID] = prop_value - prop_value = directory_item.get_playlist_id() + prop_value = directory_item.playlist_id if prop_value: special_sort = None props[PLAYLIST_ID] = prop_value @@ -684,22 +684,22 @@ def video_listitem(context, props[VIDEO_ID] = prop_value # make channel_id property available for keymapping - prop_value = video_item.get_channel_id() + prop_value = video_item.channel_id if prop_value: props[CHANNEL_ID] = prop_value # make subscription_id property available for keymapping - prop_value = video_item.get_subscription_id() + prop_value = video_item.subscription_id if prop_value: props[SUBSCRIPTION_ID] = prop_value # make playlist_id property available for keymapping - prop_value = video_item.get_playlist_id() + prop_value = video_item.playlist_id if prop_value: props[PLAYLIST_ID] = prop_value # make playlist_item_id property available for keymapping - prop_value = video_item.get_playlist_item_id() + prop_value = video_item.playlist_item_id if prop_value: props[PLAYLISTITEM_ID] = prop_value diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 072136ade..9117de73f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -192,7 +192,7 @@ def update_channel_infos(provider, context, channel_id_dict, # -- unsubscribe from channel subscription_id = subscription_id_dict.get(channel_id, '') if subscription_id: - channel_item.set_subscription_id(subscription_id) + channel_item.subscription_id = subscription_id context_menu.append( menu_items.unsubscribe_from_channel( context, subscription_id=subscription_id @@ -649,7 +649,7 @@ def update_video_infos(provider, context, video_id_dict, # update channel mapping channel_id = snippet.get('channelId', '') - video_item.set_channel_id(channel_id) + video_item.channel_id = channel_id if channel_id and channel_items_dict is not None: if channel_id not in channel_items_dict: channel_items_dict[channel_id] = [] @@ -710,8 +710,8 @@ def update_video_infos(provider, context, video_id_dict, and playlist_channel_id == 'mine' and playlist_id.strip().lower() not in {'hl', 'wl'}): playlist_item_id = playlist_item_id_dict[video_id] - video_item.set_playlist_id(playlist_id) - video_item.set_playlist_item_id(playlist_item_id) + video_item.playlist_id = playlist_id + video_item.playlist_item_id = playlist_item_id context_menu.append( menu_items.remove_video_from_playlist( context, @@ -724,7 +724,7 @@ def update_video_infos(provider, context, video_id_dict, # got to [CHANNEL] only if we are not directly in the channel if (channel_id and channel_name and context.create_path('channel', channel_id) != path): - video_item.set_channel_id(channel_id) + video_item.channel_id = channel_id context_menu.append( menu_items.go_to_channel( context, channel_id, channel_name From 01bfb2e2a9db0da4fdd9d10b02feef3a3d8d6ca1 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Jul 2024 06:03:53 +1000 Subject: [PATCH 16/21] Update bookmarks when listing rather than only using item snapshot --- .../lib/youtube_plugin/youtube/helper/v3.py | 3 + .../lib/youtube_plugin/youtube/provider.py | 86 +++++++++++-------- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 241faba59..a3f402741 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -258,6 +258,9 @@ def _process_list_response(provider, context, json_data, item_filter): position = snippet.get('position') or len(result) item.set_track_number(position + 1) + if '_callback' in yt_item: + yt_item['_callback'](item) + result.append(item) # this will also update the channel_id_dict with the correct channel_id diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 0baf9a223..735d1c501 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -45,6 +45,7 @@ NewSearchItem, SearchItem, UriItem, + VideoItem, menu_items, ) from ..kodion.utils import strip_html_from_text @@ -1407,45 +1408,62 @@ def on_bookmarks(provider, context, re_match): return True v3_response = { - 'kind': 'youtube#channelListResponse', - 'items': [ - { - 'kind': 'youtube#channel', - 'id': item_id, - '_partial': True, - } - for item_id, item in items.items() - if isinstance(item, float) - ] + 'kind': 'youtube#pluginListResponse', + 'items': [] } - channel_items = v3.response_to_items(provider, context, v3_response) - for channel_item in channel_items: - channel_id = channel_item.get_channel_id() - if channel_id not in items: - continue - timestamp = items[channel_id] - channel_item.set_bookmark_timestamp(timestamp) - items[channel_id] = channel_item - bookmarks_list.update_item( - channel_id, repr(channel_item), timestamp - ) - bookmarks = [] + def _update_bookmark(_id, timestamp): + def _update(new_item): + new_item.set_bookmark_timestamp(timestamp) + bookmarks_list.update_item(_id, repr(new_item), timestamp) + + return _update + for item_id, item in items.items(): - if not isinstance(item, BaseItem): + if isinstance(item, float): + kind = 'youtube#channel' + yt_id = item_id + callback = _update_bookmark(item_id, item) + partial = True + else: + callback = None + partial = False + if isinstance(item, VideoItem): + kind = 'youtube#video' + yt_id = item.video_id + else: + yt_id = item.playlist_id + if yt_id: + kind = 'youtube#playlist' + else: + kind = 'youtube#channel' + yt_id = item.channel_id + + if not yt_id: continue - context_menu = [ - menu_items.bookmark_remove( - context, item_id - ), - menu_items.bookmarks_clear( - context - ), - menu_items.separator(), - ] - item.add_context_menu(context_menu, position=0) - bookmarks.append(item) + item = { + 'kind': kind, + 'id': yt_id, + '_partial': partial, + '_context_menu': { + 'context_menu': ( + menu_items.bookmark_remove( + context, item_id + ), + menu_items.bookmarks_clear( + context + ), + menu_items.separator(), + ), + 'position': 0, + }, + } + if callback: + item['_callback'] = callback + v3_response['items'].append(item) + + bookmarks = v3.response_to_items(provider, context, v3_response) return bookmarks ui = context.get_ui() From cbe625984260e35d5235b682f0f24acb68684ac6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Jul 2024 06:25:59 +1000 Subject: [PATCH 17/21] Add context menu items when creating list items for internal watch later and history list --- .../lib/youtube_plugin/youtube/provider.py | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 735d1c501..9b8460877 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1017,24 +1017,23 @@ def on_playback_history(provider, context, re_match): 'kind': 'youtube#video', 'id': video_id, '_partial': True, + '_context_menu': { + 'context_menu': ( + menu_items.history_remove( + context, video_id + ), + menu_items.history_clear( + context + ), + menu_items.separator(), + ), + 'position': 0, + } } for video_id in items.keys() ] } video_items = v3.response_to_items(provider, context, v3_response) - - for video_item in video_items: - context_menu = [ - menu_items.history_remove( - context, video_item.video_id - ), - menu_items.history_clear( - context - ), - menu_items.separator(), - ] - video_item.add_context_menu(context_menu, position=0) - return video_items if action == 'clear' and context.get_ui().on_yes_no_input( @@ -1531,24 +1530,23 @@ def on_watch_later(provider, context, re_match): 'kind': 'youtube#video', 'id': video_id, '_partial': True, + '_context_menu': { + 'context_menu': ( + menu_items.watch_later_local_remove( + context, video_id + ), + menu_items.watch_later_local_clear( + context + ), + menu_items.separator(), + ), + 'position': 0, + } } for video_id in items.keys() ] } video_items = v3.response_to_items(provider, context, v3_response) - - for video_item in video_items: - context_menu = [ - menu_items.watch_later_local_remove( - context, video_item.video_id - ), - menu_items.watch_later_local_clear( - context - ), - menu_items.separator(), - ] - video_item.add_context_menu(context_menu, position=0) - return video_items if command == 'clear' and context.get_ui().on_yes_no_input( From aee02aee303dfb6d51da6f5433656a821b7412ae Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Jul 2024 06:52:18 +1000 Subject: [PATCH 18/21] Fix double playback due to busy dialog crash workaround --- .../lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0d150dc8a..a4e9912a7 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -13,7 +13,7 @@ from traceback import format_stack from ..abstract_plugin import AbstractPlugin -from ...compatibility import xbmcplugin +from ...compatibility import xbmc, xbmcplugin from ...constants import ( BUSY_FLAG, CHECK_SETTINGS, @@ -107,6 +107,8 @@ def run(self, provider, context, focused=None): 'reloading playlist') num_items = playlist.add_items(items) + if xbmc.Player().isPlaying(): + return False if position: max_wait_time = min(position, num_items) else: From acbd0654dd19f70869699e5614aac0accb3e02c0 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:11:15 +1000 Subject: [PATCH 19/21] Update client details and usage --- .../lib/youtube_plugin/youtube/client/request_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 20f820723..465cc3222 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -88,14 +88,15 @@ class YouTubeRequestClient(BaseRequestsClass): # Limited to 720p on some videos 'android_embedded': { '_id': 55, + '_disabled': True, '_query_subtitles': 'optional', 'json': { 'params': _ANDROID_PARAMS, 'context': { 'client': { 'clientName': 'ANDROID_EMBEDDED_PLAYER', - 'clientVersion': '19.17.34', 'clientScreen': 'EMBED', + 'clientVersion': '19.17.34', 'androidSdkVersion': '34', 'osName': 'Android', 'osVersion': '14', @@ -115,6 +116,9 @@ class YouTubeRequestClient(BaseRequestsClass): 'X-YouTube-Client-Name': '{_id}', 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', }, + 'params': { + 'key': _API_KEYS['android_embedded'], + }, }, # 4k with HDR # Some videos block this client, may also require embedding enabled @@ -207,7 +211,6 @@ class YouTubeRequestClient(BaseRequestsClass): }, 'media_connect_frontend': { '_id': 95, - '_access_token': KeyError, '_query_subtitles': True, 'json': { 'context': { From c55fac2da3c7ca9ff74bd2d53cd069a8a4f9ffc4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:12:54 +1000 Subject: [PATCH 20/21] Update hardcoded itags TODO: parse new itag details instead --- .../youtube/helper/stream_info.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index b282b2a3a..91f92e4de 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -566,6 +566,24 @@ class StreamInfo(YouTubeRequestClient): 'hls/video': True, 'fps': 60, 'video': {'height': 1080, 'codec': 'vp9'}}, + '620': {'container': 'hls', + 'title': '1440p', + 'hls/video': True, + 'video': {'height': 1440, 'codec': 'vp9'}}, + '623': {'container': 'hls', + 'title': '1440p@60', + 'hls/video': True, + 'fps': 60, + 'video': {'height': 1440, 'codec': 'vp9'}}, + '625': {'container': 'hls', + 'title': '4k', + 'hls/video': True, + 'video': {'height': 2160, 'codec': 'vp9'}}, + '628': {'container': 'hls', + 'title': '4k@60', + 'hls/video': True, + 'fps': 60, + 'video': {'height': 2160, 'codec': 'vp9'}}, '9994': {'container': 'hls', 'title': 'Adaptive HLS', 'hls/audio': True, From 37144d6b2ca8caa1104c4196a479be1ceb8364be Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:11:28 +1000 Subject: [PATCH 21/21] Version bump v7.0.9+beta.4 --- addon.xml | 2 +- changelog.txt | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 38810bab3..f3a9876ab 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 0f64eb1c0..5f4318280 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,17 @@ +## v7.0.9+beta.4 +### Fixed +- Fix issues with next page and jump to page in related video listings +- Attempt to workaround issue with getting system idle time on Xbox #839 +- Better handle unknown errors in player request responses #845 +- Fix double playback due to busy dialog crash workaround + +### Changed +- Respect disable certificate verification setting with cURL in ISA #841 +- Update bookmarks when listing rather than only using item snapshot + +### New +- Enable loop to first page on last page of manually paginated listings (My Subscriptions) + ## v7.0.9+beta.3 ### Fixed - Fix navigating to search page after playback and prompt re-opening