Merge pull request #1430 from MoojMidge/nexus-unofficial

Nexus unofficial v7.4.3+beta.1
This commit is contained in:
MoojMidge 2026-04-11 00:06:59 +09:00 committed by GitHub
commit 992a90dfc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 623 additions and 382 deletions

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.youtube" name="YouTube" version="7.4.2" provider-name="anxdpanic, bromix, MoojMidge">
<addon id="plugin.video.youtube" name="YouTube" version="7.4.3+beta.1" provider-name="anxdpanic, bromix, MoojMidge">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.requests" version="2.27.1"/>

View file

@ -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

View file

@ -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"

View file

@ -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
)

View file

@ -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({
'&': '&amp;',
@ -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={
'&': '&amp;',
@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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()

View file

@ -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):
<head>
<link rel="icon" href="data:;base64,=">
<meta charset="utf-8">
<title>{{title}}</title>
<style>{{css}}</style>
<title>{title}</title>
<style>{css}</style>
</head>
<body>
<div class="center">
<h5>{{header}}</h5>
<form action="{action_url}" class="config_form">
<h5>{header}</h5>
<form action="{action_url}" method="post" class="config_form" autocomplete="off">
<label for="api_key">
<span>{{api_key_head}}:</span>
<input type="text" name="api_key" value="{{api_key_value}}" size="50"/>
<span>{api_key_head}:</span>
<input type="text" name="api_key" value="{api_key_value}" size="50"/>
</label>
<label for="api_id">
<span>{{api_id_head}}:</span>
<input type="text" name="api_id" value="{{api_id_value}}" size="50"/>
<span>{api_id_head}:</span>
<input type="text" name="api_id" value="{api_id_value}" size="50"/>
</label>
<label for="api_secret">
<span>{{api_secret_head}}:</span>
<input type="text" name="api_secret" value="{{api_secret_value}}" size="50"/>
<span>{api_secret_head}:</span>
<input type="text" name="api_secret" value="{api_secret_value}" size="50"/>
</label>
<input type="submit" value="{{submit}}">
<input type="submit" value="{submit}">
</form>
<p class="text_center">
<small>{footer}</small>
</p>
</div>
</body>
</html>
'''.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):
<div class="content">
<p>{updated}</p>
<p>{enabled}</p>
<p class="text_center">
<small>{footer}</small>
</p>
</div>
</div>
</body>
@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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,
))

View file

@ -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

View file

@ -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:

View file

@ -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,

View file

@ -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:

View file

@ -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]

View file

@ -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,

View file

@ -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

View file

@ -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),

View file

@ -330,12 +330,25 @@
<control type="toggle"/>
</setting>
</group>
<group id="2" label="30633">
<group id="2" label="30634">
<setting id="youtube.api.config.page" type="boolean" label="30632" help="">
<level>0</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="youtube.api.config.page.address" type="action" label="30630" help="30633">
<level>0</level>
<constraints>
<allowempty>true</allowempty>
</constraints>
<dependencies>
<dependency type="enable">
<condition setting="youtube.api.config.page" operator="is">true</condition>
</dependency>
</dependencies>
<data>RunScript($ID,config/show_api_config_page_address)</data>
<control format="action" type="button"/>
</setting>
</group>
</category>
<category id="folders" label="30516" help="">