diff --git a/addon.xml b/addon.xml index 76e5ff1d..6e21ffb9 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 6355f723..f5679c73 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,21 @@ +## v7.4.3+beta.1 +### Fixed +- Updates to resolve inconsistent Python2 string encoding/decoding +- Ensure parameters to Kodi builtin functions are parsed as string literals +- Allow trailing slashes in url of html pages served by internal http server + +### Changed +- Open confirmation dialog, with a link for further information, when sign in process fails +- Update API config page html +- Improve handling of API key settings changes +- Detect and notify when API requests cannot be completed +- Use consistent language for signing in/out + +### New +- Add button in API settings to view address of API config page +- Fallback to playing channel uploads if no live stream is available +- Add fallback method to load playlists when v3 API request cannot be made + ## v7.4.2 ### Fixed - Ensure updated context is used in client instance when rerouting to existing provider handler methods #1418 diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 5c7a6ed2..8c6a41cd 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -490,15 +490,15 @@ msgid "No videos found." msgstr "" msgctxt "#30546" -msgid "Please complete all login prompts" +msgid "Please sign in and complete all access authorisation prompts" msgstr "" msgctxt "#30547" -msgid "You may be prompted to login and enable access to multiple applications so that this addon can function properly." +msgid "You may be prompted to sign in and enable access to multiple applications so that this addon can function properly." msgstr "" msgctxt "#30548" -msgid "" +msgid "Authorisation process failed. Check the log for more information about this message.[CR][CR]Refer to the wiki for detailed setup instructions: [B]https://ytaddon.page.link/keys[/B]" msgstr "" msgctxt "#30549" @@ -826,7 +826,7 @@ msgid "IP whitelist (comma delimited)" msgstr "" msgctxt "#30630" -msgid "" +msgid "Check API configuration page address" msgstr "" msgctxt "#30631" @@ -1166,7 +1166,7 @@ msgid "Prefer automatically translated dubbed audio over original audio" msgstr "" msgctxt "#30715" -msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing-in via the addon and activating history tracking on YouTube." +msgid "Use YouTube internal list for Watch History?[CR][CR]Requires signing in via the addon and activating history tracking on YouTube." msgstr "" msgctxt "#30716" @@ -1178,7 +1178,7 @@ msgid "Disliked video" msgstr "" msgctxt "#30718" -msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing-in via the addon." +msgid "Use YouTube internal list for Watch Later?[CR][CR]Requires signing in via the addon." msgstr "" msgctxt "#30719" diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 6646ecdc..ea6fbe30 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -38,7 +38,6 @@ from .items import ( SearchHistoryItem, UriItem, ) -from .utils.convert_format import to_unicode class AbstractProvider(object): @@ -367,30 +366,32 @@ class AbstractProvider(object): if window_cache: ui.set_property(REROUTE_PATH, path) - action = ''.join(( - 'ReplaceWindow' if window_replace else 'ActivateWindow', - '(Videos,', - uri, - ',return)' if window_return else ')', - )) - timeout = 30 while ui.busy_dialog_active(): timeout -= 1 if timeout < 0: self.log.warning('Multiple busy dialogs active' ' - Rerouting workaround') - return UriItem('command://{0}'.format(action)) + defer = True + break context.sleep(0.1) else: - context.execute( - action, - # wait=True, - # wait_for=(REROUTE_PATH if window_cache else None), - # wait_for_set=False, - # block_ui=True, - ) - return True + defer = False + + action = context.create_uri( + uri, + window={ + 'name': 'Videos', + 'replace': window_replace, + 'return': window_return, + }, + command=defer, + ) + + if defer: + return UriItem(action) + context.execute(action) + return True @staticmethod def on_bookmarks(provider, context, re_match): @@ -413,7 +414,7 @@ class AbstractProvider(object): search_history = context.get_search_history() if not command or command == 'query': - query = to_unicode(params.get('q', '')) + query = params.get('q', '') if query: result, options = provider.on_search_run(context, query=query) if not options: @@ -429,7 +430,7 @@ class AbstractProvider(object): context.set_path(PATHS.SEARCH, command) if command == 'remove': - query = to_unicode(params.get('q', '')) + query = params.get('q', '') if not ui.on_yes_no_input( localize('content.remove'), localize('content.remove.check.x', query), @@ -443,7 +444,7 @@ class AbstractProvider(object): return True, {provider.FORCE_REFRESH: True} if command == 'rename': - query = to_unicode(params.get('q', '')) + query = params.get('q', '') result, new_query = ui.on_keyboard_input( localize('search.rename'), query ) diff --git a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py index 2ad0a112..d8e02fc4 100644 --- a/resources/lib/youtube_plugin/kodion/compatibility/__init__.py +++ b/resources/lib/youtube_plugin/kodion/compatibility/__init__.py @@ -6,6 +6,8 @@ SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. """ +from __future__ import absolute_import, division, unicode_literals + __all__ = ( 'BaseHTTPRequestHandler', @@ -13,7 +15,6 @@ __all__ = ( 'TCPServer', 'ThreadingMixIn', 'available_cpu_count', - 'byte_string_type', 'datetime_infolabel', 'entity_escape', 'generate_hash', @@ -25,6 +26,7 @@ __all__ = ( 'range_type', 'string_type', 'to_str', + 'to_unicode', 'unescape', 'unquote', 'unquote_plus', @@ -72,11 +74,16 @@ try: range_type = (range, list) - byte_string_type = bytes string_type = str to_str = str + def to_unicode(text): + if isinstance(text, bytes): + text = text.decode('utf-8', errors='ignore') + return text + + def entity_escape(text, entities=str.maketrans({ '&': '&', @@ -133,8 +140,8 @@ except ImportError: urlencode as _urlencode, ) from urlparse import ( - parse_qs, - parse_qsl, + parse_qs as _parse_qs, + parse_qsl as _parse_qsl, urljoin, urlsplit, urlunsplit, @@ -150,38 +157,103 @@ except ImportError: ) - def quote(data, *args, **kwargs): - return _quote(to_str(data), *args, **kwargs) - - - def quote_plus(data, *args, **kwargs): - return _quote_plus(to_str(data), *args, **kwargs) - - - def unquote(data): - return _unquote(to_str(data)) - - - def unquote_plus(data): - return _unquote_plus(to_str(data)) - - - def urlencode(data, *args, **kwargs): - if isinstance(data, dict): - data = data.items() - kwargs = { - key: value - for key, value in kwargs.viewitems() - if key in {'query', 'doseq'} - } - return _urlencode({ - to_str(key): ( - [to_str(part) for part in value] + def parse_qs(*args, **kwargs): + num_args = len(args) + query_dict = _parse_qs( + to_str(args[0] if args else kwargs.get('qs')), + keep_blank_values=( + args[1] + if num_args >= 2 else + kwargs.get('keep_blank_values', 0) + ), + strict_parsing=( + args[2] + if num_args >= 3 else + kwargs.get('strict_parsing', 0) + ), + # max_num_fields=( + # args[3] + # if num_args >= 4 else + # kwargs.get('max_num_fields', 0) + # ), + ) + return { + to_unicode(key): ( + [to_unicode(part) for part in value] if isinstance(value, (list, tuple)) else - to_str(value) + to_unicode(value) ) - for key, value in data - }, *args, **kwargs) + for key, value in query_dict.viewitems() + } + + + def parse_qsl(*args, **kwargs): + num_args = len(args) + query_list = _parse_qsl( + to_str(args[0] if args else kwargs.get('qs')), + keep_blank_values=( + args[1] + if num_args >= 2 else + kwargs.get('keep_blank_values', 0) + ), + strict_parsing=( + args[2] + if num_args >= 3 else + kwargs.get('strict_parsing', 0) + ), + # max_num_fields=( + # args[3] + # if num_args >= 4 else + # kwargs.get('max_num_fields', 0) + # ), + ) + return [ + (to_unicode(key), to_unicode(value)) + for key, value in query_list + ] + + + def quote(*args, **kwargs): + return _quote( + to_str(args[0] if args else kwargs.get('string')), + safe=args[1] if len(args) >= 2 else kwargs.get('safe', '/'), + ) + + + def quote_plus(*args, **kwargs): + return _quote_plus( + to_str(args[0] if args else kwargs.get('string')), + safe=args[1] if len(args) >= 2 else kwargs.get('safe', '/'), + ) + + + def unquote(*args, **kwargs): + return _unquote( + to_str(args[0] if args else kwargs.get('string')) + ) + + + def unquote_plus(*args, **kwargs): + return _unquote_plus( + to_str(args[0] if args else kwargs.get('string')) + ) + + + def urlencode(*args, **kwargs): + query = args[0] if args else kwargs.get('query') + if isinstance(query, dict): + query = query.viewitems() + return _urlencode( + query={ + to_str(key): ( + [to_str(part) for part in value] + if isinstance(value, (list, tuple)) else + to_str(value) + ) + for key, value in query + }, + doseq=args[1] if len(args) >= 2 else kwargs.get('doseq', 0), + ) class StringIO(_StringIO): @@ -205,18 +277,23 @@ except ImportError: range_type = (xrange, list) - byte_string_type = (bytes, str) string_type = basestring - def to_str(value, _format='{0!s}'.format): + def to_str(value, _format=b'{0!s}'.format): if not isinstance(value, basestring): value = _format(value) - if isinstance(value, unicode): - value = value.encode('utf-8') + elif isinstance(value, unicode): + value = value.encode('utf-8', errors='ignore') return value + def to_unicode(text): + if isinstance(text, (bytes, str)): + text = text.decode('utf-8', errors='ignore') + return text + + def entity_escape(text, entities={ '&': '&', @@ -231,7 +308,7 @@ except ImportError: def generate_hash(*args, **kwargs): - return md5(''.join( + return md5(b''.join( map(to_str, args or kwargs.get('iter')) )).hexdigest() diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index aaf57c31..84f02c24 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -115,6 +115,7 @@ PLAYBACK_STOPPED = 'playback_stopped' REFRESH_CONTAINER = 'refresh_container' RELOAD_ACCESS_MANAGER = 'reload_access_manager' SERVICE_IPC = 'service_ipc' +SYNC_API_KEYS = 'sync_api_keys' SYNC_LISTITEM = 'sync_listitem' # Sleep/wakeup states @@ -284,6 +285,7 @@ __all__ = ( 'REFRESH_CONTAINER', 'RELOAD_ACCESS_MANAGER', 'SERVICE_IPC', + 'SYNC_API_KEYS', 'SYNC_LISTITEM', # Sleep/wakeup states diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index cba0747c..6ae9acb8 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -377,6 +377,11 @@ class AbstractContext(object): play=None, window=None, command=False, + _window=(('name', ''), + ('refresh', ''), + ('replace', ''), + ('return', ''), + ('update', '')), **kwargs): if isinstance(path, (list, tuple)): uri = self.create_path(*path, is_uri=True) @@ -412,66 +417,69 @@ class AbstractContext(object): command = 'command://' if command else '' - if window: - if not isinstance(window, dict): - window = {} - if window.setdefault('refresh', False): - method = 'Container.Refresh(' - if not window.setdefault('replace', False): + if run == 'addon': + method = 'RunAddon' + elif run == 'script': + method = 'RunScript' + elif run: + method = 'RunPlugin' + elif play is not None: + method = 'PlayMedia' + kwargs['playlist_type_hint'] = play + elif isinstance(window, dict): + window = dict(_window, **window) + if window['refresh']: + method = 'Container.Refresh' + if not window['replace']: uri = '' - history_replace = False - window_return = False - elif window.setdefault('update', False): - method = 'Container.Update(' - history_replace = window.setdefault('replace', False) - window_return = False + window['name'] = '' + window['return'] = '' + window['replace'] = '' + elif window['update']: + method = 'Container.Update' + window['name'] = '' + window['return'] = '' + window['replace'] = ',replace' if window['replace'] else '' else: - history_replace = False - window_name = window.setdefault('name', 'Videos') - if window.setdefault('replace', False): - method = 'ReplaceWindow(%s,' % window_name - window_return = window.setdefault('return', False) + window['name'] = '"%s",' % (window['name'] or 'Videos') + if window['replace']: + method = 'ReplaceWindow' + window['return'] = '' else: - method = 'ActivateWindow(%s,' % window_name - window_return = window.setdefault('return', True) - return ''.join(( - command, - method, - uri, - ',return' if window_return else '', - ',replace' if history_replace else '', - ')' - )) + method = 'ActivateWindow' + window['return'] = ',return' if window['return'] else '' + window['replace'] = '' + + return ('{command}{method}(' + '{window[name]}' + '{uri}' + '{window[return]}' + '{window[replace]}' + ')').format( + command=command, + method=method, + uri=('"%s"' % uri) if uri else '', + window=window, + ) + else: + return uri kwargs = ',' + ','.join([ - '%s=%s' % (kwarg, value) + '"%s=%s"' % (kwarg, value) if value is not None else - kwarg + "%s" % kwarg for kwarg, value in kwargs.items() ]) if kwargs else '' - if run: - return ''.join(( - command, - 'RunAddon(' - if run == 'addon' else - 'RunScript(' - if run == 'script' else - 'RunPlugin(', - uri, - kwargs, - ')' - )) - if play is not None: - return ''.join(( - command, - 'PlayMedia(', - uri, - kwargs, - ',playlist_type_hint=', str(play), - ')', - )) - return uri + return ('{command}{method}(' + '"{uri}"' + '{kwargs}' + ')').format( + command=command, + method=method, + uri=uri, + kwargs=kwargs, + ) def get_parent_uri(self, **kwargs): return self.create_uri(self._path_parts[:-1], **kwargs) 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 4d724dde..683988dc 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -47,7 +47,6 @@ from ...json_store import APIKeyStore, AccessManager from ...player import XbmcPlaylistPlayer from ...settings import XbmcPluginSettings from ...ui import XbmcContextUI -from ...utils.convert_format import to_unicode from ...utils.file_system import make_dirs from ...utils.methods import ( get_kodi_setting_bool, @@ -318,6 +317,7 @@ class XbmcContext(AbstractContext): 'sign.out': 30112, 'sign.multi.text': 30547, 'sign.multi.title': 30546, + 'sign.multi.failed': 30548, 'start': 335, 'stats.commentCount': 30732, # 'stats.favoriteCount': 1036, @@ -469,7 +469,7 @@ class XbmcContext(AbstractContext): def init(self): num_args = len(sys.argv) if num_args: - uri = to_unicode(sys.argv[0]) + uri = sys.argv[0] if uri.startswith('plugin://'): self._plugin_handle = int(sys.argv[1]) else: @@ -493,7 +493,7 @@ class XbmcContext(AbstractContext): # after that try to get the params if num_args > 2: - _params = to_unicode(sys.argv[2][1:]) + _params = sys.argv[2][1:] if _params: self._param_string = _params params.update(dict(parse_qsl(_params, keep_blank_values=True))) @@ -726,7 +726,7 @@ class XbmcContext(AbstractContext): else: self.log.warning(msg, text_id=text_id) return default_text - result = to_unicode(result) + result = result if _args: if localize_args: @@ -1092,18 +1092,17 @@ class XbmcContext(AbstractContext): ) return value - def is_plugin_folder(self, folder_path='', name=False): + def is_plugin_folder(self, folder_path='', name=False, partial=True): if name: return XbmcContextUI.get_container_info( FOLDER_NAME, - container_id=None, ) == self._plugin_name return self.is_plugin_path( - XbmcContextUI.get_container_info( + uri=XbmcContextUI.get_container_info( FOLDER_URI, - container_id=None, ), - folder_path, + uri_path=folder_path, + partial=partial, ) def refresh_requested(self, force=False, on=False, off=False, params=None): diff --git a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py index 2e99fc38..ac816db3 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/api_keys.py +++ b/resources/lib/youtube_plugin/kodion/json_store/api_keys.py @@ -262,65 +262,83 @@ class APIKeyStore(JSONStore): data['keys']['developer'][developer_id] = new_config return self.save(data) - def sync(self): + def sync(self, update_store=False, update_settings=False): api_data = self.get_data() settings = self._context.get_settings() - update_saved_values = False - update_settings_values = False - - saved_details = ( + forced = update_store + stored_values = ( api_data['keys']['user'].get('api_key', ''), api_data['keys']['user'].get('client_id', ''), api_data['keys']['user'].get('client_secret', ''), ) - if all(saved_details): - update_settings_values = True - # users are now pasting keys into api_keys.json - # try stripping whitespace and domain suffix from API details - # and save the results if they differ - stripped_details = self.strip_details(*saved_details) - if all(stripped_details) and saved_details != stripped_details: - saved_details = stripped_details - api_data['keys']['user'] = { - 'api_key': saved_details[0], - 'client_id': saved_details[1], - 'client_secret': saved_details[2], - } - update_saved_values = True + _stored_values = self.strip_details(*stored_values) + all_stored = all(stored_values) - setting_details = ( + settings_values = ( settings.api_key(), settings.api_id(), settings.api_secret(), ) - if all(setting_details): - update_settings_values = False - stripped_details = self.strip_details(*setting_details) - if all(stripped_details) and setting_details != stripped_details: - setting_details = ( - settings.api_key(stripped_details[0]), - settings.api_id(stripped_details[1]), - settings.api_secret(stripped_details[2]), - ) + _settings_values = self.strip_details(*settings_values) + all_settings = all(settings_values) - if saved_details != setting_details: - api_data['keys']['user'] = { - 'api_key': setting_details[0], - 'client_id': setting_details[1], - 'client_secret': setting_details[2], - } - update_saved_values = True + if all_stored: + sync_to_settings = ( + not update_store + and _settings_values != _stored_values + ) + else: + sync_to_settings = update_settings - if update_settings_values: - settings.api_key(saved_details[0]) - settings.api_id(saved_details[1]) - settings.api_secret(saved_details[2]) + if all_settings: + sync_to_store = ( + not update_settings + and _stored_values != _settings_values + ) + else: + sync_to_store = update_store - if update_saved_values: - self.save(api_data) - return True - return False + update_settings = all_settings and settings_values != _settings_values + update_store = all_stored and stored_values != _stored_values + + if sync_to_settings: + settings.api_key(_stored_values[0]) + settings.api_id(_stored_values[1]) + settings.api_secret(_stored_values[2]) + + elif update_settings: + settings.api_key(_settings_values[0]) + settings.api_id(_settings_values[1]) + settings.api_secret(_settings_values[2]) + + elif sync_to_store: + api_data = { + 'keys': { + 'user': { + 'api_key': _settings_values[0], + 'client_id': _settings_values[1], + 'client_secret': _settings_values[2], + }, + }, + } + self.save(api_data, update=True, ipc=not forced) + + elif update_store: + api_data = { + 'keys': { + 'user': { + 'api_key': _stored_values[0], + 'client_id': _stored_values[1], + 'client_secret': _stored_values[2], + }, + }, + } + self.save(api_data, update=True, ipc=not forced) + + else: + return False + return True def update(self): context = self._context diff --git a/resources/lib/youtube_plugin/kodion/json_store/json_store.py b/resources/lib/youtube_plugin/kodion/json_store/json_store.py index 194cb8e6..d0271184 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/json_store.py +++ b/resources/lib/youtube_plugin/kodion/json_store/json_store.py @@ -15,8 +15,8 @@ import errno from io import open from .. import logging +from ..compatibility import to_unicode from ..constants import DATA_PATH, FILE_READ, FILE_WRITE -from ..utils.convert_format import to_unicode from ..utils.file_system import make_dirs from ..utils.methods import merge_dicts diff --git a/resources/lib/youtube_plugin/kodion/logging.py b/resources/lib/youtube_plugin/kodion/logging.py index 457b3be4..a90cfb10 100644 --- a/resources/lib/youtube_plugin/kodion/logging.py +++ b/resources/lib/youtube_plugin/kodion/logging.py @@ -18,9 +18,9 @@ from string import Formatter from sys import exc_info as sys_exc_info from traceback import extract_stack, format_list -from .compatibility import StringIO, string_type, to_str, xbmc +from .compatibility import StringIO, string_type, to_str, to_unicode, xbmc from .constants import ADDON_ID -from .utils.convert_format import to_unicode, urls_in_text +from .utils.convert_format import urls_in_text from .utils.redact import ( parse_and_redact_uri, redact_auth_header, diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index b309ce53..6cb6265c 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -37,6 +37,7 @@ from ..constants import ( RESUMABLE, SERVER_WAKEUP, SERVICE_IPC, + SYNC_API_KEYS, SYNC_LISTITEM, VIDEO_ID, ) @@ -54,6 +55,7 @@ class ServiceMonitor(xbmc.Monitor): def __init__(self, context): self._context = context + self._api_values = ('', '', '') self._httpd_address = None self._httpd_port = None self._whitelist = None @@ -178,6 +180,8 @@ class ServiceMonitor(xbmc.Monitor): elif target == SERVER_WAKEUP: if self.httpd_required(): response = self.start_httpd() + elif data.get('force'): + response = self.restart_httpd() else: response = bool(self.httpd) if self.httpd_sleep_allowed: @@ -260,6 +264,9 @@ class ServiceMonitor(xbmc.Monitor): self._context.reload_access_manager() self.refresh_container() + elif event == SYNC_API_KEYS: + self.onSettingsChanged(force=True) + elif event == PLAYBACK_STOPPED: if data: data = json.loads(data) @@ -315,6 +322,7 @@ class ServiceMonitor(xbmc.Monitor): def onSettingsChanged(self, force=False): context = self._context + ui = context.get_ui() if force: self._settings_collect = False @@ -352,7 +360,17 @@ class ServiceMonitor(xbmc.Monitor): self.log.stack_info = False self.log.verbose_logging = False - context.get_ui().set_property(CHECK_SETTINGS) + api_values = ( + settings.api_key(), + settings.api_id(), + settings.api_secret(), + ) + if api_values != self._api_values: + context.get_api_store().sync(update_store=True) + self._api_values = api_values + ui.set_property(SYNC_API_KEYS) + + ui.set_property(CHECK_SETTINGS) self.refresh_container() httpd_started = bool(self.httpd) @@ -452,7 +470,7 @@ class ServiceMonitor(xbmc.Monitor): ip=self._httpd_address, port=self._httpd_port) self.shutdown_httpd(terminate=True) - self.start_httpd() + return self.start_httpd() def ping_httpd(self): return self.httpd and httpd_status(self._context) @@ -465,7 +483,7 @@ class ServiceMonitor(xbmc.Monitor): self._use_httpd = required elif self._httpd_error: - required = False + required = None elif on_idle: settings = self._context.get_settings() diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index 09ce1058..b3d60165 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -28,6 +28,7 @@ from ..compatibility import ( BaseHTTPRequestHandler, TCPServer, ThreadingMixIn, + parse_qs, urlencode, urlsplit, urlunsplit, @@ -38,6 +39,7 @@ from ..constants import ( LICENSE_TOKEN, LICENSE_URL, PATHS, + SYNC_API_KEYS, TEMP_PATH, ) from ..utils.convert_format import fix_subtitle_stream @@ -231,7 +233,7 @@ class RequestHandler(BaseHTTPRequestHandler, object): parts, params, log_uri, log_params, log_path = parse_and_redact_uri(uri) path = { 'uri': uri, - 'path': parts.path, + 'path': parts.path.rstrip('/'), 'query': parts.query, 'params': params, 'log_uri': log_uri, @@ -265,10 +267,7 @@ class RequestHandler(BaseHTTPRequestHandler, object): return context = self._context - localize = context.localize - settings = context.get_settings() - api_config_enabled = settings.api_config_page() empty = [None] @@ -306,7 +305,7 @@ class RequestHandler(BaseHTTPRequestHandler, object): .format(uri=path['log_uri'], file_path=file_path)) self.send_error(404, response) - elif api_config_enabled and path['path'] == PATHS.API: + elif path['path'] == PATHS.API and settings.api_config_page(): html = self.api_config_page() html = html.encode('utf-8') @@ -318,64 +317,6 @@ class RequestHandler(BaseHTTPRequestHandler, object): for chunk in self._get_chunks(html): self.wfile.write(chunk) - elif api_config_enabled and path['path'].startswith(PATHS.API_SUBMIT): - xbmc.executebuiltin('Dialog.Close(addonsettings,true)') - - query = path['query'] - params = path['params'] - updated = [] - - api_key = params.get('api_key', empty)[0] - api_id = params.get('api_id', empty)[0] - api_secret = params.get('api_secret', empty)[0] - # Bookmark this page - if api_key and api_id and api_secret: - footer = localize('api.config.bookmark') - else: - footer = '' - - if re.search(r'api_key=(?:&|$)', query): - api_key = '' - if re.search(r'api_id=(?:&|$)', query): - api_id = '' - if re.search(r'api_secret=(?:&|$)', query): - api_secret = '' - - if api_key is not None and api_key != settings.api_key(): - settings.api_key(new_key=api_key) - updated.append(localize('api.key')) - - if api_id is not None and api_id != settings.api_id(): - settings.api_id(new_id=api_id) - updated.append(localize('api.id')) - - if api_secret is not None and api_secret != settings.api_secret(): - settings.api_secret(new_secret=api_secret) - updated.append(localize('api.secret')) - - if api_key and api_id and api_secret: - enabled = localize('api.personal.enabled') - else: - enabled = localize('api.personal.disabled') - - if updated: - # Successfully updated - updated = localize('api.config.updated', ', '.join(updated)) - else: - # No changes, not updated - updated = localize('api.config.not_updated') - - html = self.api_submit_page(updated, enabled, footer) - html = html.encode('utf-8') - - self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Content-Length', str(len(html))) - self.end_headers() - - for chunk in self._get_chunks(html): - self.wfile.write(chunk) - elif path['path'] == PATHS.PING: self.send_error(204) @@ -692,7 +633,70 @@ class RequestHandler(BaseHTTPRequestHandler, object): self.send_error(403) return - if path['path'].startswith(PATHS.DRM): + context = self._context + settings = context.get_settings() + localize = context.localize + + empty = [None] + + if path['path'] == PATHS.API_SUBMIT and settings.api_config_page(): + xbmc.executebuiltin('Dialog.Close(addonsettings,true)') + + length = int(self.headers['Content-Length']) + post_data = self.rfile.read(length) + + query = post_data.decode('utf-8', 'ignore') + params = parse_qs(query, keep_blank_values=True) + updated = [] + + api_key = params.get('api_key', empty)[0] + api_id = params.get('api_id', empty)[0] + api_secret = params.get('api_secret', empty)[0] + + if re.search(r'api_key=(?:&|$)', query): + api_key = '' + if re.search(r'api_id=(?:&|$)', query): + api_id = '' + if re.search(r'api_secret=(?:&|$)', query): + api_secret = '' + + if api_key is not None and api_key != settings.api_key(): + settings.api_key(new_key=api_key) + updated.append(localize('api.key')) + + if api_id is not None and api_id != settings.api_id(): + settings.api_id(new_id=api_id) + updated.append(localize('api.id')) + + if api_secret is not None and api_secret != settings.api_secret(): + settings.api_secret(new_secret=api_secret) + updated.append(localize('api.secret')) + + if api_key and api_id and api_secret: + enabled = localize('api.personal.enabled') + else: + enabled = localize('api.personal.disabled') + + if updated: + context.send_notification(SYNC_API_KEYS) + # Successfully updated + updated = localize('api.config.updated', ', '.join(updated)) + else: + # No changes, not updated + updated = localize('api.config.not_updated') + + html = self.api_submit_page(updated, enabled) + html = html.encode('utf-8') + + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Content-Length', str(len(html))) + self.end_headers() + + for chunk in self._get_chunks(html): + self.wfile.write(chunk) + + elif path['path'].startswith(PATHS.DRM): ui = self._context.get_ui() lic_url = ui.get_property(LICENSE_URL) @@ -814,12 +818,14 @@ class RequestHandler(BaseHTTPRequestHandler, object): api_key_value=api_key, api_secret_value=api_secret, submit=localize('api.config.save'), + action_url=PATHS.API_SUBMIT, header=localize('api.config'), + footer=localize('api.config.bookmark'), ) return html @classmethod - def api_submit_page(cls, updated_keys, enabled, footer): + def api_submit_page(cls, updated_keys, enabled): localize = cls._context.localize html = Pages.api_submit.get('html') css = Pages.api_submit.get('css') @@ -828,7 +834,6 @@ class RequestHandler(BaseHTTPRequestHandler, object): title=localize('api.config'), updated=updated_keys, enabled=enabled, - footer=footer, header=localize('api.config'), ) return html @@ -842,31 +847,34 @@ class Pages(object): - {{title}} - + {title} +
-
{{header}}
-
+
{header}
+ - +
+

+ {footer} +

- '''.format(action_url=PATHS.API_SUBMIT)), + '''), 'css': ''.join('\t\t\t'.expandtabs(2) + line for line in dedent(''' body { background: #141718; @@ -878,7 +886,6 @@ class Pages(object): } .config_form { width: 575px; - height: 145px; font-size: 16px; background: #1a2123; padding: 30px 30px 15px 30px; @@ -906,7 +913,7 @@ class Pages(object): color: #fff; } .config_form label { - display:block; + display: inline-block; margin-bottom: 10px; } .config_form label > span { @@ -927,18 +934,38 @@ class Pages(object): } .config_form input[type=submit], .config_form input[type=button] { + display: block; width: 150px; background: #141718; border: 1px solid #147a96; padding: 8px 0px 8px 10px; border-radius: 5px; color: #fff; - margin-top: 10px + margin: 10px auto; } .config_form input[type=submit]:hover, .config_form input[type=button]:hover { background: #0f84a5; } + .text_center { + margin: 2em auto auto; + width: 600px; + padding: 10px; + text-align: center; + } + p { + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + color: #fff; + float: left; + width: 575px; + margin: 0.5em auto; + } + small { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + color: #fff; + } ''').splitlines(True)) + '\t\t'.expandtabs(2) } @@ -958,9 +985,6 @@ class Pages(object):

{updated}

{enabled}

-

- {footer} -

@@ -975,15 +999,8 @@ class Pages(object): width: 600px; padding: 10px; } - .text_center { - margin: 2em auto auto; - width: 600px; - padding: 10px; - text-align: center; - } .content { width: 575px; - height: 145px; background: #1a2123; padding: 30px 30px 15px 30px; border: 5px solid #1a2123; @@ -1008,11 +1025,6 @@ class Pages(object): width: 575px; margin: 0.5em auto; } - small { - font-family: Arial, Helvetica, sans-serif; - font-size: 12px; - color: #fff; - } ''').splitlines(True)) + '\t\t'.expandtabs(2) } @@ -1037,7 +1049,7 @@ def get_http_server(address, port, context): return None -def httpd_status(context, address=None): +def httpd_status(context, address=None, path=None, query=None): netloc = get_connect_address(context, as_netloc=True, address=address) url = urlunsplit(( 'http', @@ -1054,6 +1066,14 @@ def httpd_status(context, address=None): with response: result = response.status_code if result == 204: + if path: + return urlunsplit(( + 'http', + netloc, + path, + query or '', + '', + )) return True logging.debug(('Ping', @@ -1064,14 +1084,15 @@ def httpd_status(context, address=None): return False -def get_client_ip_address(context): - url = urlunsplit(( - 'http', - get_connect_address(context, as_netloc=True), - PATHS.IP, - '', - '', - )) +def get_client_ip_address(context, url=None): + if url is None: + url = urlunsplit(( + 'http', + get_connect_address(context, as_netloc=True), + PATHS.IP, + '', + '', + )) if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=context) response = RequestHandler.requests.request(url, cache=False) 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 cb606576..b9ed2239 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -35,6 +35,7 @@ from ...constants import ( REROUTE_PATH, SORT_DIR, SORT_METHOD, + SYNC_API_KEYS, SYNC_LISTITEM, TRAKT_PAUSE_FLAG, VIDEO_ID, @@ -197,6 +198,9 @@ class XbmcPlugin(AbstractPlugin): if ui.get_property(PLUGIN_SLEEPING): context.ipc_exec(PLUGIN_WAKEUP) + if ui.pop_property(SYNC_API_KEYS): + context.get_api_store().sync(update_store=True) + if ui.pop_property(RELOAD_ACCESS_MANAGER): context.reload_access_manager() diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index ba43b0ac..a2b3330a 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -17,6 +17,7 @@ from .constants import ( DATA_PATH, DEFAULT_LANGUAGES, DEFAULT_REGIONS, + PATHS, RELOAD_ACCESS_MANAGER, SERVER_WAKEUP, TEMP_PATH, @@ -114,9 +115,10 @@ def _config_actions(context, action, *_args): settings.httpd_listen(addresses[selected_address]) elif action == 'show_client_ip': - context.ipc_exec(SERVER_WAKEUP, timeout=5) - if httpd_status(context): - client_ip = get_client_ip_address(context) + context.ipc_exec(SERVER_WAKEUP, timeout=5, payload={'force': True}) + url = httpd_status(context, path=PATHS.IP) + if url: + client_ip = get_client_ip_address(context, url) if client_ip: ui.on_ok(context.get_name(), context.localize('client.ip.is.x', client_ip)) @@ -125,6 +127,15 @@ def _config_actions(context, action, *_args): else: ui.show_notification(context.localize('httpd.not.running')) + elif action == 'show_api_config_page_address': + context.ipc_exec(SERVER_WAKEUP, timeout=5, payload={'force': True}) + url = httpd_status(context, path=PATHS.API) + if url: + ui.on_ok(context.localize('api.config'), + context.localize('go.to.x', ui.bold(url))) + else: + ui.show_notification(context.localize('httpd.not.running')) + elif action == 'geo_location': locator = Locator(context) locator.locate_requester() diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index 11b9e465..820c7d2a 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -40,7 +40,6 @@ from ...constants import ( UPDATING, URI, ) -from ...utils.convert_format import to_unicode class XbmcContextUI(AbstractContextUI): @@ -90,13 +89,9 @@ class XbmcContextUI(AbstractContextUI): def on_keyboard_input(title, default='', hidden=False): # Starting with Gotham (13.X > ...) dialog = xbmcgui.Dialog() - result = dialog.input(title, - to_unicode(default), - type=xbmcgui.INPUT_ALPHANUM) + result = dialog.input(title, default, type=xbmcgui.INPUT_ALPHANUM) if result: - text = to_unicode(result) - return True, text - + return True, result return False, '' @staticmethod @@ -105,7 +100,6 @@ class XbmcContextUI(AbstractContextUI): result = dialog.input(title, str(default), type=xbmcgui.INPUT_NUMERIC) if result: return True, int(result) - return False, None @staticmethod @@ -121,19 +115,19 @@ class XbmcContextUI(AbstractContextUI): def on_remove_content(self, name): return self.on_yes_no_input( self._context.localize('content.remove'), - self._context.localize('content.remove.check.x', to_unicode(name)), + self._context.localize('content.remove.check.x', name), ) def on_delete_content(self, name): return self.on_yes_no_input( self._context.localize('content.delete'), - self._context.localize('content.delete.check.x', to_unicode(name)), + self._context.localize('content.delete.check.x', name), ) def on_clear_content(self, name): return self.on_yes_no_input( self._context.localize('content.clear'), - self._context.localize('content.clear.check.x', to_unicode(name)), + self._context.localize('content.clear.check.x', name), ) @staticmethod @@ -269,7 +263,7 @@ class XbmcContextUI(AbstractContextUI): except (TypeError, ValueError): return - xbmc.executebuiltin('SetFocus({0},{1},absolute)'.format( + xbmc.executebuiltin('SetFocus("{0}","{1}",absolute)'.format( container_id, position + offset, )) diff --git a/resources/lib/youtube_plugin/kodion/utils/convert_format.py b/resources/lib/youtube_plugin/kodion/utils/convert_format.py index cbc41624..515cf5eb 100644 --- a/resources/lib/youtube_plugin/kodion/utils/convert_format.py +++ b/resources/lib/youtube_plugin/kodion/utils/convert_format.py @@ -14,7 +14,15 @@ from datetime import timedelta from math import floor, log from re import DOTALL, compile as re_compile -from ..compatibility import byte_string_type + +__all__ = ( + 'channel_filter_split', + 'custom_filter_split', + 'fix_subtitle_stream', + 'friendly_number', + 'strip_html_from_text', + 'urls_in_text', +) __RE_URL = re_compile(r'(https?://\S+)') @@ -26,15 +34,6 @@ def urls_in_text(text, process=None, count=0): return __RE_URL.findall(text) -def to_unicode(text): - if isinstance(text, byte_string_type): - try: - return text.decode('utf-8', 'ignore') - except UnicodeError: - pass - return text - - def strip_html_from_text(text, tag_re=re_compile('<[^<]+?>')): """ Removes html tags diff --git a/resources/lib/youtube_plugin/kodion/utils/redact.py b/resources/lib/youtube_plugin/kodion/utils/redact.py index ecd087ff..c5fbcaac 100644 --- a/resources/lib/youtube_plugin/kodion/utils/redact.py +++ b/resources/lib/youtube_plugin/kodion/utils/redact.py @@ -181,7 +181,9 @@ def parse_and_redact_uri(uri, redact_only=False): params = parse_qs(parts.query, keep_blank_values=True) headers = params.get('__headers', [None])[0] if headers: - params['__headers'] = [urlsafe_b64decode(headers).decode('utf-8')] + params['__headers'] = [ + urlsafe_b64decode(headers.encode('utf-8')).decode('utf-8') + ] log_params = redact_params(params) log_query = urlencode(log_params, doseq=True) else: diff --git a/resources/lib/youtube_plugin/youtube/client/data_client.py b/resources/lib/youtube_plugin/youtube/client/data_client.py index 01e0e742..5fe96d59 100644 --- a/resources/lib/youtube_plugin/youtube/client/data_client.py +++ b/resources/lib/youtube_plugin/youtube/client/data_client.py @@ -2976,6 +2976,13 @@ class YouTubeDataClient(YouTubeLoginClient): v3_response['_item_filter'] = item_filter return v3_response + @classmethod + def v3_api_available(cls): + user_config = cls._configs.get('user') + if user_config: + return bool(user_config.get('key')) + return False + def _auth_required(self, params): if params: if params.get('mine') or params.get('forMine'): @@ -3105,15 +3112,30 @@ class YouTubeDataClient(YouTubeLoginClient): do_auth=None, cache=None, **kwargs): + context = self._context + + if client == 'v3' and not self.v3_api_available(): + abort = True + abort_msg = 'Request skipped: API key not provided' + abort_prompt = 'key.requirement' + else: + abort = False + abort_msg = None + abort_prompt = None + if not client_data: client_data = {} - client_data.setdefault('method', method) + if path: client_data['_endpoint'] = path.strip('/') + if url: client_data['url'] = url + if headers: client_data['headers'] = headers + + client_data.setdefault('method', method) if method in {'POST', 'PUT'}: if post_data: client_data['json'] = post_data @@ -3124,16 +3146,18 @@ class YouTubeDataClient(YouTubeLoginClient): if do_auth is None and method == 'DELETE': do_auth = True clear_data = True + if params: client_data['params'] = params if do_auth is None: do_auth = self._auth_required(params) if do_auth: - abort = not self.logged_in + if not self.logged_in: + abort = True + abort_msg = 'Request skipped: Authorisation required' + abort_prompt = 'sign.multi.title' client_data.setdefault('_auth_required', do_auth) - else: - abort = False client_data['_access_tokens'] = access_tokens = {} client_data['_api_keys'] = api_keys = {} @@ -3161,17 +3185,18 @@ class YouTubeDataClient(YouTubeLoginClient): params = client.get('params') if params and 'key' in params: key = params['key'] - if key: - abort = False - elif not client['_has_auth']: + if not key and not client['_has_auth']: abort = True + abort_msg = 'Request skipped: API key not provided' + abort_prompt = 'key.requirement' else: client_data.setdefault('_name', client) client = client_data params = client.get('params') abort = True + abort_msg = 'Request skipped: Invalid or disabled client' + abort_prompt = None - context = self._context self.log.debug(('{request_name} API request', 'method: {method!r}', 'path: {path!u}', @@ -3185,18 +3210,18 @@ class YouTubeDataClient(YouTubeLoginClient): data=client.get('json'), headers=client.get('headers'), stacklevel=2) + if abort: - if kwargs.get('notify', True): - context.get_ui().on_ok( - context.get_name(), - context.localize('key.requirement'), - ) - self.log.warning('Aborted', stacklevel=2) + if abort_prompt and kwargs.get('notify', True): + context.get_ui().on_ok(context.get_name(), abort_prompt) + self.log.warning(abort_msg, stacklevel=2) return {} + if cache is None and 'no_content' in kwargs: cache = False - elif cache is not False and self._context.refresh_requested(): + elif cache is not False and context.refresh_requested(): cache = 'refresh' + return self.request(response_hook=self._request_response_hook, event_hook_kwargs=kwargs, error_hook=self._request_error_hook, diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 9bb034c5..ee28c7e4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -61,15 +61,18 @@ class ResourceManager(object): def get_channels(self, ids, suppress_errors=False, defer_cache=False): context = self._context client = self._client - data_cache = context.get_data_cache() + function_cache = context.get_function_cache() refresh = context.refresh_requested() - forced_cache = not function_cache.run( - client.internet_available, - function_cache.ONE_MINUTE * 5, - _refresh=refresh, - ) + if not client.v3_api_available(): + forced_cache = True + else: + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) refresh = not forced_cache and refresh updated = [] @@ -97,6 +100,7 @@ class ResourceManager(object): if refresh or not ids: result = {} else: + data_cache = context.get_data_cache() result = data_cache.get_items( ids, None if forced_cache else data_cache.ONE_DAY, @@ -168,14 +172,17 @@ class ResourceManager(object): defer_cache=False): context = self._context client = self._client - function_cache = context.get_function_cache() refresh = context.refresh_requested() - forced_cache = not function_cache.run( - client.internet_available, - function_cache.ONE_MINUTE * 5, - _refresh=refresh, - ) + if not client.v3_api_available(): + forced_cache = True + else: + function_cache = context.get_function_cache() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) refresh = not forced_cache and refresh if not refresh and channel_data: @@ -289,14 +296,17 @@ class ResourceManager(object): context = self._context client = self._client - function_cache = context.get_function_cache() refresh = context.refresh_requested() - forced_cache = not function_cache.run( - client.internet_available, - function_cache.ONE_MINUTE * 5, - _refresh=refresh, - ) + if not client.v3_api_available(): + forced_cache = True + else: + function_cache = context.get_function_cache() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) refresh = not forced_cache and refresh if refresh or not ids: @@ -379,18 +389,18 @@ class ResourceManager(object): context = self._context client = self._client - function_cache = context.get_function_cache() refresh = context.refresh_requested() - forced_cache = ( - not function_cache.run( - client.internet_available, - function_cache.ONE_MINUTE * 5, - _refresh=refresh, - ) - or (context.get_param(CHANNEL_ID) == 'mine' - and not client.logged_in) - ) + v3_api_available = client.v3_api_available() + if not client.logged_in and context.get_param(CHANNEL_ID) == 'mine': + forced_cache = True + else: + function_cache = context.get_function_cache() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) refresh = not forced_cache and refresh if batch_id: @@ -467,7 +477,18 @@ class ResourceManager(object): batch_id = (playlist_id, page_token) if batch_id in result: break - batch = client.get_playlist_items(*batch_id, **kwargs) + batch = ( + client.get_playlist_items(*batch_id, **kwargs) + if v3_api_available else + client.get_browse_items( + browse_id='VL' + playlist_id, + playlist_id=playlist_id, + page_token=page_token, + response_type='playlistItems', + client='tv', + json_path=client.JSON_PATHS['tv_playlist'], + ) + ) if not batch: break new_batch_ids.append(batch_id) @@ -564,14 +585,17 @@ class ResourceManager(object): context = self._context client = self._client - function_cache = context.get_function_cache() refresh = context.refresh_requested() - forced_cache = not function_cache.run( - client.internet_available, - function_cache.ONE_MINUTE * 5, - _refresh=refresh, - ) + if not client.v3_api_available(): + forced_cache = True + else: + function_cache = context.get_function_cache() + forced_cache = not function_cache.run( + client.internet_available, + function_cache.ONE_MINUTE * 5, + _refresh=refresh, + ) refresh = not forced_cache and refresh if refresh or not ids: diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index e2139a1f..7dc8121f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -18,9 +18,10 @@ SIGN_IN = 'in' SIGN_OUT = 'out' -def _do_logout(provider, context, client=None, **kwargs): - ui = context.get_ui() - if not context.get_param('confirmed') and not ui.on_yes_no_input( +def _do_logout(provider, context, client=None, confirmed=None, **kwargs): + if confirmed is None: + confirmed = context.pop_param('confirmed', False) + if not confirmed and not context.get_ui().on_yes_no_input( context.localize('sign.out'), context.localize('are_you_sure') ): @@ -158,7 +159,8 @@ def _do_login(provider, context, client=None, **kwargs): context.sleep(interval) except LoginException: - _do_logout(provider, context, client=client) + ui.on_ok(context.get_name(), localize('sign.multi.failed')) + _do_logout(provider, context, client=client, confirmed=True) break finally: new_access_tokens[token_type] = new_token[0] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index c801a9ee..78fd0f8c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -201,6 +201,10 @@ def _play_playlist(provider, context): playlist_id = params.get(PLAYLIST_ID) if playlist_id: playlist_ids = [playlist_id] + else: + channel_id = params.get(CHANNEL_ID) + if channel_id and channel_id.startswith('UC'): + playlist_ids = [channel_id.replace('UC', 'UU', 1)] video_ids = params.get(VIDEO_IDS) if not playlist_ids and not video_ids: @@ -284,6 +288,9 @@ def _play_channel_live(provider, context): if not json_data: return False + if not json_data.get('items'): + return _play_playlist(provider, context) + channel_streams = v3.response_to_items(provider, context, json_data, diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 2544cdf4..2d3e4c3c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -12,11 +12,10 @@ from __future__ import absolute_import, division, unicode_literals import os -from ...kodion.compatibility import urlencode, xbmcvfs +from ...kodion.compatibility import to_unicode, urlencode, xbmcvfs from ...kodion.constants import ADDON_ID, DATA_PATH, WAIT_END_FLAG from ...kodion.network import get_listen_addresses, httpd_status from ...kodion.sql_store import PlaybackHistory, SearchHistory -from ...kodion.utils.convert_format import to_unicode from ...kodion.utils.datetime import since_epoch, strptime diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index a8114ade..5edbbb63 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -64,7 +64,6 @@ from ..kodion.items import ( from ..kodion.utils.convert_format import ( channel_filter_split, strip_html_from_text, - to_unicode, ) from ..kodion.utils.datetime import now, since_epoch @@ -145,7 +144,6 @@ class Provider(AbstractProvider): settings = context.get_settings() user = access_manager.get_current_user() - api_last_origin = access_manager.get_last_origin() client = self._client if not client or not client.initialised: @@ -222,16 +220,8 @@ class Provider(AbstractProvider): _, ) = access_manager.get_access_tokens(dev_id) - if client and not client.context_changed(context): - if api_last_origin != origin: - access_manager.set_last_origin(origin) - self.log.info(('API key origin changed - Resetting client', - 'Previous: {old!r}', - 'Current: {new!r}'), - old=api_last_origin, - new=origin) - client.initialised = False - else: + api_last_origin = access_manager.get_last_origin() + if not client: client = YouTubePlayerClient( context=context, language=settings.get_language(), @@ -242,6 +232,17 @@ class Provider(AbstractProvider): self._client = client if api_last_origin != origin: access_manager.set_last_origin(origin) + elif api_last_origin != origin: + access_manager.set_last_origin(origin) + self.log.info(('Resetting client - API key origin changed', + 'Previous: {old!r}', + 'Current: {new!r}'), + old=api_last_origin, + new=origin) + client.initialised = False + elif client.context_changed(context): + self.log.debug('Resetting client - Current context changed') + client.initialised = False if not client.initialised: self.reset_client( @@ -259,7 +260,8 @@ class Provider(AbstractProvider): ) = access_manager.get_refresh_tokens(dev_id) if not num_access_tokens and not num_refresh_tokens: - access_manager.update_access_token(dev_id, access_token='') + if any(access_tokens): + access_manager.update_access_token(dev_id, access_token='') return client if num_access_tokens == num_refresh_tokens and client.logged_in: return client @@ -710,7 +712,7 @@ class Provider(AbstractProvider): and identifier.lower() == 'property' and li_channel_id and li_channel_id.lower().startswith(('mine', 'uc'))): - context.execute('ActivateWindow(Videos, {channel}, return)'.format( + context.execute('ActivateWindow(Videos,"{channel}",return)'.format( channel=create_uri( (PATHS.CHANNEL, li_channel_id,), ) @@ -989,7 +991,7 @@ class Provider(AbstractProvider): def on_search_run(self, context, query=None): params = context.get_params() if query is None: - query = to_unicode(params.get('q', '')) + query = params.get('q', '') # Search by url to access unlisted videos if query.startswith(('https://', 'http://')): @@ -1333,7 +1335,6 @@ class Provider(AbstractProvider): if command == 'remove': video_name = params.get('item_name') or video_id - video_name = to_unicode(video_name) if not ui.on_yes_no_input( localize('content.remove'), localize('content.remove.check.x', video_name), @@ -2079,7 +2080,6 @@ class Provider(AbstractProvider): if command == 'remove': bookmark_name = params.get('item_name') or localize('bookmark') - bookmark_name = to_unicode(bookmark_name) if not ui.on_yes_no_input( localize('content.remove'), localize('content.remove.check.x', bookmark_name), @@ -2179,7 +2179,6 @@ class Provider(AbstractProvider): if command == 'remove': video_name = params.get('item_name') or localize('untitled') - video_name = to_unicode(video_name) if not ui.on_yes_no_input( localize('content.remove'), localize('content.remove.check.x', video_name), diff --git a/resources/settings.xml b/resources/settings.xml index be23ad1e..8092e193 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -330,12 +330,25 @@ - + 0 false + + 0 + + true + + + + true + + + RunScript($ID,config/show_api_config_page_address) + +