From efb1997862ab5ab38d89b727712d5b44258b934c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 4 Oct 2024 05:19:10 +1000 Subject: [PATCH 01/12] Standardise logging for script, plugin and service endpoints --- .../youtube_plugin/kodion/plugin_runner.py | 22 ++-- .../youtube_plugin/kodion/script_actions.py | 14 +++ .../youtube_plugin/kodion/service_runner.py | 10 +- .../kodion/utils/system_version.py | 102 +++++++++--------- 4 files changed, 84 insertions(+), 64 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index e5fa69fcf..8dcee5dd7 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -10,8 +10,6 @@ from __future__ import absolute_import, division, unicode_literals -from platform import python_version - from .context import XbmcContext from .plugin import XbmcPlugin from ..youtube import Provider @@ -24,6 +22,7 @@ _provider = Provider() _profiler = _context.get_infobool('System.GetBool(debug.showloginfo)') +_profiler = True if _profiler: from .debug import Profiler @@ -37,8 +36,6 @@ def run(context=_context, if profiler: profiler.enable(flush=True) - context.log_debug('Starting Kodion framework by bromix...') - current_uri = context.get_uri() context.init() new_uri = context.get_uri() @@ -48,14 +45,15 @@ def run(context=_context, if key in params: params[key] = '' - context.log_notice('Running: {plugin} ({version})' - ' on {kodi} with Python {python}\n' - 'Path: {path}\n' - 'Params: {params}' - .format(plugin=context.get_name(), - version=context.get_version(), - kodi=context.get_system_version(), - python=python_version(), + system_version = context.get_system_version() + context.log_notice('Plugin: Running |v{version}|\n' + 'Kodi: |v{kodi}|\n' + 'Python: |v{python}|\n' + 'Path: |{path}|\n' + 'Params: |{params}|' + .format(version=context.get_version(), + kodi=str(system_version), + python=system_version.get_python_version(), path=context.get_path(), params=params)) diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index cd4bffd34..3fc589c02 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -646,6 +646,20 @@ def run(argv): if params: params = dict(parse_qsl(args.query)) + system_version = context.get_system_version() + context.log_notice('Script: Running |v{version}|\n' + 'Kodi: |v{kodi}|\n' + 'Python: |v{python}|\n' + 'Category: |{category}|\n' + 'Action: |{action}|\n' + 'Params: |{params}|' + .format(version=context.get_version(), + kodi=str(system_version), + python=system_version.get_python_version(), + category=category, + action=action, + params=params)) + if not category: xbmcaddon.Addon().openSettings() return diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index 89fc41fff..d4d35cfb9 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -28,10 +28,16 @@ def run(): context = XbmcContext() - context.log_debug('YouTube service initialization...') - provider = Provider() + system_version = context.get_system_version() + context.log_notice('Service: Starting |v{version}|\n' + 'Kodi: |v{kodi}|\n' + 'Python: |v{python}|' + .format(version=context.get_version(), + kodi=str(system_version), + python=system_version.get_python_version())) + get_listitem_info = context.get_listitem_info get_listitem_property = context.get_listitem_property diff --git a/resources/lib/youtube_plugin/kodion/utils/system_version.py b/resources/lib/youtube_plugin/kodion/utils/system_version.py index b306abac1..7711fe580 100644 --- a/resources/lib/youtube_plugin/kodion/utils/system_version.py +++ b/resources/lib/youtube_plugin/kodion/utils/system_version.py @@ -10,70 +10,69 @@ from __future__ import absolute_import, division, unicode_literals +from platform import python_version + from .methods import jsonrpc from ..compatibility import string_type class SystemVersion(object): - def __init__(self, version=None, releasename=None, appname=None): - self._version = ( - version if version and isinstance(version, tuple) - else (0, 0, 0, 0) - ) + RELEASE_MAP = { + (22, 0): 'Piers', + (21, 0): 'Omega', + (20, 0): 'Nexus', + (19, 0): 'Matrix', + (18, 0): 'Leia', + (17, 0): 'Krypton', + (16, 0): 'Jarvis', + (15, 0): 'Isengard', + (14, 0): 'Helix', + (13, 0): 'Gotham', + (12, 0): 'Frodo', + } - self._releasename = ( - releasename if releasename and isinstance(releasename, string_type) - else 'UNKNOWN' - ) - - self._appname = ( - appname if appname and isinstance(appname, string_type) - else 'UNKNOWN' - ) + def __init__(self, version=None, releasename=None, appname=None): + if isinstance(version, tuple): + self._version = version + else: + version = None - try: - response = jsonrpc(method='Application.GetProperties', - params={'properties': ['version', 'name']}) - version_installed = response['result']['version'] - self._version = (version_installed.get('major', 1), - version_installed.get('minor', 0)) - self._appname = response['result']['name'] - except (KeyError, TypeError): - self._version = (1, 0) # Frodo - self._appname = 'Unknown Application' - - if self._version >= (22, 0): - self._releasename = 'Piers' - elif self._version >= (21, 0): - self._releasename = 'Omega' - elif self._version >= (20, 0): - self._releasename = 'Nexus' - elif self._version >= (19, 0): - self._releasename = 'Matrix' - elif self._version >= (18, 0): - self._releasename = 'Leia' - elif self._version >= (17, 0): - self._releasename = 'Krypton' - elif self._version >= (16, 0): - self._releasename = 'Jarvis' - elif self._version >= (15, 0): - self._releasename = 'Isengard' - elif self._version >= (14, 0): - self._releasename = 'Helix' - elif self._version >= (13, 0): - self._releasename = 'Gotham' - elif self._version >= (12, 0): - self._releasename = 'Frodo' + if appname and isinstance(appname, string_type): + self._appname = appname + else: + appname = None + + if version is None or appname is None: + try: + result = jsonrpc( + method='Application.GetProperties', + params={'properties': ['version', 'name']}, + )['result'] or {} + except (KeyError, TypeError): + result = {} + + if version is None: + version = result.get('version') or {} + self._version = (version.get('major', 1), + version.get('minor', 0)) + + if appname is None: + self._appname = result.get('name', 'Unknown application') + + if releasename and isinstance(releasename, string_type): + self._releasename = releasename else: - self._releasename = 'Unknown Release' + version = (self._version[0], self._version[1]) + self._releasename = self.RELEASE_MAP.get(version, 'Unknown release') + + self._python_version = python_version() def __str__(self): - obj_str = '{releasename} ({appname}-{version[0]}.{version[1]})'.format( + return '{version[0]}.{version[1]} ({appname} {releasename})'.format( releasename=self._releasename, appname=self._appname, version=self._version ) - return obj_str def get_release_name(self): return self._releasename @@ -84,6 +83,9 @@ def get_version(self): def get_app_name(self): return self._appname + def get_python_version(self): + return self._python_version + def compatible(self, *version): return self._version >= version From 818b4a10a1ba83c1137efbb724d61537f8f634de Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 4 Oct 2024 06:10:32 +1000 Subject: [PATCH 02/12] Improve API request error logging --- resources/lib/youtube_plugin/kodion/network/requests.py | 4 ++-- resources/lib/youtube_plugin/youtube/client/youtube.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 886bbffb4..c30e43ba6 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -145,7 +145,7 @@ def request(self, url, method='GET', error_details.update(_detail) if _response is not None: response = _response - response_text = str(_response) + response_text = repr(_response) if _trace is not None: stack_trace = _trace if _exc is not None: @@ -168,7 +168,7 @@ def request(self, url, method='GET', error_info = str(exc) if response_text: - response_text = 'Request response:\n{0}'.format(response_text) + response_text = 'Response:\n\t|{0}|'.format(response_text) if stack_trace: stack_trace = ( diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index d7c2d2a5e..6504f778f 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1968,14 +1968,16 @@ def _error_hook(self, **kwargs): if getattr(exc, 'pass_data', False): data = json_data else: - data = None + data = kwargs['response'] if getattr(exc, 'raise_exc', False): exception = YouTubeException else: exception = None if not json_data or 'error' not in json_data: - return None, None, None, data, None, exception + info = 'Exception:\n\t|{exc!r}|' + details = kwargs + return None, info, details, data, None, exception details = json_data['error'] reason = details.get('errors', [{}])[0].get('reason', 'Unknown') @@ -2005,7 +2007,7 @@ def _error_hook(self, **kwargs): time_ms=timeout) info = ('API error: {reason}\n' - 'exc: |{exc}|\n' + 'exc: |{exc!r}|\n' 'message: |{message}|') details = {'reason': reason, 'message': message} return '', info, details, data, False, exception From 279f270d37974045c00efb9f94a3bbcc1167b6fa Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 4 Oct 2024 06:25:37 +1000 Subject: [PATCH 03/12] Simplify server start when playing video --- .../lib/youtube_plugin/kodion/constants/__init__.py | 2 -- .../youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py | 2 ++ .../lib/youtube_plugin/kodion/service_runner.py | 12 +++--------- .../lib/youtube_plugin/youtube/helper/yt_play.py | 6 ------ 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index ba16a54bb..b0431d4fe 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -59,7 +59,6 @@ PLUGIN_WAKEUP = 'plugin_wakeup' PLUGIN_SLEEPING = 'plugin_sleeping' SERVER_WAKEUP = 'server_wakeup' -SERVER_POST_START = 'server_post_start' WAKEUP = 'wakeup' # Play options @@ -118,7 +117,6 @@ # Sleep/wakeup states 'PLUGIN_SLEEPING', 'PLUGIN_WAKEUP', - 'SERVER_POST_START', 'SERVER_WAKEUP', 'WAKEUP', 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 971b2d814..7e54f32d3 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -28,6 +28,7 @@ REFRESH_CONTAINER, RELOAD_ACCESS_MANAGER, REROUTE_PATH, + SERVER_WAKEUP, VIDEO_ID, ) from ...exceptions import KodionException @@ -227,6 +228,7 @@ def run(self, provider, context, focused=None): playlist_player = context.get_playlist_player() playlist_player.play_item(item=uri, listitem=item) else: + context.wakeup(SERVER_WAKEUP, timeout=5) xbmcplugin.setResolvedUrl(self.handle, succeeded=result, listitem=item) diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index d4d35cfb9..da8403d3d 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -13,7 +13,6 @@ from .constants import ( ABORT_FLAG, PLUGIN_SLEEPING, - SERVER_POST_START, TEMP_PATH, VIDEO_ID, ) @@ -43,7 +42,6 @@ def run(): ui = context.get_ui() clear_property = ui.clear_property - pop_property = ui.pop_property set_property = ui.set_property clear_property(ABORT_FLAG) @@ -95,13 +93,9 @@ def run(): if httpd_idle_time_ms >= httpd_idle_timeout_ms: httpd_idle_time_ms = 0 monitor.shutdown_httpd(sleep=True) - else: - if monitor.httpd_sleep_allowed is None: - if pop_property(SERVER_POST_START): - monitor.httpd_sleep_allowed = True - httpd_idle_time_ms = 0 - else: - pop_property(SERVER_POST_START) + elif monitor.httpd_sleep_allowed is None: + monitor.httpd_sleep_allowed = True + httpd_idle_time_ms = 0 else: if httpd_idle_time_ms >= httpd_ping_period_ms: httpd_idle_time_ms = 0 diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index fef2d338d..31888ba5d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -29,8 +29,6 @@ PLAY_PROMPT_SUBTITLES, PLAY_TIMESHIFT, PLAY_WITH, - SERVER_POST_START, - SERVER_WAKEUP, ) from ...kodion.items import AudioItem, VideoItem from ...kodion.network import get_connect_address @@ -356,11 +354,7 @@ def process(provider, context, **_kwargs): position, _ = playlist_player.get_position() items = playlist_player.get_items() - ui.clear_property(SERVER_POST_START) - context.wakeup(SERVER_WAKEUP, timeout=5) media_item = _play_stream(provider, context) - ui.set_property(SERVER_POST_START) - if media_item: if position and items: ui.set_property(PLAYLIST_PATH, From dafd0c8af679eaa59a5c973e6b7276365e2420d0 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 8 Oct 2024 07:12:04 +1100 Subject: [PATCH 04/12] Misc tidy ups --- .../kodion/context/xbmc/xbmc_context.py | 2 +- .../lib/youtube_plugin/kodion/items/menu_items.py | 4 ++-- .../youtube_plugin/kodion/items/xbmc/xbmc_items.py | 6 +++--- .../youtube_plugin/kodion/network/http_server.py | 14 ++++++++++---- .../lib/youtube_plugin/kodion/script_actions.py | 4 ++-- .../kodion/settings/abstract_settings.py | 2 +- .../kodion/settings/xbmc/xbmc_plugin_settings.py | 8 ++++---- .../youtube_plugin/youtube/client/login_client.py | 2 +- .../lib/youtube_plugin/youtube/client/youtube.py | 5 ++--- .../youtube_plugin/youtube/helper/stream_info.py | 10 +++++----- .../youtube/helper/yt_setup_wizard.py | 4 ++-- 11 files changed, 33 insertions(+), 28 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 d370f1968..45232bbef 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -550,7 +550,7 @@ def apply_content(self): ) def add_sort_method(self, *sort_methods): - args = slice(None if current_system_version.compatible(19, 0) else 2) + args = slice(None if current_system_version.compatible(19) else 2) for sort_method in sort_methods: xbmcplugin.addSortMethod(self._plugin_handle, *sort_method[args]) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 5ce366f99..0654b81d1 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -533,7 +533,7 @@ def bookmark_add_channel(context, channel_id, channel_name=''): return ( (context.localize('bookmark.channel') % ( context.get_ui().bold(channel_name) if channel_name else - context.localize(19029) + context.localize(19029) # "Channel" )), context.create_uri( (PATHS.BOOKMARKS, 'add',), @@ -614,7 +614,7 @@ def separator(): def goto_home(context): return ( - context.localize(10000), + context.localize(10000), # "Home" context.create_uri( (PATHS.ROUTE, PATHS.HOME,), { 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 2c9c6aa26..cc1087f5f 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -28,7 +28,7 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): - if not current_system_version.compatible(20, 0): + if not current_system_version.compatible(20): if isinstance(item, VideoItem): info_labels = {} info_type = 'video' @@ -446,12 +446,12 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs): props['inputstream.adaptive.stream_selection_type'] = 'adaptive' props['inputstream.adaptive.chooser_resolution_max'] = 'auto' - if current_system_version.compatible(19, 0): + if current_system_version.compatible(19): props['inputstream'] = 'inputstream.adaptive' else: props['inputstreamaddon'] = 'inputstream.adaptive' - if not current_system_version.compatible(21, 0): + if not current_system_version.compatible(21): props['inputstream.adaptive.manifest_type'] = manifest_type if media_item.live: diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index b2ecba8bb..cece36b7a 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -26,9 +26,15 @@ xbmcgui, xbmcvfs, ) -from ..constants import ADDON_ID, LICENSE_TOKEN, LICENSE_URL, PATHS, TEMP_PATH +from ..constants import ( + ADDON_ID, + LICENSE_TOKEN, + LICENSE_URL, + PATHS, + TEMP_PATH, +) from ..logger import log_debug, log_error -from ..utils import validate_ip_address, redact_ip, wait +from ..utils import redact_ip, validate_ip_address, wait class HTTPServer(TCPServer): @@ -629,9 +635,9 @@ def get_connect_address(context, as_netloc=False): sock = None try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - if hasattr(socket, "SO_REUSEADDR"): + if hasattr(socket, 'SO_REUSEADDR'): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if hasattr(socket, "SO_REUSEPORT"): + if hasattr(socket, 'SO_REUSEPORT'): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except socket.error: address = xbmc.getIPAddress() diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index 3fc589c02..403f79b92 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -21,7 +21,7 @@ ) from .context import XbmcContext from .network import Locator, get_client_ip_address, httpd_status -from .utils import current_system_version, rm_dir, validate_ip_address +from .utils import rm_dir, validate_ip_address from ..youtube import Provider @@ -426,7 +426,7 @@ def _maintenance_actions(context, action, params): if target == 'settings_xml' and ui.on_yes_no_input( context.get_name(), localize('refresh.settings.confirm') ): - if not current_system_version.compatible(20, 0): + if not context.get_system_version().compatible(20): ui.show_notification(localize('failed')) return diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index f644a4769..9690b1da0 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -628,7 +628,7 @@ def get_history_playlist(self): def set_history_playlist(self, value): return self.set_string(SETTINGS.HISTORY_PLAYLIST, value) - if current_system_version.compatible(20, 0): + if current_system_version.compatible(20): def get_label_color(self, label_part): setting_name = '.'.join((SETTINGS.LABEL_COLOR, label_part)) return self.get_string(setting_name, 'white') 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 ba22a9731..d8e82c413 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -24,7 +24,7 @@ class SettingsProxy(object): def __init__(self, instance): self.ref = instance - if current_system_version.compatible(21, 0): + if current_system_version.compatible(21): def get_bool(self, *args, **kwargs): return self.ref.getBool(*args, **kwargs) @@ -75,7 +75,7 @@ def set_str_list(self, setting, value): value = ','.join(value) return self.ref.setSetting(setting, value) - if not current_system_version.compatible(19, 0): + if not current_system_version.compatible(19): @property def ref(self): if self._ref: @@ -121,7 +121,7 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): self._echo = get_kodi_setting_bool('debug.showloginfo') self._cache = {} - if current_system_version.compatible(21, 0): + if current_system_version.compatible(21): self._proxy = SettingsProxy(xbmc_addon.getSettings()) # set methods in new Settings class are documented as returning a # bool, True if value was set, False otherwise, similar to how the @@ -130,7 +130,7 @@ def flush(self, xbmc_addon=None, fill=False, flush_all=True): # Ignore return value until bug is fixed in Kodi self._check_set = False else: - if fill and not current_system_version.compatible(19, 0): + if fill and not current_system_version.compatible(19): self.__class__._instances.add(xbmc_addon) self._proxy = SettingsProxy(xbmc_addon) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 0c63f17b3..b69b33435 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -248,7 +248,7 @@ def authenticate(self, username, password): 'User-Agent': 'GoogleAuth/1.4 (GT-I9100 KTU84Q)', 'content-type': 'application/x-www-form-urlencoded', 'Host': 'android.clients.google.com', - 'Connection': 'Keep-Alive', + 'Connection': 'keep-alive', 'Accept-Encoding': 'gzip'} post_data = { diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 6504f778f..793587314 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -23,7 +23,6 @@ from ...kodion.compatibility import cpu_count, string_type, to_str from ...kodion.items import DirectoryItem from ...kodion.utils import ( - current_system_version, datetime_parser, strip_html_from_text, to_unicode, @@ -1551,7 +1550,7 @@ def _get_feed(output, channel_id, _headers=headers): } def _parse_feeds(feeds, - encode=not current_system_version.compatible(19, 0), + utf8=self._context.get_system_version().compatible(19), filters=subscription_filters, _ns=namespaces, _cache=cache): @@ -1567,7 +1566,7 @@ def _parse_feeds(feeds, content.encoding = 'utf-8' content = to_unicode(content.content).replace('\n', '') - root = ET.fromstring(to_str(content) if encode else content) + root = ET.fromstring(content if utf8 else to_str(content)) channel_name = (root.findtext('atom:title', '', _ns) .lower().replace(',', '')) feed_items = [{ diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index d07bc20d5..880de217a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -980,9 +980,7 @@ def _make_curl_headers(headers, cookies=None): '='.join((cookie.name, cookie.value)) for cookie in cookies ]) # Headers used in xbmc_items.video_playback_item' - return '&'.join([ - '='.join((key, quote(value))) for key, value in headers.items() - ]) + return urlencode(headers, safe='/', quote_via=quote) @staticmethod def _normalize_url(url): @@ -2201,8 +2199,10 @@ def _filter_group(previous_group, previous_stream, item): )) if license_url: - license_url = (license_url.replace("&", "&") - .replace('"', """).replace("<", "<") + license_url = (license_url + .replace("&", "&") + .replace('"', """) + .replace("<", "<") .replace(">", ">")) output.extend(( '\t\t\t Date: Wed, 9 Oct 2024 16:44:42 +1100 Subject: [PATCH 05/12] Explicitly enable TCP keep alive #913 --- .../youtube_plugin/kodion/network/requests.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index c30e43ba6..deb19940a 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals import atexit +import socket from traceback import format_stack from requests import Session @@ -28,6 +29,20 @@ class SSLHTTPAdapter(HTTPAdapter): + _SOCKET_OPTIONS = ( + (socket.SOL_SOCKET, getattr(socket, 'SO_KEEPALIVE', None), 1), + (socket.IPPROTO_TCP, getattr(socket, 'TCP_NODELAY', None), 1), + (socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPIDLE', None), 300), + # TCP_KEEPALIVE equivalent to TCP_KEEPIDLE on iOS/macOS + (socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPALIVE', None), 300), + # TCP_KEEPINTVL may not be implemented at app level on iOS/macOS + (socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPINTVL', None), 60), + # TCP_KEEPCNT may not be implemented at app level on iOS/macOS + (socket.IPPROTO_TCP, getattr(socket, 'TCP_KEEPCNT', None), 5), + # TCP_USER_TIMEOUT = TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT + (socket.IPPROTO_TCP, getattr(socket, 'TCP_USER_TIMEOUT', None), 600), + ) + _ssl_context = create_urllib3_context() _ssl_context.load_verify_locations( capath=extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH) @@ -35,6 +50,12 @@ class SSLHTTPAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): kwargs['ssl_context'] = self._ssl_context + + kwargs['socket_options'] = [ + socket_option for socket_option in self._SOCKET_OPTIONS + if socket_option[1] is not None + ] + return super(SSLHTTPAdapter, self).init_poolmanager(*args, **kwargs) def cert_verify(self, conn, url, verify, cert): From 74c7d043371066d785341ac6b14e42445fb1d6ba Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:45:27 +1100 Subject: [PATCH 06/12] Fix http server not listening on any interface if listen IP is 0.0.0.0 #927 --- .../kodion/constants/const_paths.py | 2 +- .../kodion/monitors/service_monitor.py | 2 +- .../kodion/network/http_server.py | 59 ++++++++++--------- .../youtube/helper/stream_info.py | 27 ++++----- 4 files changed, 46 insertions(+), 44 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index e0003afb4..ff8d35917 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -32,6 +32,6 @@ API_SUBMIT = '/youtube/api/submit' DRM = '/youtube/widevine' IP = '/youtube/client_ip' -MPD = '/youtube/manifest/dash/' +MPD = '/youtube/manifest/dash' PING = '/youtube/ping' REDIRECT = '/youtube/redirect' diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index ecc2b6a84..fea592596 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -204,7 +204,7 @@ def start_httpd(self): self.httpd_thread.start() address = self.httpd.socket.getsockname() - log_debug('HTTPServer: Serving on |{ip}:{port}|' + log_debug('HTTPServer: Listening on |{ip}:{port}|' .format(ip=address[0], port=address[1])) def shutdown_httpd(self, sleep=False): diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index cece36b7a..047d661d7 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -21,7 +21,9 @@ BaseHTTPRequestHandler, TCPServer, parse_qs, + parse_qsl, urlsplit, + urlunsplit, xbmc, xbmcgui, xbmcvfs, @@ -118,15 +120,22 @@ def do_GET(self): self.wfile.write(client_json.encode('utf-8')) elif stripped_path.startswith(PATHS.MPD): - filepath = os.path.join(self.BASE_PATH, self.path[len(PATHS.MPD):]) - file_chunk = True try: + file = dict(parse_qsl(urlsplit(self.path).query)).get('file') + if file: + filepath = os.path.join(self.BASE_PATH, file) + else: + filepath = None + raise IOError + with open(filepath, 'rb') as f: self.send_response(200) self.send_header('Content-Type', 'application/dash+xml') self.send_header('Content-Length', str(os.path.getsize(filepath))) self.end_headers() + + file_chunk = True while file_chunk: file_chunk = f.read(self.chunk_size) if file_chunk: @@ -583,13 +592,13 @@ def get_http_server(address, port, context): def httpd_status(context): - address, port = get_connect_address(context) - url = ''.join(( - 'http://', - address, - ':', - str(port), + netloc = get_connect_address(context, as_netloc=True) + url = urlunsplit(( + 'http', + netloc, PATHS.PING, + '', + '', )) if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=context) @@ -598,22 +607,20 @@ def httpd_status(context): if result == 204: return True - log_debug('HTTPServer: Ping |{address}:{port}| - |{response}|' - .format(address=address, - port=port, + log_debug('HTTPServer: Ping |{netloc}| - |{response}|' + .format(netloc=netloc, response=result or 'failed')) return False def get_client_ip_address(context): ip_address = None - address, port = get_connect_address(context) - url = ''.join(( - 'http://', - address, - ':', - str(port), + url = urlunsplit(( + 'http', + get_connect_address(context, as_netloc=True), PATHS.IP, + '', + '', )) if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=context) @@ -627,10 +634,8 @@ def get_client_ip_address(context): def get_connect_address(context, as_netloc=False): settings = context.get_settings() - address = settings.httpd_listen() - port = settings.httpd_port() - if address == '0.0.0.0': - address = '127.0.0.1' + listen_address = settings.httpd_listen() + listen_port = settings.httpd_port() sock = None try: @@ -640,18 +645,18 @@ def get_connect_address(context, as_netloc=False): if hasattr(socket, 'SO_REUSEPORT'): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except socket.error: - address = xbmc.getIPAddress() + listen_address = xbmc.getIPAddress() if sock: sock.settimeout(0) try: - sock.connect((address, 0)) - address = sock.getsockname()[0] + sock.connect((listen_address, 0)) + connect_address = sock.getsockname()[0] except socket.error: - address = xbmc.getIPAddress() + connect_address = xbmc.getIPAddress() finally: sock.close() if as_netloc: - return ':'.join((address, str(port))) - return address, port + return ':'.join((connect_address, str(listen_port))) + return listen_address, listen_port diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 880de217a..71e25b5ad 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -30,6 +30,7 @@ urlencode, urljoin, urlsplit, + urlunsplit, xbmcvfs, ) from ...kodion.constants import PATHS, TEMP_PATH @@ -1587,17 +1588,15 @@ def load_stream_info(self, video_id): continue self._context.log_debug('Found widevine license url: {0}' .format(url)) - address, port = get_connect_address(self._context) license_info = { 'url': url, - 'proxy': ''.join(( - 'http://', - address, - ':', - str(port), + 'proxy': urlunsplit(( + 'http', + get_connect_address(self._context, as_netloc=True), PATHS.DRM, - '||R{{SSM}}|', - )), + '', + '', + )) + '||R{{SSM}}|R', 'token': self._access_token, } break @@ -2343,13 +2342,11 @@ def _filter_group(previous_group, previous_stream, item): .format(file=filepath)) success = False if success: - address, port = get_connect_address(self._context) - return ''.join(( - 'http://', - address, - ':', - str(port), + return urlunsplit(( + 'http', + get_connect_address(self._context, as_netloc=True), PATHS.MPD, - filename, + urlencode({'file': filename}), + '', )), main_stream return None, None From e909be78457a17afcb4f074ec9a19e7a6940fa75 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 9 Oct 2024 04:13:44 +1100 Subject: [PATCH 07/12] Simplify handling of query parameters in http server requests --- .../youtube_plugin/kodion/network/http_server.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 047d661d7..276a423c3 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -20,7 +20,6 @@ from ..compatibility import ( BaseHTTPRequestHandler, TCPServer, - parse_qs, parse_qsl, urlsplit, urlunsplit, @@ -161,12 +160,12 @@ def do_GET(self): xbmc.executebuiltin('Dialog.Close(addonsettings,true)') query = urlsplit(self.path).query - params = parse_qs(query) + params = dict(parse_qsl(query)) updated = [] - api_key = params.get('api_key', [None])[0] - api_id = params.get('api_id', [None])[0] - api_secret = params.get('api_secret', [None])[0] + api_key = params.get('api_key') + api_id = params.get('api_id') + api_secret = params.get('api_secret') # Bookmark this page if api_key and api_id and api_secret: footer = localize(30638) @@ -219,11 +218,11 @@ def do_GET(self): self.send_error(204) elif stripped_path.startswith(PATHS.REDIRECT): - url = parse_qs(urlsplit(self.path).query).get('url') + url = dict(parse_qsl(urlsplit(self.path).query)).get('url') if url: wait(1) self.send_response(301) - self.send_header('Location', url[0]) + self.send_header('Location', url) self.end_headers() else: self.send_error(501) From c956e030e00e6fa57f5491386e17007cfa333b73 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:33:50 +1100 Subject: [PATCH 08/12] Simply OAuth login and remove duplicated code Follow up to 0acd0f3 --- resources/lib/youtube_authentication.py | 2 +- .../kodion/json_store/access_manager.py | 16 +- .../youtube/client/login_client.py | 124 ++++++++------- .../youtube_plugin/youtube/client/youtube.py | 2 + .../youtube_plugin/youtube/helper/yt_login.py | 48 +++--- .../lib/youtube_plugin/youtube/provider.py | 146 +++++++----------- 6 files changed, 165 insertions(+), 173 deletions(-) diff --git a/resources/lib/youtube_authentication.py b/resources/lib/youtube_authentication.py index d32ca5b17..e2f04c942 100644 --- a/resources/lib/youtube_authentication.py +++ b/resources/lib/youtube_authentication.py @@ -167,5 +167,5 @@ def reset_access_tokens(addon_id): return context = XbmcContext(params={'addon_id': addon_id}) context.get_access_manager().update_access_token( - addon_id, access_token='', refresh_token='' + addon_id, access_token='', expiry=-1, refresh_token='' ) diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index 9114bd281..f71467b52 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -435,18 +435,18 @@ def is_access_token_expired(self, addon_id=None): def update_access_token(self, addon_id, access_token=None, - unix_timestamp=None, + expiry=None, refresh_token=None): """ Updates the old access token with the new one. :param access_token: - :param unix_timestamp: + :param expiry: :param refresh_token: :return: """ details = { 'access_token': ( - '|'.join(access_token) + '|'.join([token or '' for token in access_token]) if isinstance(access_token, (list, tuple)) else access_token if access_token else @@ -454,16 +454,16 @@ def update_access_token(self, ) } - if unix_timestamp is not None: + if expiry is not None: details['token_expires'] = ( - min(map(int, [val for val in unix_timestamp if val])) - if isinstance(unix_timestamp, (list, tuple)) else - int(unix_timestamp) + min(map(int, [val for val in expiry if val])) + if isinstance(expiry, (list, tuple)) else + int(expiry) ) if refresh_token is not None: details['refresh_token'] = ( - '|'.join(refresh_token) + '|'.join([token or '' for token in refresh_token]) if isinstance(refresh_token, (list, tuple)) else refresh_token ) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index b69b33435..ab14c4421 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -37,6 +37,12 @@ class LoginClient(YouTubeRequestClient): 'identity.plus.page.impersonation', )) TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' + TOKEN_TYPES = { + 0: 'tv', + 'tv': 'tv', + 1: 'personal', + 'personal': 'personal', + } def __init__(self, configs=None, @@ -106,16 +112,19 @@ def revoke(self, refresh_token): error_info='Revoke failed: {exc}', raise_exc=True) - def refresh_token_tv(self, refresh_token): - client_id = self._config_tv.get('id') - client_secret = self._config_tv.get('secret') - if not client_id or not client_secret: - return '', 0 - return self.refresh_token(refresh_token, - client_id=client_id, - client_secret=client_secret) + def refresh_token(self, token_type, refresh_token=None): + login_type = self.TOKEN_TYPES.get(token_type) + if login_type == 'tv': + client_id = self._config_tv.get('id') + client_secret = self._config_tv.get('secret') + elif login_type == 'personal': + client_id = self._config.get('id') + client_secret = self._config.get('secret') + else: + return None + if not client_id or not client_secret or not refresh_token: + return None - def refresh_token(self, refresh_token, client_id='', client_secret=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'www.googleapis.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' @@ -123,21 +132,21 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} - client_id = client_id or self._config.get('id', '') - client_secret = client_secret or self._config.get('secret', '') post_data = {'client_id': client_id, 'client_secret': client_secret, 'refresh_token': refresh_token, 'grant_type': 'refresh_token'} config_type = self._get_config_type(client_id, client_secret) - client = ''.join(( - '(config_type: |', config_type, - '| client_id: |', client_id[:3], '...', client_id[-5:], - '| client_secret: |', client_secret[:3], '...', client_secret[-3:], - '|)' - )) - log_debug('Refresh token for {0}'.format(client)) + client = (('config_type: |{config_type}|\n' + 'client_id: |{id_start}...{id_end}|\n' + 'client_secret: |{secret_start}...{secret_end}|') + .format(config_type=config_type, + id_start=client_id[:3], + id_end=client_id[-5:], + secret_start=client_secret[:3], + secret_end=client_secret[-3:])) + log_debug('Refresh token\n{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -146,8 +155,9 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): response_hook=LoginClient._response_hook, error_hook=LoginClient._error_hook, error_title='Login Failed', - error_info=('Refresh token failed' - ' {client}:\n{{exc}}' + error_info=('Refresh token failed\n' + '{client}:\n' + '{{exc}}' .format(client=client)), raise_exc=True) @@ -157,16 +167,19 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): return access_token, expiry return '', 0 - def request_access_token_tv(self, code, client_id='', client_secret=''): - client_id = client_id or self._config_tv.get('id') - client_secret = client_secret or self._config_tv.get('secret') - if not client_id or not client_secret: - return '', '' - return self.request_access_token(code, - client_id=client_id, - client_secret=client_secret) + def request_access_token(self, token_type, code=None): + login_type = self.TOKEN_TYPES.get(token_type) + if login_type == 'tv': + client_id = self._config_tv.get('id') + client_secret = self._config_tv.get('secret') + elif login_type == 'personal': + client_id = self._config.get('id') + client_secret = self._config.get('secret') + else: + return None + if not client_id or not client_secret or not code: + return None - def request_access_token(self, code, client_id='', client_secret=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'www.googleapis.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' @@ -174,21 +187,21 @@ def request_access_token(self, code, client_id='', client_secret=''): ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} - client_id = client_id or self._config.get('id', '') - client_secret = client_secret or self._config.get('secret', '') post_data = {'client_id': client_id, 'client_secret': client_secret, 'code': code, 'grant_type': 'http://oauth.net/grant_type/device/1.0'} config_type = self._get_config_type(client_id, client_secret) - client = ''.join(( - '(config_type: |', config_type, - '| client_id: |', client_id[:3], '...', client_id[-5:], - '| client_secret: |', client_secret[:3], '...', client_secret[-3:], - '|)' - )) - log_debug('Requesting access token for {0}'.format(client)) + client = (('config_type: |{config_type}|\n' + 'client_id: |{id_start}...{id_end}|\n' + 'client_secret: |{secret_start}...{secret_end}|') + .format(config_type=config_type, + id_start=client_id[:3], + id_end=client_id[-5:], + secret_start=client_secret[:3], + secret_end=client_secret[-3:])) + log_debug('Requesting access token\n{0}'.format(client)) json_data = self.request(self.TOKEN_URL, method='POST', @@ -197,19 +210,24 @@ def request_access_token(self, code, client_id='', client_secret=''): response_hook=LoginClient._response_hook, error_hook=LoginClient._error_hook, error_title='Login Failed: Unknown response', - error_info=('Access token request failed' - ' {client}:\n{{exc}}' + error_info=('Access token request failed\n' + '{client}:\n' + '{{exc}}' .format(client=client)), raise_exc=True) return json_data - def request_device_and_user_code_tv(self): - client_id = self._config_tv.get('id') + def request_device_and_user_code(self, token_type): + login_type = self.TOKEN_TYPES.get(token_type) + if login_type == 'tv': + client_id = self._config_tv.get('id') + elif login_type == 'personal': + client_id = self._config.get('id') + else: + return None if not client_id: return None - return self.request_device_and_user_code(client_id=client_id) - def request_device_and_user_code(self, client_id=''): # https://developers.google.com/youtube/v3/guides/auth/devices headers = {'Host': 'accounts.google.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' @@ -217,17 +235,16 @@ def request_device_and_user_code(self, client_id=''): ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} - client_id = client_id or self._config.get('id', '') post_data = {'client_id': client_id, 'scope': 'https://www.googleapis.com/auth/youtube'} config_type = self._get_config_type(client_id) - client = ''.join(( - '(config_type: |', config_type, - '| client_id: |', client_id[:3], '...', client_id[-5:], - '|)' - )) - log_debug('Requesting device and user code for {0}'.format(client)) + client = (('config_type: |{config_type}|\n' + 'client_id: |{id_start}...{id_end}|') + .format(config_type=config_type, + id_start=client_id[:3], + id_end=client_id[-5:])) + log_debug('Requesting device and user code\n{0}'.format(client)) json_data = self.request(self.DEVICE_CODE_URL, method='POST', @@ -236,8 +253,9 @@ def request_device_and_user_code(self, client_id=''): response_hook=LoginClient._response_hook, error_hook=LoginClient._error_hook, error_title='Login Failed: Unknown response', - error_info=('Device/user code request failed' - ' {client}:\n{{exc}}' + error_info=('Device/user code request failed\n' + '{client}\n' + '{{exc}}' .format(client=client)), raise_exc=True) return json_data diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 793587314..3fa170e8a 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -2041,6 +2041,8 @@ def api_request(self, # a config can decide if a token is allowed elif self._access_token and self._config.get('token-allowed', True): client_data['_access_token'] = self._access_token + elif self._access_token_tv: + client_data['_access_token'] = self._access_token_tv # abort if authentication is required but not available for request elif self.CLIENTS.get(version, {}).get('auth_required'): abort = True diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index 44c00beae..7057206ff 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -32,21 +32,17 @@ def _do_logout(): except LoginException: pass access_manager.update_access_token( - addon_id, access_token='', refresh_token='', + addon_id, access_token='', expiry=-1, refresh_token='', ) provider.reset_client() - def _do_login(login_type): - for_tv = login_type == 'tv' + def _do_login(token_type): _client = provider.get_client(context) try: - if for_tv: - json_data = _client.request_device_and_user_code_tv() - else: - json_data = _client.request_device_and_user_code() + json_data = _client.request_device_and_user_code(token_type) if not json_data: - return '', 0, '' + return None except LoginException: _do_logout() raise @@ -75,10 +71,8 @@ def _do_login(login_type): for _ in range(steps): dialog.update() try: - if for_tv: - json_data = _client.request_access_token_tv(device_code) - else: - json_data = _client.request_access_token(device_code) + json_data = _client.request_access_token(token_type, + device_code) if not json_data: break except LoginException: @@ -115,7 +109,7 @@ def _do_login(login_type): break context.sleep(interval) - return '', 0, '' + return None if mode == 'out': _do_logout() @@ -125,21 +119,25 @@ def _do_login(login_type): elif mode == 'in': ui.on_ok(localize('sign.multi.title'), localize('sign.multi.text')) - token_types = ('tv', 'personal') - tokens = [None, None] - for idx, token in enumerate(token_types): - new_token = _do_login(login_type=token) - tokens[idx] = new_token - access_token, expiry, refresh_token = new_token + tokens = ['tv', 'personal'] + for token_type, token in enumerate(tokens): + new_token = _do_login(token_type) + tokens[token_type] = new_token + if new_token: + access_token, expiry, refresh_token = new_token + else: + access_token = None + expiry = 0 + refresh_token = None context.log_debug('YouTube Login:' - ' Type |{0}|,' - ' Access Token |{1}|,' - ' Refresh Token |{2}|,' - ' Expires |{3}|' + 'Type: |{0}|\n' + 'Access token: |{1}|\n' + 'Refresh token: |{2}|\n' + 'Expires: |{3}|' .format(token, - access_token != '', - refresh_token != '', + bool(access_token), + bool(refresh_token), expiry)) provider.reset_client() diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 31dff371a..332c050ea 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -197,73 +197,49 @@ def get_client(self, context): access_manager.set_last_origin(origin) self.reset_client() - if dev_id: - access_tokens = access_manager.get_access_token(dev_id) - if access_manager.is_access_token_expired(dev_id): - # reset access_token - access_tokens = [] - access_manager.update_access_token(dev_id, access_tokens) - elif self._client: - return self._client - - if dev_keys: - context.log_debug('Selecting YouTube developer config "{0}"' - .format(dev_id)) - configs['main'] = dev_keys - else: - dev_keys = configs['main'] - context.log_debug('Selecting YouTube config "{0}"' - ' w/ developer access tokens' - .format(dev_keys['system'])) - - refresh_tokens = access_manager.get_refresh_token(dev_id) - if refresh_tokens: - keys_changed = access_manager.dev_keys_changed( - dev_id, dev_keys['key'], dev_keys['id'], dev_keys['secret'] - ) - if keys_changed: - context.log_warning('API key set changed: Resetting client' - ' and updating access token') - self.reset_client() - access_tokens = [] - refresh_tokens = [] - access_manager.update_access_token( - dev_id, access_tokens, -1, refresh_tokens - ) - - context.log_debug( - 'Access token count: |{0}|, refresh token count: |{1}|' - .format(len(access_tokens), len(refresh_tokens)) - ) - else: - access_tokens = access_manager.get_access_token(dev_id) - if access_manager.is_access_token_expired(dev_id): - # reset access_token - access_tokens = [] - access_manager.update_access_token(dev_id, access_tokens) - elif self._client: - return self._client - + access_tokens = access_manager.get_access_token(dev_id) + if access_manager.is_access_token_expired(dev_id): + # reset access_token + access_tokens = [None, None] + access_manager.update_access_token(dev_id, access_token='') + elif self._client: + return self._client + + if not dev_id: context.log_debug('Selecting YouTube config "{0}"' .format(configs['main']['system'])) - - refresh_tokens = access_manager.get_refresh_token(dev_id) - if refresh_tokens: - if self._api_check.changed: - context.log_warning('API key set changed: Resetting client' - ' and updating access token') - self.reset_client() - access_tokens = [] - refresh_tokens = [] - access_manager.update_access_token( - dev_id, access_tokens, -1, refresh_tokens, - ) - - context.log_debug( - 'Access token count: |{0}|, refresh token count: |{1}|' - .format(len(access_tokens), len(refresh_tokens)) + elif dev_keys: + context.log_debug('Selecting YouTube developer config "{0}"' + .format(dev_id)) + configs['main'] = dev_keys + else: + dev_keys = configs['main'] + context.log_debug('Selecting YouTube config "{0}"' + ' w/ developer access tokens' + .format(dev_keys['system'])) + + refresh_tokens = access_manager.get_refresh_token(dev_id) + if refresh_tokens: + keys_changed = access_manager.dev_keys_changed( + dev_id, dev_keys['key'], dev_keys['id'], dev_keys['secret'] + ) if dev_id else self._api_check.changed + if keys_changed: + context.log_warning('API key set changed: Resetting client' + ' and updating access token') + self.reset_client() + access_tokens = [None, None] + refresh_tokens = [None, None] + access_manager.update_access_token( + dev_id, access_token='', expiry=-1, refresh_token='' ) + num_access_tokens = sum(1 for token in access_tokens if token) + num_refresh_tokens = sum(1 for token in refresh_tokens if token) + context.log_debug( + 'Access token count: |{0}|, refresh token count: |{1}|' + .format(num_access_tokens, num_refresh_tokens) + ) + settings = context.get_settings() client = YouTube(context=context, language=settings.get_language(), @@ -276,33 +252,25 @@ def get_client(self, context): self._client = client # create new access tokens - elif len(access_tokens) != 2 and len(refresh_tokens) == 2: - access_tokens = ['', ''] + elif num_access_tokens != num_refresh_tokens: + access_tokens = [None, None] token_expiry = 0 - token_index = { - 0: { - 'type': 'tv', - 'refresh': client.refresh_token_tv, - }, - 1: { - 'type': 'personal', - 'refresh': client.refresh_token, - }, - } try: - for idx, value in enumerate(refresh_tokens): + for token_type, value in enumerate(refresh_tokens): if not value: continue - token, expiry = token_index[idx]['refresh'](value) + token, expiry = client.refresh_token(token_type, value) if token and expiry > 0: - access_tokens[idx] = token + access_tokens[token_type] = token if not token_expiry or expiry < token_expiry: token_expiry = expiry - if any(access_tokens) and expiry: + if any(access_tokens) and token_expiry: access_manager.update_access_token( - dev_id, access_tokens, token_expiry, + dev_id, + access_token=access_tokens, + expiry=token_expiry, ) else: raise InvalidGrant @@ -312,21 +280,26 @@ def get_client(self, context): # reset access token # reset refresh token if InvalidGrant otherwise leave as-is # to retry later + if isinstance(exc, InvalidGrant): + refresh_token = '' + else: + refresh_token = None access_manager.update_access_token( dev_id, - refresh_token=('' if isinstance(exc, InvalidGrant) - else None), + refresh_token=refresh_token, ) - # in debug log the login status - self._logged_in = any(access_tokens) - if self._logged_in: + num_access_tokens = sum(1 for token in access_tokens if token) + + if num_access_tokens and access_tokens[1]: + self._logged_in = True context.log_debug('User is logged in') client.set_access_token( personal=access_tokens[1], tv=access_tokens[0], ) else: + self._logged_in = False context.log_debug('User is not logged in') client.set_access_token(personal='', tv='') @@ -966,7 +939,7 @@ def on_maintenance_actions(provider, context, re_match): success = False provider.reset_client() access_manager.update_access_token( - addon_id, access_token='', refresh_token='', + addon_id, access_token='', expiry=-1, refresh_token='', ) ui.refresh_container() ui.show_notification(localize('succeeded' if success else 'failed')) @@ -1646,6 +1619,7 @@ def handle_exception(self, context, exception_to_handle): context.get_access_manager().update_access_token( context.get_param('addon_id', None), access_token='', + expiry=-1, refresh_token='', ) ok_dialog = True From 32aa586620ea6839c83c1c07616c802c29dc9efa Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:20:42 +1100 Subject: [PATCH 09/12] Add localised title and description for videos, channels and playlists --- .../youtube_plugin/kodion/items/media_item.py | 27 ++++++++++--- .../youtube_plugin/youtube/helper/utils.py | 38 ++++++++++++++---- .../lib/youtube_plugin/youtube/helper/v3.py | 39 ++++++++++++++++--- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/media_item.py b/resources/lib/youtube_plugin/kodion/items/media_item.py index 4118e59e1..3cec4bf54 100644 --- a/resources/lib/youtube_plugin/kodion/items/media_item.py +++ b/resources/lib/youtube_plugin/kodion/items/media_item.py @@ -25,7 +25,12 @@ class MediaItem(BaseItem): _playable = True - def __init__(self, name, uri, image='DefaultFile.png', fanart=None): + def __init__(self, + name, + uri, + image='DefaultFile.png', + fanart=None, + plot=None): super(MediaItem, self).__init__(name, uri, image, fanart) self._aired = None self._premiered = None @@ -44,7 +49,7 @@ def __init__(self, name, uri, image='DefaultFile.png', fanart=None): self._start_time = None self._mediatype = None - self._plot = None + self._plot = plot self._production_code = None self._rating = None self._title = self.get_name() @@ -384,8 +389,13 @@ class AudioItem(MediaItem): _ALLOWABLE_MEDIATYPES = {CONTENT.AUDIO_TYPE, 'song', 'album', 'artist'} _DEFAULT_MEDIATYPE = CONTENT.AUDIO_TYPE - def __init__(self, name, uri, image='DefaultAudio.png', fanart=None): - super(AudioItem, self).__init__(name, uri, image, fanart) + def __init__(self, + name, + uri, + image='DefaultAudio.png', + fanart=None, + plot=None): + super(AudioItem, self).__init__(name, uri, image, fanart, plot) self._album = None def set_album_name(self, album_name): @@ -405,8 +415,13 @@ class VideoItem(MediaItem): r'(http(s)?://)?www.imdb.(com|de)/title/(?P[t0-9]+)(/)?' ) - def __init__(self, name, uri, image='DefaultVideo.png', fanart=None): - super(VideoItem, self).__init__(name, uri, image, fanart) + def __init__(self, + name, + uri, + image='DefaultVideo.png', + fanart=None, + plot=None): + super(VideoItem, self).__init__(name, uri, image, fanart, plot) self._directors = None self._episode = None self._imdb_id = None diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index d44ff78a2..2550ef8bb 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -153,6 +153,7 @@ def update_channel_infos(provider, context, channel_id_dict, settings = context.get_settings() logged_in = provider.is_logged_in() path = context.get_path() + untitled = context.localize('untitled') filter_list = None if path.startswith(PATHS.SUBSCRIPTIONS): @@ -183,9 +184,16 @@ def update_channel_infos(provider, context, channel_id_dict, channel_item = channel_id_dict[channel_id] # title - title = snippet['title'] + localised_info = snippet.get('localized') or {} + title = localised_info.get('title') or snippet.get('title') or untitled channel_item.set_name(title) + # plot + description = strip_html_from_text(localised_info.get('description') + or snippet.get('description') + or '') + channel_item.set_plot(description) + # image image = get_thumbnail(thumb_size, snippet.get('thumbnails')) channel_item.set_image(image) @@ -261,6 +269,7 @@ def update_playlist_infos(provider, context, playlist_id_dict, logged_in = provider.is_logged_in() path = context.get_path() thumb_size = context.get_settings().get_thumbnail_size() + untitled = context.localize('untitled') # if the path directs to a playlist of our own, set channel id to 'mine' if path.startswith(PATHS.MY_PLAYLISTS): @@ -280,9 +289,17 @@ def update_playlist_infos(provider, context, playlist_id_dict, playlist_item = playlist_id_dict[playlist_id] - title = snippet['title'] + # title + localised_info = snippet.get('localized') or {} + title = localised_info.get('title') or snippet.get('title') or untitled playlist_item.set_name(title) + # plot + description = strip_html_from_text(localised_info.get('description') + or snippet.get('description') + or '') + playlist_item.set_plot(description) + image = get_thumbnail(thumb_size, snippet.get('thumbnails')) playlist_item.set_image(image) @@ -582,9 +599,12 @@ def update_video_infos(provider, context, video_id_dict, media_item.set_production_code(label_stats) # update and set the title + localised_info = snippet.get('localized') or {} title = media_item.get_title() if not title or title == untitled: - title = snippet.get('title') or untitled + title = (localised_info.get('title') + or snippet.get('title') + or untitled) media_item.set_title(ui.italic(title) if media_item.upcoming else title) """ @@ -614,7 +634,7 @@ def update_video_infos(provider, context, video_id_dict, break # channel name - channel_name = snippet.get('channelTitle', '') + channel_name = snippet.get('channelTitle', '') or untitled media_item.add_artist(channel_name) if 'cast' in channel_name_aliases: media_item.add_cast(channel_name, role=channel_role) @@ -622,15 +642,17 @@ def update_video_infos(provider, context, video_id_dict, media_item.add_studio(channel_name) # plot - description = strip_html_from_text(snippet['description']) + description = strip_html_from_text(localised_info.get('description') + or snippet.get('description') + or '') if show_details: description = ''.join(( - ui.bold(channel_name, cr_after=1) if channel_name else '', + ui.bold(channel_name, cr_after=1), ui.new_line(stats, cr_after=1) if stats else '', (ui.italic(start_at, cr_after=1) if media_item.upcoming else ui.new_line(start_at, cr_after=1)) if start_at else '', - description, - ui.new_line('https://youtu.be/' + video_id, cr_before=1) + ui.new_line(description, cr_after=1) if description else '', + 'https://youtu.be/' + video_id, )) media_item.set_plot(description) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 46125f64e..3d6212b89 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -25,6 +25,7 @@ from ...kodion import KodionException from ...kodion.constants import PATHS from ...kodion.items import CommandItem, DirectoryItem, NextPageItem, VideoItem +from ...kodion.utils import strip_html_from_text def _process_list_response(provider, context, json_data, item_filter): @@ -58,6 +59,7 @@ def _process_list_response(provider, context, json_data, item_filter): fanart_type = settings.get_thumbnail_size(settings.THUMB_SIZE_BEST) else: fanart_type = False + untitled = context.localize('untitled') for yt_item in yt_items: kind, is_youtube, is_plugin, kind_type = _parse_kind(yt_item) @@ -71,7 +73,15 @@ def _process_list_response(provider, context, json_data, item_filter): if is_youtube: item_id = yt_item.get('id') snippet = yt_item.get('snippet', {}) - title = snippet.get('title', context.localize('untitled')) + + localised_info = snippet.get('localized') or {} + title = (localised_info.get('title') + or snippet.get('title') + or untitled) + description = strip_html_from_text(localised_info.get('description') + or snippet.get('description') + or '') + # context.log_debug(f'***********\n{item_id = }, {title = }\n{yt_item = }\n***************') thumbnails = snippet.get('thumbnails') if not thumbnails: @@ -109,7 +119,11 @@ def _process_list_response(provider, context, json_data, item_filter): (PATHS.PLAY,), item_params, ) - item = VideoItem(title, item_uri, image=image, fanart=fanart) + item = VideoItem(title, + item_uri, + image=image, + fanart=fanart, + plot=description) video_id_dict[item_id] = item elif kind_type == 'channel': @@ -121,6 +135,7 @@ def _process_list_response(provider, context, json_data, item_filter): item_uri, image=image, fanart=fanart, + plot=description, channel_id=item_id) channel_id_dict[item_id] = item @@ -130,7 +145,11 @@ def _process_list_response(provider, context, json_data, item_filter): ('special', 'browse_channels'), item_params, ) - item = DirectoryItem(title, item_uri) + item = DirectoryItem(title, + item_uri, + image=image, + fanart=fanart, + plot=description) elif kind_type == 'subscription': subscription_id = item_id @@ -146,6 +165,7 @@ def _process_list_response(provider, context, json_data, item_filter): item_uri, image=image, fanart=fanart, + plot=description, channel_id=item_id, subscription_id=subscription_id) channel_id_dict[item_id] = item @@ -171,6 +191,7 @@ def _process_list_response(provider, context, json_data, item_filter): item_uri, image=image, fanart=fanart, + plot=description, channel_id=channel_id, playlist_id=item_id) playlist_id_dict[item_id] = item @@ -186,7 +207,11 @@ def _process_list_response(provider, context, json_data, item_filter): (PATHS.PLAY,), item_params, ) - item = VideoItem(title, item_uri, image=image, fanart=fanart) + item = VideoItem(title, + item_uri, + image=image, + fanart=fanart, + plot=description) video_id_dict[item_id] = item elif kind_type == 'activity': @@ -204,7 +229,11 @@ def _process_list_response(provider, context, json_data, item_filter): (PATHS.PLAY,), item_params, ) - item = VideoItem(title, item_uri, image=image, fanart=fanart) + item = VideoItem(title, + item_uri, + image=image, + fanart=fanart, + plot=description) video_id_dict[item_id] = item elif kind_type == 'commentthread': From 315444edab3a3b28b47385716fc6f0d9cd7c7944 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 10 Oct 2024 01:04:19 +1100 Subject: [PATCH 10/12] Update display of playlists Show the following details: - item count - date - channel name - description - web url - podcast status --- .../resource.language.en_gb/strings.po | 10 +- .../kodion/context/xbmc/xbmc_context.py | 3 + .../youtube_plugin/kodion/items/__init__.py | 3 +- .../youtube_plugin/kodion/items/base_item.py | 57 +++ .../youtube_plugin/kodion/items/media_item.py | 56 -- .../kodion/items/xbmc/xbmc_items.py | 480 +++++++++--------- .../kodion/settings/abstract_settings.py | 6 + .../youtube_plugin/youtube/client/youtube.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 83 ++- 9 files changed, 391 insertions(+), 309 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index dccf900e2..5fda428e8 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1254,11 +1254,11 @@ msgid "Shorts (1 minute or less)" msgstr "" msgctxt "#30737" -msgid "" +msgid "Episodes" msgstr "" msgctxt "#30738" -msgid "" +msgid "Videos" msgstr "" msgctxt "#30739" @@ -1486,7 +1486,7 @@ msgid "Likes count display colour" msgstr "" msgctxt "#30795" -msgid "Comments count display colour" +msgid "Videos/Comments count display colour" msgstr "" msgctxt "#30796" @@ -1584,3 +1584,7 @@ msgstr "" msgctxt "#30819" msgid "Play from start" msgstr "" + +msgctxt "#30820" +msgid "Podcast" +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 45232bbef..0761fdd2a 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -150,6 +150,7 @@ class XbmcContext(AbstractContext): 'playlist.play.reverse': 30533, 'playlist.play.select': 30535, 'playlist.play.shuffle': 30534, + 'playlist.podcast': 30820, 'playlist.progress.updating': 30536, 'playlist.removed_from': 30715, 'playlist.select': 30521, @@ -210,7 +211,9 @@ class XbmcContext(AbstractContext): 'sign.multi.title': 30546, 'stats.commentCount': 30732, # 'stats.favoriteCount': 1036, + 'stats.itemCount': 30737, 'stats.likeCount': 30733, + 'stats.videoCount': 30738, 'stats.viewCount': 30767, 'stream.alternate': 30747, 'stream.automatic': 30583, diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index 7e11964e8..48bb1ac62 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -15,7 +15,7 @@ from .command_item import CommandItem from .directory_item import DirectoryItem from .image_item import ImageItem -from .media_item import AudioItem, VideoItem +from .media_item import AudioItem, MediaItem, VideoItem from .new_search_item import NewSearchItem from .next_page_item import NextPageItem from .search_history_item import SearchHistoryItem @@ -38,6 +38,7 @@ 'CommandItem', 'DirectoryItem', 'ImageItem', + 'MediaItem', 'NewSearchItem', 'NextPageItem', 'SearchHistoryItem', diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index e286e3cce..84aa969d7 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -42,6 +42,11 @@ def __init__(self, name, uri, image=None, fanart=None): self._date = None self._dateadded = None self._short_details = None + self._production_code = None + + self._cast = None + self._artists = None + self._studios = None def __str__(self): return ('------------------------------\n' @@ -195,6 +200,58 @@ def get_bookmark_timestamp(self): def playable(self): return self._playable + def add_artist(self, artist): + if artist: + if self._artists is None: + self._artists = [] + self._artists.append(to_str(artist)) + + def get_artists(self): + return self._artists + + def get_artists_string(self): + if self._artists: + return ', '.join(self._artists) + return None + + def set_artists(self, artists): + self._artists = list(artists) + + def set_cast(self, members): + self._cast = list(members) + + def add_cast(self, name, role=None, order=None, thumbnail=None): + if name: + if self._cast is None: + self._cast = [] + self._cast.append({ + 'name': to_str(name), + 'role': to_str(role) if role else '', + 'order': int(order) if order else len(self._cast) + 1, + 'thumbnail': to_str(thumbnail) if thumbnail else '', + }) + + def get_cast(self): + return self._cast + + def add_studio(self, studio): + if studio: + if self._studios is None: + self._studios = [] + self._studios.append(to_str(studio)) + + def get_studios(self): + return self._studios + + def set_studios(self, studios): + self._studios = list(studios) + + def set_production_code(self, value): + self._production_code = value or '' + + def get_production_code(self): + return self._production_code + class _Encoder(json.JSONEncoder): def encode(self, obj, nested=False): diff --git a/resources/lib/youtube_plugin/kodion/items/media_item.py b/resources/lib/youtube_plugin/kodion/items/media_item.py index 3cec4bf54..1b59fbc9c 100644 --- a/resources/lib/youtube_plugin/kodion/items/media_item.py +++ b/resources/lib/youtube_plugin/kodion/items/media_item.py @@ -37,10 +37,7 @@ def __init__(self, self._scheduled_start_utc = None self._year = None - self._artists = None - self._cast = None self._genres = None - self._studios = None self._duration = -1 self._play_count = None @@ -50,7 +47,6 @@ def __init__(self, self._mediatype = None self._plot = plot - self._production_code = None self._rating = None self._title = self.get_name() self._track_number = None @@ -115,40 +111,6 @@ def set_year_from_datetime(self, date_time): def get_year(self): return self._year - def add_artist(self, artist): - if artist: - if self._artists is None: - self._artists = [] - self._artists.append(to_str(artist)) - - def get_artists(self): - return self._artists - - def get_artists_string(self): - if self._artists: - return ', '.join(self._artists) - return None - - def set_artists(self, artists): - self._artists = list(artists) - - def set_cast(self, members): - self._cast = list(members) - - def add_cast(self, name, role=None, order=None, thumbnail=None): - if name: - if self._cast is None: - self._cast = [] - self._cast.append({ - 'name': to_str(name), - 'role': to_str(role) if role else '', - 'order': int(order) if order else len(self._cast) + 1, - 'thumbnail': to_str(thumbnail) if thumbnail else '', - }) - - def get_cast(self): - return self._cast - def add_genre(self, genre): if genre: if self._genres is None: @@ -161,18 +123,6 @@ def get_genres(self): def set_genres(self, genres): self._genres = list(genres) - def add_studio(self, studio): - if studio: - if self._studios is None: - self._studios = [] - self._studios.append(to_str(studio)) - - def get_studios(self): - return self._studios - - def set_studios(self, studios): - self._studios = list(studios) - def set_duration(self, hours=0, minutes=0, seconds=0, duration=''): if duration: _seconds = duration_to_seconds(duration) @@ -237,12 +187,6 @@ def set_plot(self, plot): def get_plot(self): return self._plot - def set_production_code(self, value): - self._production_code = value or '' - - def get_production_code(self): - return self._production_code - def set_rating(self, rating): rating = float(rating) if rating > 10: 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 cc1087f5f..3e57e76bb 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -12,7 +12,13 @@ from json import dumps -from .. import AudioItem, DirectoryItem, ImageItem, VideoItem +from .. import ( + AudioItem, + DirectoryItem, + ImageItem, + MediaItem, + VideoItem, +) from ...compatibility import to_str, xbmc, xbmcgui from ...constants import ( CHANNEL_ID, @@ -29,61 +35,86 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): if not current_system_version.compatible(20): - if isinstance(item, VideoItem): - info_labels = {} - info_type = 'video' + info_labels = {} + info_type = None + + if isinstance(item, MediaItem): + if isinstance(item, VideoItem): + info_type = 'video' + + value = item.get_episode() + if value is not None: + info_labels['episode'] = value + + value = item.get_season() + if value is not None: + info_labels['season'] = value + + elif isinstance(item, AudioItem): + info_type = 'music' + + value = item.get_album_name() + if value is not None: + info_labels['album'] = value + + else: + return value = item.get_aired(as_info_label=True) if value is not None: info_labels['aired'] = value - value = item.get_cast() + value = item.get_premiered(as_info_label=True) if value is not None: - info_labels['castandrole'] = [(member['name'], member['role']) - for member in value] + info_labels['premiered'] = value - value = item.get_production_code() + value = item.get_plot() if value is not None: - info_labels['code'] = value + info_labels['plot'] = value - value = item.get_dateadded(as_info_label=True) + value = item.get_last_played(as_info_label=True) if value is not None: - info_labels['dateadded'] = value + info_labels['lastplayed'] = value - value = item.get_episode() + value = item.get_mediatype() if value is not None: - info_labels['episode'] = value + info_labels['mediatype'] = value - value = item.get_plot() + value = item.get_play_count() if value is not None: - info_labels['plot'] = value + if set_play_count: + info_labels['playcount'] = value + properties[PLAY_COUNT] = value - value = item.get_premiered(as_info_label=True) + value = item.get_rating() if value is not None: - info_labels['premiered'] = value + info_labels['rating'] = value - value = item.get_season() + value = item.get_title() if value is not None: - info_labels['season'] = value + info_labels['title'] = value - value = item.get_studios() + value = item.get_track_number() if value is not None: - info_labels['studio'] = value - - elif isinstance(item, AudioItem): - info_labels = {} - info_type = 'music' + info_labels['tracknumber'] = value - value = item.get_album_name() + value = item.get_year() if value is not None: - info_labels['album'] = value + info_labels['year'] = value - value = item.get_plot() - if value is not None: - info_labels['plot'] = value + resume_time = resume and item.get_start_time() + if resume_time: + properties['ResumeTime'] = str(resume_time) + duration = item.get_duration() + if duration: + properties['TotalTime'] = str(duration) + if info_type == 'video': + list_item.addStreamInfo(info_type, {'duration': duration}) + + if duration is not None: + info_labels['duration'] = value elif isinstance(item, DirectoryItem): - info_labels = {} info_type = 'video' value = item.get_name() @@ -94,204 +125,205 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): if value is not None: info_labels['plot'] = value - if info_labels: - list_item.setInfo(info_type, info_labels) - - if properties: - list_item.setProperties(properties) - return - elif isinstance(item, ImageItem): + info_type = 'picture' + value = item.get_title() if value is not None: - list_item.setInfo('picture', {'title': value}) - - if properties: - list_item.setProperties(properties) - return + info_labels['title'] = value else: return - value = item.get_artists() - if value is not None: - info_labels['artist'] = value - - value = item.get_count() - if value is not None: - info_labels['count'] = value - - value = item.get_date(as_info_label=True) - if value is not None: - info_labels['date'] = value - - value = item.get_duration() - if value is not None: - info_labels['duration'] = value - - value = item.get_last_played(as_info_label=True) + value = item.get_production_code() if value is not None: - info_labels['lastplayed'] = value + info_labels['code'] = value - value = item.get_mediatype() + value = item.get_dateadded(as_info_label=True) if value is not None: - info_labels['mediatype'] = value + info_labels['dateadded'] = value - value = item.get_play_count() + value = item.get_studios() if value is not None: - if set_play_count: - info_labels['playcount'] = value - properties[PLAY_COUNT] = value + info_labels['studio'] = value - value = item.get_rating() + value = item.get_cast() if value is not None: - info_labels['rating'] = value + info_labels['castandrole'] = [(member['name'], member['role']) + for member in value] - value = item.get_title() + value = item.get_artists() if value is not None: - info_labels['title'] = value + info_labels['artist'] = value - value = item.get_track_number() + value = item.get_count() if value is not None: - info_labels['tracknumber'] = value + info_labels['count'] = value - value = item.get_year() + value = item.get_date(as_info_label=True) if value is not None: - info_labels['year'] = value - - resume_time = resume and item.get_start_time() - if resume_time: - properties['ResumeTime'] = str(resume_time) - duration = item.get_duration() - if duration: - properties['TotalTime'] = str(duration) - if info_type == 'video': - list_item.addStreamInfo(info_type, {'duration': duration}) + info_labels['date'] = value if properties: list_item.setProperties(properties) - if info_labels: + if info_labels and info_type: list_item.setInfo(info_type, info_labels) return - value = item.get_date(as_info_label=True) - if value is not None: - list_item.setDateTime(value) + if isinstance(item, MediaItem): + if isinstance(item, VideoItem): + info_tag = list_item.getVideoInfoTag() + info_type = 'video' - if isinstance(item, VideoItem): - info_tag = list_item.getVideoInfoTag() - info_type = 'video' + # episode: int + value = item.get_episode() + if value is not None: + info_tag.setEpisode(value) - value = item.get_aired(as_info_label=True) - if value is not None: - info_tag.setFirstAired(value) + # season: int + value = item.get_season() + if value is not None: + info_tag.setSeason(value) - value = item.get_dateadded(as_info_label=True) - if value is not None: - info_tag.setDateAdded(value) + value = item.get_premiered(as_info_label=True) + if value is not None: + info_tag.setPremiered(value) - value = item.get_premiered(as_info_label=True) - if value is not None: - info_tag.setPremiered(value) + value = item.get_aired(as_info_label=True) + if value is not None: + info_tag.setFirstAired(value) - # artist: list[str] - # eg. ["Angerfist"] - # Used as alias for channel name - value = item.get_artists() - if value is not None: - info_tag.setArtists(value) + # plot: str + value = item.get_plot() + if value is not None: + info_tag.setPlot(value) - # cast: list[xbmc.Actor] - # From list[{member: str, role: str, order: int, thumbnail: str}] - # Used as alias for channel name if enabled - value = item.get_cast() - if value is not None: - info_tag.setCast([xbmc.Actor(**member) for member in value]) + # tracknumber: int + # eg. 12 + value = item.get_track_number() + if value is not None: + info_tag.setTrackNumber(value) + + # director: list[str] + # eg. "Steven Spielberg" + # Currently unused + # value = item.get_directors() + # if value is not None: + # info_tag.setDirectors(value) + + # imdbnumber: str + # eg. "tt3458353" + # Currently unused + # value = item.get_imdb_id() + # if value is not None: + # info_tag.setIMDBNumber(value) - # director: list[str] - # eg. "Steven Spielberg" - # Currently unused - # value = item.get_directors() - # if value is not None: - # info_tag.setDirectors(value) + elif isinstance(item, AudioItem): + info_tag = list_item.getMusicInfoTag() + info_type = 'music' - # episode: int - value = item.get_episode() - if value is not None: - info_tag.setEpisode(value) + # album: str + # eg. "Buckle Up" + value = item.get_album_name() + if value is not None: + info_tag.setAlbum(value) - # imdbnumber: str - # eg. "tt3458353" - # Currently unused - # value = item.get_imdb_id() - # if value is not None: - # info_tag.setIMDBNumber(value) + value = item.get_premiered(as_info_label=True) + if value is not None: + info_tag.setReleaseDate(value) - # plot: str - value = item.get_plot() - if value is not None: - info_tag.setPlot(value) + # comment: str + value = item.get_plot() + if value is not None: + info_tag.setComment(value) - # code: str - # eg. "466K | 3.9K | 312" - # Production code, currently used to store misc video data for label - # formatting - value = item.get_production_code() - if value is not None: - info_tag.setProductionCode(value) + # artist: str + # eg. "Artist 1, Artist 2" + # Used as alias for channel name + value = item.get_artists_string() + if value is not None: + info_tag.setArtist(value) - # season: int - value = item.get_season() - if value is not None: - info_tag.setSeason(value) + # track: int + # eg. 12 + value = item.get_track_number() + if value is not None: + info_tag.setTrack(value) - # studio: list[str] - # Used as alias for channel name if enabled - value = item.get_studios() - if value is not None: - info_tag.setStudios(value) + else: + return - # tracknumber: int - # eg. 12 - value = item.get_track_number() + value = item.get_last_played(as_info_label=True) if value is not None: - info_tag.setTrackNumber(value) - - elif isinstance(item, AudioItem): - info_tag = list_item.getMusicInfoTag() - info_type = 'music' + info_tag.setLastPlayed(value) - value = item.get_premiered(as_info_label=True) + # mediatype: str + value = item.get_mediatype() if value is not None: - info_tag.setReleaseDate(value) + info_tag.setMediaType(value) - # album: str - # eg. "Buckle Up" - value = item.get_album_name() + # playcount: int + value = item.get_play_count() if value is not None: - info_tag.setAlbum(value) + if set_play_count: + if info_type == 'video': + info_tag.setPlaycount(value) + elif info_type == 'music': + info_tag.setPlayCount(value) + properties[PLAY_COUNT] = value - # artist: str - # eg. "Artist 1, Artist 2" - # Used as alias for channel name - value = item.get_artists_string() + # rating: float + value = item.get_rating() if value is not None: - info_tag.setArtist(value) + info_tag.setRating(value) - # comment: str - value = item.get_plot() + # title: str + # eg. "Blow Your Head Off" + value = item.get_title() if value is not None: - info_tag.setComment(value) + info_tag.setTitle(value) - # track: int - # eg. 12 - value = item.get_track_number() + # year: int + # eg. 1994 + value = item.get_year() if value is not None: - info_tag.setTrack(value) + info_tag.setYear(value) + + # genre: list[str] + # eg. ["Hardcore"] + # Currently unused + # value = item.get_genres() + # if value is not None: + # info_tag.setGenres(value) + + resume_time = resume and item.get_start_time() + duration = item.get_duration() + if info_type == 'video': + if resume_time and duration: + info_tag.setResumePoint(resume_time, float(duration)) + elif resume_time: + info_tag.setResumePoint(resume_time) + if duration: + info_tag.addVideoStream(xbmc.VideoStreamDetail( + duration=duration, + )) + elif info_type == 'music': + # These properties are deprecated but there is no other way to set + # these details for a ListItem with a MusicInfoTag + if resume_time: + properties['ResumeTime'] = str(resume_time) + if duration: + properties['TotalTime'] = str(duration) + + # duration: int + # As seconds + if duration is not None: + info_tag.setDuration(duration) elif isinstance(item, DirectoryItem): info_tag = list_item.getVideoInfoTag() + info_type = 'video' value = item.get_name() if value is not None: @@ -301,64 +333,49 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): if value is not None: info_tag.setPlot(value) - if properties: - list_item.setProperties(properties) - return - elif isinstance(item, ImageItem): info_tag = list_item.getPictureInfoTag() + info_type = 'picture' value = item.get_title() if value is not None: info_tag.setTitle(value) - if properties: - list_item.setProperties(properties) - return - else: return - resume_time = resume and item.get_start_time() - duration = item.get_duration() if info_type == 'video': - if resume_time and duration: - info_tag.setResumePoint(resume_time, float(duration)) - elif resume_time: - info_tag.setResumePoint(resume_time) - if duration: - info_tag.addVideoStream(xbmc.VideoStreamDetail(duration=duration)) - elif info_type == 'music': - # These properties are deprecated but there is no other way to set these - # details for a ListItem with a MusicInfoTag - if resume_time: - properties['ResumeTime'] = str(resume_time) - if duration: - properties['TotalTime'] = str(duration) - - # duration: int - # As seconds - if duration is not None: - info_tag.setDuration(duration) - - # mediatype: str - value = item.get_mediatype() - if value is not None: - info_tag.setMediaType(value) + # code: str + # eg. "466K | 3.9K | 312" + # Production code, currently used to store misc video data for label + # formatting + value = item.get_production_code() + if value is not None: + info_tag.setProductionCode(value) - value = item.get_last_played(as_info_label=True) - if value is not None: - info_tag.setLastPlayed(value) + value = item.get_dateadded(as_info_label=True) + if value is not None: + info_tag.setDateAdded(value) - # playcount: int - value = item.get_play_count() - if value is not None: - if set_play_count: - if info_type == 'video': - info_tag.setPlaycount(value) - elif info_type == 'music': - info_tag.setPlayCount(value) - properties[PLAY_COUNT] = value + # studio: list[str] + # Used as alias for channel name if enabled + value = item.get_studios() + if value is not None: + info_tag.setStudios(value) + + # cast: list[xbmc.Actor] + # From list[{member: str, role: str, order: int, thumbnail: str}] + # Used as alias for channel name if enabled + value = item.get_cast() + if value is not None: + info_tag.setCast([xbmc.Actor(**member) for member in value]) + + # artist: list[str] + # eg. ["Angerfist"] + # Used as alias for channel name + value = item.get_artists() + if value is not None: + info_tag.setArtists(value) # count: int # eg. 12 @@ -368,29 +385,9 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True): if value is not None: list_item.setInfo(info_type, {'count': value}) - # genre: list[str] - # eg. ["Hardcore"] - # Currently unused - # value = item.get_genres() - # if value is not None: - # info_tag.setGenres(value) - - # rating: float - value = item.get_rating() - if value is not None: - info_tag.setRating(value) - - # title: str - # eg. "Blow Your Head Off" - value = item.get_title() - if value is not None: - info_tag.setTitle(value) - - # year: int - # eg. 1994 - value = item.get_year() + value = item.get_date(as_info_label=True) if value is not None: - info_tag.setYear(value) + list_item.setDateTime(value) if properties: list_item.setProperties(properties) @@ -523,6 +520,7 @@ def directory_listitem(context, directory_item, show_fanart=None, **_kwargs): kwargs = { 'label': directory_item.get_name(), + 'label2': directory_item.get_short_details(), 'path': uri, 'offscreen': True, } diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 9690b1da0..83e62be2e 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -629,13 +629,19 @@ def set_history_playlist(self, value): return self.set_string(SETTINGS.HISTORY_PLAYLIST, value) if current_system_version.compatible(20): + _COLOR_SETTING_MAP = { + 'itemCount': 'commentCount', + } + def get_label_color(self, label_part): + label_part = self._COLOR_SETTING_MAP.get(label_part) or label_part setting_name = '.'.join((SETTINGS.LABEL_COLOR, label_part)) return self.get_string(setting_name, 'white') else: _COLOR_MAP = { 'commentCount': 'cyan', 'favoriteCount': 'gold', + 'itemCount': 'cyan', 'likeCount': 'lime', 'viewCount': 'lightblue', } diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 3fa170e8a..48e30566f 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -989,7 +989,7 @@ def get_playlists(self, playlist_id, **kwargs): if not isinstance(playlist_id, string_type): playlist_id = ','.join(playlist_id) - params = {'part': 'snippet,contentDetails', + params = {'part': 'snippet,status,contentDetails', 'id': playlist_id} return self.api_request(method='GET', path='playlists', diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 2550ef8bb..421763ef1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -267,9 +267,22 @@ def update_playlist_infos(provider, context, playlist_id_dict, custom_watch_later_id = access_manager.get_watch_later_id() custom_history_id = access_manager.get_watch_history_id() logged_in = provider.is_logged_in() + + settings = context.get_settings() + thumb_size = settings.get_thumbnail_size() + channel_name_aliases = settings.get_channel_name_aliases() + show_details = settings.show_detailed_description() + item_count_color = settings.get_label_color('itemCount') + + localize = context.localize + channel_role = localize(19029) # "Channel" + episode_count_label = localize('stats.itemCount') + video_count_label = localize('stats.videoCount') + podcast_label = context.localize('playlist.podcast') + untitled = localize('untitled') + path = context.get_path() - thumb_size = context.get_settings().get_thumbnail_size() - untitled = context.localize('untitled') + ui = context.get_ui() # if the path directs to a playlist of our own, set channel id to 'mine' if path.startswith(PATHS.MY_PLAYLISTS): @@ -289,22 +302,78 @@ def update_playlist_infos(provider, context, playlist_id_dict, playlist_item = playlist_id_dict[playlist_id] + is_podcast = yt_item.get('status', {}).get('podcastStatus') == 'enabled' + item_count_str, item_count = friendly_number( + yt_item.get('contentDetails', {}).get('itemCount', 0), + as_str=False, + ) + count_label = episode_count_label if is_podcast else video_count_label + + label_details = ' | '.join([item for item in ( + ui.bold('((○))') if is_podcast else '', + ui.color(item_count_color, item_count_str), + ) if item]) + + # Used for label2, but is poorly supported in skins + playlist_item.set_short_details(label_details) + # Hack to force a custom label mask containing production code, + # activated on sort order selection, to display details + # Refer XbmcContext.set_content for usage + playlist_item.set_production_code(label_details) + # title localised_info = snippet.get('localized') or {} title = localised_info.get('title') or snippet.get('title') or untitled playlist_item.set_name(title) - # plot + # channel name + channel_name = snippet.get('channelTitle') or untitled + playlist_item.add_artist(channel_name) + if 'cast' in channel_name_aliases: + playlist_item.add_cast(channel_name, role=channel_role) + if 'studio' in channel_name_aliases: + playlist_item.add_studio(channel_name) + + # plot with channel name, podcast status and item count description = strip_html_from_text(localised_info.get('description') or snippet.get('description') or '') + if show_details: + description = ''.join(( + ui.bold(channel_name, cr_after=1), + ui.bold(podcast_label) if is_podcast else '', + ' | ' if is_podcast else '', + ui.color( + item_count_color, + ui.bold(' '.join((item_count_str, + count_label.rstrip('s') + if item_count == 1 else + count_label))), + cr_after=1, + ), + ui.new_line(description, cr_after=1) if description else '', + 'https://youtu.be/playlist?list=' + playlist_id, + )) playlist_item.set_plot(description) + # date time + published_at = snippet.get('publishedAt') + if published_at: + datetime = datetime_parser.parse(published_at) + playlist_item.set_added_utc(datetime) + local_datetime = datetime_parser.utc_to_local(datetime) + playlist_item.set_date_from_datetime(local_datetime) + image = get_thumbnail(thumb_size, snippet.get('thumbnails')) playlist_item.set_image(image) - channel_id = 'mine' if in_my_playlists else snippet['channelId'] - channel_name = snippet.get('channelTitle', '') + # update channel mapping + channel_id = snippet.get('channelId', '') + playlist_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] = [] + channel_items_dict[channel_id].append(playlist_item) # play all videos of the playlist context_menu = [ @@ -399,7 +468,6 @@ def update_video_infos(provider, context, video_id_dict, else: watch_later_id = None - localize = context.localize settings = context.get_settings() alternate_player = settings.support_alternative_player() default_web_urls = settings.default_player_web_urls() @@ -412,7 +480,8 @@ def update_video_infos(provider, context, video_id_dict, thumb_stamp = get_thumb_timestamp() use_play_data = settings.use_local_history() - channel_role = localize(19029) + localize = context.localize + channel_role = localize(19029) # "Channel" untitled = localize('untitled') path = context.get_path() From 1573e0a5da4a080162f5f29e0943d4b54c547a92 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:26:15 +1100 Subject: [PATCH 11/12] Update display of channels Show the following details: - view count - subscriber count - video count - date - description - channel name in description - web url --- .../resource.language.en_gb/strings.po | 4 +- .../kodion/context/xbmc/xbmc_context.py | 1 + .../kodion/settings/abstract_settings.py | 2 + .../youtube_plugin/youtube/client/youtube.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 78 +++++++++++++++++-- 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 5fda428e8..b9b3ba928 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1262,7 +1262,7 @@ msgid "Videos" msgstr "" msgctxt "#30739" -msgid "" +msgid "Subscribers" msgstr "" msgctxt "#30740" @@ -1482,7 +1482,7 @@ msgid "Views count display colour" msgstr "" msgctxt "#30794" -msgid "Likes count display colour" +msgid "Subscriber/Likes count display colour" msgstr "" msgctxt "#30795" 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 0761fdd2a..c1023005d 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -213,6 +213,7 @@ class XbmcContext(AbstractContext): # 'stats.favoriteCount': 1036, 'stats.itemCount': 30737, 'stats.likeCount': 30733, + 'stats.subscriberCount': 30739, 'stats.videoCount': 30738, 'stats.viewCount': 30767, 'stream.alternate': 30747, diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 83e62be2e..81dbc3812 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -631,6 +631,8 @@ def set_history_playlist(self, value): if current_system_version.compatible(20): _COLOR_SETTING_MAP = { 'itemCount': 'commentCount', + 'subscriberCount': 'likeCount', + 'videoCount': 'commentCount', } def get_label_color(self, label_part): diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 48e30566f..005896c01 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -937,7 +937,7 @@ def get_channels(self, channel_id, **kwargs): if not isinstance(channel_id, string_type): channel_id = ','.join(channel_id) - params = {'part': 'snippet,contentDetails,brandingSettings'} + params = {'part': 'snippet,contentDetails,brandingSettings,statistics'} if channel_id != 'mine': params['id'] = channel_id else: diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 421763ef1..ab3068405 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -150,10 +150,18 @@ def update_channel_infos(provider, context, channel_id_dict, if subscription_id_dict is None: subscription_id_dict = {} - settings = context.get_settings() logged_in = provider.is_logged_in() + + settings = context.get_settings() + channel_name_aliases = settings.get_channel_name_aliases() + show_details = settings.show_detailed_description() + + localize = context.localize + channel_role = localize(19029) # "Channel" + untitled = localize('untitled') + path = context.get_path() - untitled = context.localize('untitled') + ui = context.get_ui() filter_list = None if path.startswith(PATHS.SUBSCRIPTIONS): @@ -183,17 +191,71 @@ def update_channel_infos(provider, context, channel_id_dict, channel_item = channel_id_dict[channel_id] - # title + label_stats = [] + stats = [] + if 'statistics' in yt_item: + for stat, value in yt_item['statistics'].items(): + label = context.LOCAL_MAP.get('stats.' + stat) + if not label: + continue + + str_value, value = friendly_number(value, as_str=False) + if not value: + continue + + color = settings.get_label_color(stat) + label = localize(label) + if value == 1: + label = label.rstrip('s') + + label_stats.append(ui.color(color, str_value)) + stats.append(ui.color(color, ui.bold(' '.join(( + str_value, label + ))))) + + label_stats = ' | '.join(label_stats) + stats = ' | '.join(stats) + + # Used for label2, but is poorly supported in skins + channel_item.set_short_details(label_stats) + # Hack to force a custom label mask containing production code, + # activated on sort order selection, to display details + # Refer XbmcContext.set_content for usage + channel_item.set_production_code(label_stats) + + # channel name and title localised_info = snippet.get('localized') or {} - title = localised_info.get('title') or snippet.get('title') or untitled - channel_item.set_name(title) + channel_name = (localised_info.get('title') + or snippet.get('title') + or untitled) + channel_item.set_name(channel_name) + channel_item.add_artist(channel_name) + if 'cast' in channel_name_aliases: + channel_item.add_cast(channel_name, role=channel_role) + if 'studio' in channel_name_aliases: + channel_item.add_studio(channel_name) # plot description = strip_html_from_text(localised_info.get('description') or snippet.get('description') or '') + if show_details: + description = ''.join(( + ui.bold(channel_name, cr_after=1), + ui.new_line(stats, cr_after=1) if stats else '', + ui.new_line(description, cr_after=1) if description else '', + 'https://youtu.be/channel' + channel_id, + )) channel_item.set_plot(description) + # date time + published_at = snippet.get('publishedAt') + if published_at: + datetime = datetime_parser.parse(published_at) + channel_item.set_added_utc(datetime) + local_datetime = datetime_parser.utc_to_local(datetime) + channel_item.set_date_from_datetime(local_datetime) + # image image = get_thumbnail(thumb_size, snippet.get('thumbnails')) channel_item.set_image(image) @@ -221,13 +283,13 @@ def update_channel_infos(provider, context, channel_id_dict, # add/remove from filter list if in_subscription_list and filter_list is not None: - channel = title.lower().replace(',', '') + channel = channel_name.lower().replace(',', '') context_menu.append( menu_items.remove_my_subscriptions_filter( - context, title + context, channel_name ) if channel in filter_list else menu_items.add_my_subscriptions_filter( - context, title + context, channel_name ) ) From 20975378cb8ce808e7ce2e3e6a91002133778e8e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:35:31 +1100 Subject: [PATCH 12/12] Version bump v7.1.1+beta.1 --- addon.xml | 2 +- changelog.txt | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 1fb75467d..c76fee920 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 2595977a9..7f6f6f6d4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,26 @@ +## v7.1.1+beta.1 +### Fixed +- Fix http server not listening on any interface if listen IP is 0.0.0.0 #927 + +### New +- Explicitly enable TCP keep alive #913 +- Add localised title and description for videos, channels and playlists +- Update display of playlists to show the following details: + - item count + - date + - channel name + - description + - web url + - podcast status +- Update display of channels to show the following details: + - view count + - subscriber count + - video count + - date + - description + - channel name in description + - web url + ## v7.1.0.1 ### Fixed - Fix logging/retry of sqlite3.OperationalError