From 6cc994d467d6bb2a413e3f28702774e2ab83d718 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:15:38 +0900 Subject: [PATCH 01/19] Log stream proxy request range #1363 --- 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 dfdd3736..7a963794 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -440,6 +440,7 @@ class RequestHandler(BaseHTTPRequestHandler, object): 'Method: {method!r}', 'Server: {server!r}', 'Target: {target!r}', + 'Range: {range!r}', 'Status: {status} {reason}') response = None @@ -493,6 +494,7 @@ class RequestHandler(BaseHTTPRequestHandler, object): method=method, server=server, target=target, + range=self.headers.get('Range'), status=-1, reason='Failed', ) @@ -547,6 +549,7 @@ class RequestHandler(BaseHTTPRequestHandler, object): method=method, server=server, target=target, + range=self.headers.get('Range'), status=status, reason=reason, ) From e5d81c20aee482206d1099414ee8a6c62c692d7d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:45:06 +0900 Subject: [PATCH 02/19] Fix possible exception when using default fallback for failed search --- resources/lib/youtube_plugin/kodion/abstract_provider.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index d5a43064..71c528c1 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -16,6 +16,7 @@ from re import ( ) from . import logging +from .compatibility import string_type from .constants import ( CHECK_SETTINGS, CONTENT, @@ -416,7 +417,8 @@ class AbstractProvider(object): fallback = options.setdefault( provider.FALLBACK, context.get_uri() ) - ui.set_property(provider.FALLBACK, fallback) + if fallback and isinstance(fallback, string_type): + ui.set_property(provider.FALLBACK, fallback) return result, options command = 'list' context.set_path(PATHS.SEARCH, command) From 163f3c610abe2428f847a380b1fa5d5f6aa38959 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:15:38 +0900 Subject: [PATCH 03/19] Log summary of stream proxy request and response details #1363 - Only log full request parameters if verbose logging is enabled --- .../kodion/network/http_server.py | 62 ++++++++++++++++--- 1 file changed, 54 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 7a963794..c74b43fc 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -230,7 +230,7 @@ class RequestHandler(BaseHTTPRequestHandler, object): 'log_uri': log_uri, } - if not path['path'].startswith(PATHS.PING): + if not path['path'].startswith(PATHS.PING) and self.log.verbose_logging: self.log.debug(('{status}', 'Method: {method!r}', 'Path: {path[path]!r}', @@ -385,13 +385,14 @@ class RequestHandler(BaseHTTPRequestHandler, object): params = path['params'] original_path = params.pop('__path', empty)[0] or '/videoplayback' request_servers = params.pop('__host', empty) - stream_id = params.pop('__id', empty)[0] + stream_id = params.pop('__id', empty) method = params.pop('__method', empty)[0] or 'POST' if original_path == '/videoplayback': - stream_id = (stream_id, params.get('itag', empty)[0]) + stream_id += params.get('itag', empty) + stream_id = tuple(stream_id) stream_type = params.get('mime', empty)[0] if stream_type: - stream_type = stream_type.split('/') + stream_type = tuple(stream_type.split('/')) else: stream_type = (None, None) ids = self.server_priority_list['stream_ids'] @@ -416,11 +417,13 @@ class RequestHandler(BaseHTTPRequestHandler, object): 'list': priority_list, } elif original_path == '/api/timedtext': + stream_id = tuple(stream_id) stream_type = (params.get('type', ['track'])[0], params.get('fmt', empty)[0], params.get('kind', empty)[0]) priority_list = [] else: + stream_id = tuple(stream_id) stream_type = (None, None) priority_list = [] @@ -432,16 +435,51 @@ class RequestHandler(BaseHTTPRequestHandler, object): else: headers = self.headers + byte_range = headers.get('Range') + client = headers.get('X-YouTube-Client-Name') + if self.log.debugging: + if 'c' in params: + if client: + client = '%s (%s)' % ( + client, + params.get('c', empty)[0], + ) + else: + client = params.get('c', empty)[0] + + clen = params.get('clen', empty)[0] + duration = params.get('dur', empty)[0] + if (not byte_range + or not clen + or not duration + or not byte_range.startswith('bytes=')): + timestamp = '' + else: + try: + timestamp = ' (~%.2fs)' % ( + float(duration) + * + next(map(int, byte_range[6:].split('-'))) + / + int(clen) + ) + except (IndexError, StopIteration, ValueError): + timestamp = '' + else: + timestamp = '' + original_query_str = urlencode(params, doseq=True) stream_redirect = settings.httpd_stream_redirect() log_msg = ('Stream proxy response {success}', + 'Stream: {stream_id} - {stream_type}', 'Method: {method!r}', 'Server: {server!r}', 'Target: {target!r}', - 'Range: {range!r}', - 'Status: {status} {reason}') + 'Status: {status} {reason}', + 'Client: {client}', + 'Range: {byte_range!r}{timestamp}') response = None server = None @@ -491,12 +529,16 @@ class RequestHandler(BaseHTTPRequestHandler, object): level=logging.WARNING, msg=log_msg, success='not OK', + stream_id=stream_id, + stream_type=stream_type, method=method, server=server, target=target, - range=self.headers.get('Range'), status=-1, reason='Failed', + client=client, + byte_range=byte_range, + timestamp=timestamp, ) break with response: @@ -546,12 +588,16 @@ class RequestHandler(BaseHTTPRequestHandler, object): level=log_level, msg=log_msg, success=('OK' if success else 'not OK'), + stream_id=stream_id, + stream_type=stream_type, method=method, server=server, target=target, - range=self.headers.get('Range'), status=status, reason=reason, + client=client, + byte_range=byte_range, + timestamp=timestamp, ) if not success: From df67a86eaa2a2b8e8412947abaec6819dcc83b23 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:36:58 +0900 Subject: [PATCH 04/19] Simplify API request verbose logging --- .../youtube/client/data_client.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/data_client.py b/resources/lib/youtube_plugin/youtube/client/data_client.py index 45f3413a..9b57d5bf 100644 --- a/resources/lib/youtube_plugin/youtube/client/data_client.py +++ b/resources/lib/youtube_plugin/youtube/client/data_client.py @@ -2976,21 +2976,19 @@ class YouTubeDataClient(YouTubeLoginClient): return None, None with response: headers = response.headers - if kwargs.get('extended_debug'): - self.log.debug(('Request response', - 'Status: {response.status_code!r}', - 'Headers: {headers!r}', - 'Content: {response.text}'), - response=response, - headers=headers._store if headers else None, - stacklevel=4) + if self.log.verbose_logging: + log_msg = ('Request response', + 'Status: {response.status_code!r}', + 'Headers: {headers!r}', + 'Content: {response.text}') else: - self.log.debug(('Request response', - 'Status: {response.status_code!r}', - 'Headers: {headers!r}'), - response=response, - headers=headers._store if headers else None, - stacklevel=4) + log_msg = ('Request response', + 'Status: {response.status_code!r}', + 'Headers: {headers!r}') + self.log.debug(log_msg, + response=response, + headers=headers._store if headers else None, + stacklevel=4) if response.status_code == 204 and 'no_content' in kwargs: return None, True @@ -3166,8 +3164,6 @@ class YouTubeDataClient(YouTubeLoginClient): ) self.log.warning('Aborted', stacklevel=2) return {} - if context.get_settings().log_level() & 2: - kwargs.setdefault('extended_debug', True) if cache is None and 'no_content' in kwargs: cache = False elif cache is not False and self._context.refresh_requested(): From 364856f692f9e38deb24b936e2e862bb49aa08f4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:45:19 +0900 Subject: [PATCH 05/19] Update XbmcContext.is_plugin_folder() to specifically use current container - Was previously using current control, which may not be a container - Also update AbstractContext.is_plugin_folder() to match signature --- .../lib/youtube_plugin/kodion/context/abstract_context.py | 3 +-- .../lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index f23f2292..11b1eaa1 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -694,8 +694,7 @@ class AbstractContext(object): def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False): raise NotImplementedError() - @staticmethod - def is_plugin_folder(folder_name=None): + def is_plugin_folder(self, folder_name=None): raise NotImplementedError() def refresh_requested(self, force=False, on=False, off=False, params=None): 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 05412ae9..b672493b 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -1027,7 +1027,7 @@ class XbmcContext(AbstractContext): def is_plugin_folder(self, folder_name=None): if folder_name is None: folder_name = XbmcContextUI.get_container_info(FOLDER_NAME, - container_id=False) + container_id=None) return folder_name == self._plugin_name def refresh_requested(self, force=False, on=False, off=False, params=None): From be4c7800ed3c304110dd7860d98896b338f395fd Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:47:05 +0900 Subject: [PATCH 06/19] Update AbstractContext.create_uri() to allow passing additional kwargs to builtins --- .../kodion/context/abstract_context.py | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 11b1eaa1..b896813b 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -364,7 +364,8 @@ class AbstractContext(object): run=False, play=None, window=None, - command=False): + command=False, + **kwargs): if isinstance(path, (list, tuple)): uri = self.create_path(*path, is_uri=True) elif path: @@ -398,22 +399,7 @@ class AbstractContext(object): uri = '?'.join((uri, params)) command = 'command://' if command else '' - if run: - return ''.join((command, - 'RunAddon(' - if run == 'addon' else - 'RunScript(' - if run == 'script' else - 'RunPlugin(', - uri, - ')')) - if play is not None: - return ''.join(( - command, - 'PlayMedia(', - uri, - ',playlist_type_hint=', str(play), ')', - )) + if window: if not isinstance(window, dict): window = {} @@ -444,6 +430,35 @@ class AbstractContext(object): ',replace' if history_replace else '', ')' )) + + kwargs = ',' + ','.join([ + '%s=%s' % (kwarg, value) + if value is not None else + kwarg + for kwarg, value in kwargs.items() + ]) if kwargs else '' + + if run: + return ''.join(( + command, + 'RunAddon(' + if run == 'addon' else + 'RunScript(' + if run == 'script' else + 'RunPlugin(', + uri, + kwargs, + ')' + )) + if play is not None: + return ''.join(( + command, + 'PlayMedia(', + uri, + kwargs, + ',playlist_type_hint=', str(play), + ')', + )) return uri def get_parent_uri(self, **kwargs): From 179bf4e40eb91522dd8326d6913f7b42e14c6048 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:56:21 +0900 Subject: [PATCH 07/19] Use existing Kodi translatable string for queue context menu label --- resources/language/resource.language.en_gb/strings.po | 2 +- .../lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index c6eaa069..d99882b4 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -350,7 +350,7 @@ msgid "My Subscriptions" msgstr "" msgctxt "#30511" -msgid "Queue video" +msgid "" msgstr "" msgctxt "#30512" 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 b672493b..80791eb8 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -377,7 +377,7 @@ class XbmcContext(AbstractContext): 'video.play.timeshift': 30819, 'video.play.using': 15213, 'video.play.with_subtitles': 30702, - 'video.queue': 30511, + 'video.queue': 13347, 'video.rate': 30528, 'video.rate.dislike': 30530, 'video.rate.like': 30529, From 1d1f44c0e529839d195c8623d099d516408ff414 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:27:43 +0900 Subject: [PATCH 08/19] Add context menu items to open Settings/Setup Wizard from Setup Wizard/Settings main menu entries --- .../kodion/constants/const_paths.py | 3 +++ .../youtube_plugin/kodion/items/menu_items.py | 20 +++++++++++++++++++ .../lib/youtube_plugin/youtube/provider.py | 12 +++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index 0164be43..a68532e7 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -33,6 +33,9 @@ PLAYLIST = '/playlist' SUBSCRIPTIONS = '/subscriptions' VIDEO = '/video' +SETTINGS = '/config/youtube' +SETUP_WIZARD = '/config/setup_wizard' + SPECIAL = '/special' DESCRIPTION_LINKS = SPECIAL + '/description_links' DISLIKED_VIDEOS = SPECIAL + '/disliked_videos' diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 4b8db436..0ff60014 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -901,3 +901,23 @@ def goto_page(context, params=None): params or context.get_params(), ), ) + + +def open_settings(context): + return ( + context.localize('settings'), + context_menu_uri( + context, + PATHS.SETTINGS, + ), + ) + + +def open_setup_wizard(context): + return ( + context.localize('setup_wizard'), + context_menu_uri( + context, + PATHS.SETUP_WIZARD, + ), + ) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index b786069d..123321c8 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1771,19 +1771,27 @@ class Provider(AbstractProvider): if settings_bool(settings.SHOW_SETUP_WIZARD, True): settings_menu_item = DirectoryItem( localize('setup_wizard'), - create_uri(('config', 'setup_wizard')), + create_uri(PATHS.SETUP_WIZARD), image='{media}/settings.png', action=True, ) + context_menu = [ + menu_items.open_settings(context) + ] + settings_menu_item.add_context_menu(context_menu) result.append(settings_menu_item) if settings_bool(settings.SHOW_SETTINGS): settings_menu_item = DirectoryItem( localize('settings'), - create_uri(('config', 'youtube')), + create_uri(PATHS.SETTINGS), image='{media}/settings.png', action=True, ) + context_menu = [ + menu_items.open_setup_wizard(context) + ] + settings_menu_item.add_context_menu(context_menu) result.append(settings_menu_item) return result, options From 03fa6755814c21205435ac6712b9ebcb3f2e2992 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:36:25 +0900 Subject: [PATCH 09/19] Fix typo in evaluating whether plugin container is loaded or active --- .../lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index fd279c21..8e8af769 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -323,8 +323,8 @@ class XbmcContextUI(AbstractContextUI): return { 'is_plugin': is_plugin, 'id': container_id, - 'is_loaded': is_active, - 'is_active': is_loaded, + 'is_active': is_active, + 'is_loaded': is_loaded, } @classmethod From 55da567a5413252704044a5276886c95610d510d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:59:38 +0900 Subject: [PATCH 10/19] Avoid unnecessary window navigation when opening More... context menu dialog --- .../youtube_plugin/youtube/helper/yt_video.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index c0c258e8..fa889bf8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -104,7 +104,7 @@ def _process_rate_video(provider, ) -def _process_more_for_video(context): +def _process_more_for_video(provider, context): params = context.get_params() video_id = params.get(VIDEO_ID) @@ -126,8 +126,22 @@ def _process_more_for_video(context): ] result = context.get_ui().on_select(context.localize('video.more'), items) - if result != -1: - context.execute(result) + if result == -1: + return ( + False, + { + provider.FALLBACK: False, + provider.FORCE_RETURN: True, + }, + ) + return ( + True, + { + provider.FALLBACK: result, + provider.FORCE_RETURN: True, + provider.POST_RUN: True, + }, + ) def process(provider, context, re_match=None, command=None, **kwargs): @@ -138,6 +152,6 @@ def process(provider, context, re_match=None, command=None, **kwargs): return _process_rate_video(provider, context, re_match, **kwargs) if command == 'more': - return _process_more_for_video(context) + return _process_more_for_video(provider, context) raise KodionException('Unknown video command: %s' % command) From 8ea2d1363f1b3bfd9c5b6578d9279cf019170299 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:35:41 +0900 Subject: [PATCH 11/19] Improve context menu runtime checks when used with widget containers --- resources/lib/youtube_plugin/kodion/constants/__init__.py | 2 ++ .../lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py | 2 +- .../lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index b2810694..e3cc93cd 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -77,6 +77,7 @@ FOLDER_URI = 'FolderPath' HAS_FILES = 'HasFiles' HAS_FOLDERS = 'HasFolders' HAS_PARENT = 'HasParent' +NUM_ALL_ITEMS = 'NumAllItems' SCROLLING = 'Scrolling' UPDATING = 'IsUpdating' @@ -243,6 +244,7 @@ __all__ = ( 'HAS_FILES', 'HAS_FOLDERS', 'HAS_PARENT', + 'NUM_ALL_ITEMS', 'SCROLLING', 'UPDATING', 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 5ff6b445..f8edca3b 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -441,7 +441,7 @@ class XbmcPlugin(AbstractPlugin): timeout = kwargs.get('timeout', 30) interval = kwargs.get('interval', 0.1) for action in actions: - while not ui.get_container(container_type=None, check_ready=True): + while not ui.get_container(container_type=False, check_ready=True): timeout -= interval if timeout < 0: logging.error('Container not ready' diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 8e8af769..f084d1f5 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -32,6 +32,7 @@ from ...constants import ( HIDE_PROGRESS, LISTITEM_INFO, LISTITEM_PROP, + NUM_ALL_ITEMS, PLUGIN_CONTAINER_INFO, PROPERTY, REFRESH_CONTAINER, @@ -298,6 +299,12 @@ class XbmcContextUI(AbstractContextUI): stacklevel=stacklevel, ) and ( + self.get_container_info( + NUM_ALL_ITEMS, + _container_id, + stacklevel=stacklevel, + ) + or self.get_container_bool( HAS_FOLDERS, _container_id, From f7f0dff842c79fc60a63b12421e5f4d63e530270 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:29:57 +0900 Subject: [PATCH 12/19] Workaround Kodi not executing Play action on listitems in non-video containers - Allows Play context menu item to be used in widgets --- .../lib/youtube_plugin/kodion/items/menu_items.py | 14 ++++++++++---- .../lib/youtube_plugin/youtube/helper/yt_play.py | 8 ++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 0ff60014..a9d9c797 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -46,12 +46,12 @@ URI_INFOLABEL = PROPERTY_AS_LABEL % URI VIDEO_ID_INFOLABEL = PROPERTY_AS_LABEL % VIDEO_ID -def context_menu_uri(context, path, params=None): +def context_menu_uri(context, path, params=None, run=True, play=False): if params is None: params = {CONTEXT_MENU: True} else: params[CONTEXT_MENU] = True - return context.create_uri(path, params, run=True) + return context.create_uri(path, params, run=run, play=play) def video_more_for(context, @@ -178,10 +178,16 @@ def folder_play(context, path, order='normal'): ) -def media_play(context): +def media_play(context, video_id=VIDEO_ID_INFOLABEL): return ( context.localize('video.play'), - 'Action(Play)' + context_menu_uri( + context, + (PATHS.PLAY,), + { + VIDEO_ID: video_id, + }, + ), ) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 9d15af7c..604cd130 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -22,6 +22,7 @@ from ...kodion.constants import ( BUSY_FLAG, CHANNEL_ID, CONTENT, + CONTEXT_MENU, FORCE_PLAY_PARAMS, INCOGNITO, ORDER, @@ -535,8 +536,11 @@ def process(provider, context, **_kwargs): if context.get_handle() == -1: # This is required to trigger Kodi resume prompt, along with using - # RunPlugin. Prompt will not be used if using PlayMedia - if force_play_params and not params.get(PLAY_STRM): + # RunPlugin. Prompt will not be used if using PlayMedia, however + # Action(Play) does not work in non-video windows + if ((force_play_params or params.get(CONTEXT_MENU)) + and not params.get(PLAY_STRM) + and context.is_plugin_folder()): return UriItem('command://Action(Play)') return UriItem('command://{0}'.format( From a30d3503c4bc8a45d1dd657da941dfd3b3c3a2e7 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:16:02 +0900 Subject: [PATCH 13/19] Pass additional headers to all player request and manifest urls by default --- .../youtube/client/player_client.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/player_client.py b/resources/lib/youtube_plugin/youtube/client/player_client.py index 6b7d3fde..c744448d 100644 --- a/resources/lib/youtube_plugin/youtube/client/player_client.py +++ b/resources/lib/youtube_plugin/youtube/client/player_client.py @@ -1128,12 +1128,14 @@ class YouTubePlayerClient(YouTubeDataClient): if itag in stream_list: break - url = response['mpd_manifest'] + headers = response['client']['headers'] + url = self._process_url_params( + response['mpd_manifest'], + headers=headers, + ) if not url: continue - headers = response['client']['headers'] - url_components = urlsplit(url) if url_components.query: params = dict(parse_qs(url_components.query)) @@ -1197,12 +1199,14 @@ class YouTubePlayerClient(YouTubeDataClient): itags = ('9995', '9996') if is_live else ('9993', '9994') for client_name, response in responses.items(): - url = response['hls_manifest'] + headers = response['client']['headers'] + url = self._process_url_params( + response['hls_manifest'], + headers=headers, + ) if not url: continue - headers = response['client']['headers'] - result = self.request( url, headers=headers, @@ -1318,11 +1322,10 @@ class YouTubePlayerClient(YouTubeDataClient): else: new_url = url - new_url = self._process_url_params(new_url, - mpd=False, - headers=headers, - referrer=None, - visitor_data=None) + new_url = self._process_url_params( + new_url, + headers=headers, + ) if not new_url: continue @@ -1416,11 +1419,11 @@ class YouTubePlayerClient(YouTubeDataClient): def _process_url_params(self, url, - mpd=True, + stream_proxy=False, headers=None, cpn=False, - referrer=False, - visitor_data=False, + referrer=None, + visitor_data=None, method='POST', digits_re=re_compile(r'\d+')): if not url: @@ -1473,7 +1476,7 @@ class YouTubePlayerClient(YouTubeDataClient): or 'https://www.youtube.com/watch?v=%s' % self.video_id, ) - if mpd: + if stream_proxy: new_params['__id'] = self.video_id new_params['__method'] = method new_params['__host'] = [parts.hostname] @@ -2406,6 +2409,7 @@ class YouTubePlayerClient(YouTubeDataClient): urls = self._process_url_params( unquote(url), + stream_proxy=True, headers=client['headers'], cpn=client.get('_cpn'), ) @@ -2851,9 +2855,8 @@ class YouTubePlayerClient(YouTubeDataClient): url = entity_escape(unquote(self._process_url_params( subtitle['url'], + stream_proxy=True, headers=headers, - referrer=None, - visitor_data=None, ))) if not url: continue From b67dc061b212568f568927d148860e5d00c1074b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:16:30 +0900 Subject: [PATCH 14/19] Combine modification of MPD manifest url with YouTubePlayerClient._process_url_params --- .../youtube/client/player_client.py | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/player_client.py b/resources/lib/youtube_plugin/youtube/client/player_client.py index c744448d..0881a418 100644 --- a/resources/lib/youtube_plugin/youtube/client/player_client.py +++ b/resources/lib/youtube_plugin/youtube/client/player_client.py @@ -1131,28 +1131,12 @@ class YouTubePlayerClient(YouTubeDataClient): headers = response['client']['headers'] url = self._process_url_params( response['mpd_manifest'], + mpd_manifest=True, headers=headers, ) if not url: continue - url_components = urlsplit(url) - if url_components.query: - params = dict(parse_qs(url_components.query)) - params['mpd_version'] = ['7'] - url = url_components._replace( - query=urlencode(params, doseq=True), - ).geturl() - else: - path = re_sub( - r'/mpd_version/\d+|/?$', - '/mpd_version/7', - url_components.path, - ) - url = url_components._replace( - path=path, - ).geturl() - stream_list[itag] = self._get_stream_format( itag=itag, title='', @@ -1420,6 +1404,7 @@ class YouTubePlayerClient(YouTubeDataClient): def _process_url_params(self, url, stream_proxy=False, + mpd_manifest=False, headers=None, cpn=False, referrer=None, @@ -1498,15 +1483,23 @@ class YouTubePlayerClient(YouTubeDataClient): if cpn is not False: new_params['cpn'] = cpn or self._generate_cpn() - params.update(new_params) - query_str = urlencode(params, doseq=True) - - return parts._replace( + parts = parts._replace( scheme='http', netloc=get_connect_address(self._context, as_netloc=True), path=PATHS.STREAM_PROXY, - query=query_str, - ).geturl() + ) + + elif mpd_manifest: + if 'mpd_version' in params: + new_params['mpd_version'] = ['7'] + else: + parts = parts._replace( + path=re_sub( + r'/mpd_version/\d+|/?$', + '/mpd_version/7', + parts.path, + ), + ) elif 'ratebypass' not in params and 'range' not in params: content_length = params.get('clen', [''])[0] @@ -1515,7 +1508,7 @@ class YouTubePlayerClient(YouTubeDataClient): if new_params: params.update(new_params) query_str = urlencode(params, doseq=True) - return parts._replace(query=query_str).geturl() + parts = parts._replace(query=query_str) return parts.geturl() From 9d0cd79e2bc2fa5ae6ab8c49214cf201f74853b8 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 9 Jan 2026 04:08:34 +0900 Subject: [PATCH 15/19] Allow for recursive copy and redaction of logged params --- .../lib/youtube_plugin/kodion/utils/redact.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/utils/redact.py b/resources/lib/youtube_plugin/kodion/utils/redact.py index 2b426cea..b5d3de15 100644 --- a/resources/lib/youtube_plugin/kodion/utils/redact.py +++ b/resources/lib/youtube_plugin/kodion/utils/redact.py @@ -51,12 +51,15 @@ def redact_params(params, _seq_types=(list, tuple)): log_params = params.copy() for param, value in params.items(): - if param in {'key', - 'api_key', - API_KEY, - 'api_secret', - API_SECRET, - 'client_secret'}: + if isinstance(value, dict): + log_value = redact_params(value) + elif param in {'key', + 'api_key', + API_KEY, + 'api_secret', + API_SECRET, + 'client_secret', + 'secret'}: log_value = ( ['...'.join((val[:3], val[-3:])) if len(val) > 9 else From bd3b4d9d14b1411ba121c82a53706d9129c22d0c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:23:25 +0900 Subject: [PATCH 16/19] Additional workarounds for JSON file operations failing due to addon service start delays #1362 - Initial data load bypasses file I/O IPC service - Do not save to file unless - valid data has previously been read from file - or existing data is missing/corrupt --- .../kodion/json_store/json_store.py | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 2e87dd75..36bad9a3 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -28,6 +28,7 @@ class JSONStore(object): _process_data = None def __init__(self, filename, context): + self._filename = filename if self.BASE_PATH: self.filepath = os.path.join(self.BASE_PATH, filename) else: @@ -43,34 +44,49 @@ class JSONStore(object): self.init() def init(self): - if self.load(stacklevel=4): - self._loaded = True - self.set_defaults() - else: - self.set_defaults(reset=True) - return self._loaded + loaded = self.load(stacklevel=4, ipc=False) + self.set_defaults(reset=(not loaded)) + return loaded def set_defaults(self, reset=False): raise NotImplementedError def save(self, data, update=False, process=True, ipc=True, stacklevel=2): + loaded = self._loaded filepath = self.filepath - if not filepath: - return False + try: + if not filepath: + raise IOError - if update: - data = merge_dicts(self._data, data) - if data == self._data: - self.log.debug(('Data unchanged', 'File: %s'), + self.log.debug(('Saving', 'File: %s'), filepath, stacklevel=stacklevel) - return None - self.log.debug(('Saving', 'File: %s'), - filepath, - stacklevel=stacklevel) - try: + + _data = self._data + if loaded is False: + loaded = self.load(stacklevel=4) + if loaded: + self.log.warning(('File state out of sync - data discarded', + 'File: {file}', + 'Old data: {old_data!p}', + 'New data: {new_data!p}'), + file=filepath, + old_data=_data, + new_data=data, + stacklevel=stacklevel) + return None + + if update and _data: + data = merge_dicts(_data, data) if not data: raise ValueError + + if data == _data: + self.log.debug(('Data unchanged', 'File: %s'), + filepath, + stacklevel=stacklevel) + return None + _data = json.dumps( data, ensure_ascii=False, indent=4, sort_keys=True ) @@ -79,6 +95,12 @@ class JSONStore(object): object_pairs_hook=(self._process_data if process else None), ) + if loaded is False: + self.log.debug(('File write deferred', 'File: %s'), + filepath, + stacklevel=stacklevel) + return None + if ipc: self._context.get_ui().set_property( '-'.join((FILE_WRITE, filepath)), @@ -103,7 +125,7 @@ class JSONStore(object): file.write(to_unicode(_data)) except (RuntimeError, IOError, OSError): self.log.exception(('Access error', 'File: %s'), - filepath, + filepath or self._filename, stacklevel=stacklevel) return False except (TypeError, ValueError): @@ -115,14 +137,17 @@ class JSONStore(object): return True def load(self, process=True, ipc=True, stacklevel=2): + loaded = False filepath = self.filepath - if not filepath: - return False - - self.log.debug(('Loading', 'File: %s'), - filepath, - stacklevel=stacklevel) + data = '' try: + if not filepath: + raise IOError + + self.log.debug(('Loading', 'File: %s'), + filepath, + stacklevel=stacklevel) + if ipc: if self._context.ipc_exec( FILE_READ, @@ -145,17 +170,19 @@ class JSONStore(object): data, object_pairs_hook=(self._process_data if process else None), ) + loaded = True except (RuntimeError, IOError, OSError): self.log.exception(('Access error', 'File: %s'), - filepath, + filepath or self._filename, stacklevel=stacklevel) - return False except (TypeError, ValueError): self.log.exception(('Invalid data', 'Data: {data!r}'), data=data, stacklevel=stacklevel) - return False - return True + loaded = None + + self._loaded = loaded + return loaded def get_data(self, process=True, fallback=True, stacklevel=2): if not self._loaded: From b15a3b36287ec3621efb72b2f866775e9ef180ba Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 9 Jan 2026 05:03:42 +0900 Subject: [PATCH 17/19] Avoid creating PlayerMonitorThread as a subclass of threading.Thread - Python v3.14 adds the context parameter to threading.Thread - This conflicts with existing context parameter and instance variables of PlayerMonitorThread --- .../kodion/monitors/player_monitor.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py index 53b6bcc0..42db94b3 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py @@ -29,7 +29,7 @@ from ..constants import ( from ..utils.redact import redact_params -class PlayerMonitorThread(threading.Thread): +class PlayerMonitorThread(object): def __init__(self, player, provider, context, monitor, player_data): self.player_data = player_data video_id = player_data.get(VIDEO_ID) @@ -53,11 +53,13 @@ class PlayerMonitorThread(threading.Thread): class_name=self.__class__.__name__, video_id=video_id, ) + self.name = name self.log = logging.getLogger(name) - super(PlayerMonitorThread, self).__init__(name=name) - self.daemon = True - self.start() + thread = threading.Thread(name=name, target=self.run, args=(self,)) + self._thread = thread + thread.daemon = True + thread.start() def abort_now(self): return (not self._player.isPlaying() @@ -334,6 +336,9 @@ class PlayerMonitorThread(threading.Thread): def ended(self): return self._ended.is_set() + def join(self, timeout=None): + return self._thread.join(timeout) + class PlayerMonitor(xbmc.Player): log = logging.getLogger(__name__) From 86547c900717e87a12f0c8139ca74cdc0aef3f65 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:00:52 +0900 Subject: [PATCH 18/19] Update and trigger Setup Wizard to set default value for My Subscription sources --- .../kodion/settings/abstract_settings.py | 35 ++++++++++++------- .../youtube/helper/yt_setup_wizard.py | 6 ++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 8add3b9e..7d8b48da 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -110,8 +110,9 @@ class AbstractSettings(object): def setup_wizard_enabled(self, value=None): # Set run_required to release date (as Unix timestamp in seconds) # to enable oneshot on first run - # Tuesday, 8 April 2025 12:00:00 AM = 1744070400 - run_required = 1744070400 + # 2026/01/10 @ 12:00 AM + # datetime(2026,01,10,0,0).timestamp() = 1767970800 + run_required = 1767970800 if value is False: self.set_int(SETTINGS.SETUP_WIZARD_RUNS, run_required) @@ -580,10 +581,13 @@ class AbstractSettings(object): return qualities[0]['nom_height'] return self.fixed_video_quality() - def stream_features(self, value=None): + def stream_features(self, value=None, raw_values=False): if value is not None: return self.set_string_list(SETTINGS.MPD_STREAM_FEATURES, value) - return frozenset(self.get_string_list(SETTINGS.MPD_STREAM_FEATURES)) + stream_features = self.get_string_list(SETTINGS.MPD_STREAM_FEATURES) + if raw_values: + return stream_features + return frozenset(stream_features) _STREAM_SELECT = { 1: 'auto', @@ -685,17 +689,24 @@ class AbstractSettings(object): default=('subscriptions', 'saved_playlists', 'bookmark_channels', - 'bookmark_playlists')): + 'bookmark_playlists'), + match_values=True, + raw_values=False): if value is not None: return self.set_string_list(SETTINGS.MY_SUBSCRIPTIONS_SOURCES, value) - sources = frozenset( - self.get_string_list(SETTINGS.MY_SUBSCRIPTIONS_SOURCES) or default - ) - return tuple([ - source in sources - for source in default - ]) + sources = self.get_string_list(SETTINGS.MY_SUBSCRIPTIONS_SOURCES) + if default: + if not sources: + sources = default + if match_values and not raw_values: + return tuple([ + source in sources + for source in default + ]) + if raw_values: + return sources + return frozenset(sources) def subscriptions_filter_enabled(self, value=None): if value is not None: 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 b1eb592a..82a7c3a1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -23,6 +23,12 @@ from ...kodion.utils.datetime import since_epoch, strptime def process_pre_run(context): context.get_function_cache().clear() + settings = context.get_settings() + if not settings.subscriptions_sources(default=False, raw_values=True): + settings.subscriptions_sources( + settings.subscriptions_sources(raw_values=True) + ) + def process_language(context, step, steps, **_kwargs): localize = context.localize From 48c82afa29241589f227c9ec6bbe4f1132c1875c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:01:24 +0900 Subject: [PATCH 19/19] Version bump v7.4.0+beta.3 --- addon.xml | 2 +- changelog.txt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 8161fce3..fdc74a4c 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 7768c820..7dbbaf08 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,22 @@ +## v7.4.0+beta.3 +### Fixed +- Avoid creating PlayerMonitorThread as a subclass of threading.Thread +- Additional workarounds for JSON file operations failing due to addon service start delays #1362 +- Avoid unnecessary window navigation when opening More... context menu dialog +- Fix typo in evaluating whether plugin container is loaded or active +- Fix possible exception when using default fallback for failed search + +### Changed +- Update and trigger Setup Wizard to set default value for My Subscription sources +- Pass additional headers to all player request and manifest urls by default +- Workaround Kodi not executing Play action on listitems in non-video containers +- Improve context menu runtime checks when used with widget containers + +### New +- Add context menu items to open Settings/Setup Wizard from Setup Wizard/Settings main menu entries +- Log summary of stream proxy request and response details #1363 +- Log stream proxy request range #1363 + ## v7.4.0+beta.2 ### Fixed - Fix retrieving items from local history database #1356