mirror of
https://github.com/anxdpanic/plugin.video.youtube.git
synced 2026-04-12 08:41:47 -07:00
Merge pull request #1430 from MoojMidge/nexus-unofficial
Nexus unofficial v7.4.3+beta.1
This commit is contained in:
commit
992a90dfc8
25 changed files with 623 additions and 382 deletions
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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="">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue