Merge pull request #1329 from MoojMidge/v7.3

v7.3.0+beta.9
This commit is contained in:
MoojMidge 2025-11-10 11:43:08 +11:00 committed by GitHub
commit ed3f53c60b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 582 additions and 307 deletions

View file

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

View file

@ -1,3 +1,17 @@
## v7.3.0+beta.9
### Fixed
- Disable label masks being used in Kodi 18 #1327
- Python 2 compatibility workaround for lack of timeout when trying to acquire an RLock #1327
- More expansive handling of inconsistent urllib3 exception re-raising
### Changed
- Improve robustness of fetching recommended and related videos
- Improve workarounds for SQLite concurrency issues
- Remove possibly invalid access token if an authentication error occurs
- Better organise and use standard labels for http server address and port settings
- Try to make http server IP address selection even more obvious when running Setup Wizard #1320
- Improve logging of errors caused by localised strings that have been incorrectly translated
## v7.3.0+beta.8 ## v7.3.0+beta.8
### Fixed ### Fixed
- Fix regression in handling audio only setting after d154325c5b672dccc6a17413063cfdeb32256ffd - Fix regression in handling audio only setting after d154325c5b672dccc6a17413063cfdeb32256ffd

View file

@ -878,7 +878,7 @@ msgid "Delete access_manager.json"
msgstr "" msgstr ""
msgctxt "#30643" msgctxt "#30643"
msgid "Listen on IP" msgid ""
msgstr "" msgstr ""
msgctxt "#30644" msgctxt "#30644"

View file

@ -399,7 +399,14 @@ class AbstractContext(object):
command = 'command://' if command else '' command = 'command://' if command else ''
if run: if run:
return ''.join((command, 'RunPlugin(', uri, ')')) return ''.join((command,
'RunAddon('
if run == 'addon' else
'RunScript('
if run == 'script' else
'RunPlugin(',
uri,
')'))
if play is not None: if play is not None:
return ''.join(( return ''.join((
command, command,

View file

@ -10,9 +10,9 @@
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
import atexit
import json import json
import sys import sys
from atexit import register as atexit_register
from timeit import default_timer from timeit import default_timer
from weakref import proxy from weakref import proxy
@ -461,7 +461,7 @@ class XbmcContext(AbstractContext):
self._ui = None self._ui = None
self._playlist = None self._playlist = None
atexit.register(self.tear_down) atexit_register(self.tear_down)
def init(self): def init(self):
num_args = len(sys.argv) num_args = len(sys.argv)
@ -684,8 +684,9 @@ class XbmcContext(AbstractContext):
return result % _args return result % _args
except TypeError: except TypeError:
self.log.exception(('Localization error', self.log.exception(('Localization error',
'text_id: {text_id!r}', 'String: {result!r} ({text_id!r})',
'args: {original_args!r}'), 'args: {original_args!r}'),
result=result,
text_id=text_id, text_id=text_id,
original_args=args) original_args=args)
return result return result
@ -743,13 +744,19 @@ class XbmcContext(AbstractContext):
) )
if current_system_version.compatible(19): if current_system_version.compatible(19):
def add_sort_method(self, sort_methods): def add_sort_method(self,
sort_methods,
_add_sort_method=xbmcplugin.addSortMethod):
handle = self._plugin_handle
for sort_method in sort_methods: for sort_method in sort_methods:
xbmcplugin.addSortMethod(self._plugin_handle, *sort_method) _add_sort_method(handle, *sort_method)
else: else:
def add_sort_method(self, sort_methods): def add_sort_method(self,
sort_methods,
_add_sort_method=xbmcplugin.addSortMethod):
handle = self._plugin_handle
for sort_method in sort_methods: for sort_method in sort_methods:
xbmcplugin.addSortMethod(self._plugin_handle, *sort_method[:2]) _add_sort_method(handle, *sort_method[:3:2])
def clone(self, new_path=None, new_params=None): def clone(self, new_path=None, new_params=None):
if not new_path: if not new_path:

View file

@ -181,8 +181,11 @@ class RequestHandler(BaseHTTPRequestHandler, object):
return return
except (HTTPError, OSError) as exc: except (HTTPError, OSError) as exc:
self.close_connection = True self.close_connection = True
if exc.errno not in self.SWALLOWED_ERRORS: self.log.exception('Request failed')
raise exc if (isinstance(exc, HTTPError)
or getattr(exc, 'errno', None) in self.SWALLOWED_ERRORS):
return
raise exc
def ip_address_status(self, ip_address): def ip_address_status(self, ip_address):
is_whitelisted = ip_address in self.whitelist_ips is_whitelisted = ip_address in self.whitelist_ips
@ -413,7 +416,7 @@ class RequestHandler(BaseHTTPRequestHandler, object):
'list': priority_list, 'list': priority_list,
} }
elif original_path == '/api/timedtext': elif original_path == '/api/timedtext':
stream_type = (params.get('type', empty)[0], stream_type = (params.get('type', ['track'])[0],
params.get('fmt', empty)[0], params.get('fmt', empty)[0],
params.get('kind', empty)[0]) params.get('kind', empty)[0])
priority_list = [] priority_list = []

View file

@ -9,8 +9,8 @@
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
import atexit
import socket import socket
from atexit import register as atexit_register
from requests import Request, Session from requests import Request, Session
from requests.adapters import HTTPAdapter, Retry from requests.adapters import HTTPAdapter, Retry
@ -79,7 +79,7 @@ class BaseRequestsClass(object):
allowed_methods=None, allowed_methods=None,
) )
)) ))
atexit.register(_session.close) atexit_register(_session.close)
_context = None _context = None
_verify = True _verify = True
@ -390,27 +390,28 @@ class BaseRequestsClass(object):
raise raise_exc raise raise_exc
raise exc raise exc
if cache: if not cache:
if cached_response is not None: pass
self.log.debug(('Using cached response', elif cached_response is not None:
'Request ID: {request_id}', self.log.debug(('Using cached response',
'Etag: {etag}', 'Request ID: {request_id}',
'Modified: {timestamp}'), 'Etag: {etag}',
request_id=request_id, 'Modified: {timestamp}'),
etag=etag, request_id=request_id,
timestamp=timestamp, etag=etag,
stacklevel=stacklevel) timestamp=timestamp,
cache.set(request_id) stacklevel=stacklevel)
response = cached_response cache.set(request_id)
elif response is not None: response = cached_response
self.log.debug(('Saving response to cache', elif response is not None:
'Request ID: {request_id}', self.log.debug(('Saving response to cache',
'Etag: {etag}', 'Request ID: {request_id}',
'Modified: {timestamp}'), 'Etag: {etag}',
request_id=request_id, 'Modified: {timestamp}'),
etag=etag, request_id=request_id,
timestamp=timestamp, etag=etag,
stacklevel=stacklevel) timestamp=timestamp,
cache.set(request_id, response, etag) stacklevel=stacklevel)
cache.set(request_id, response, etag)
return response return response

View file

@ -464,7 +464,8 @@ class AbstractSettings(object):
ip_address = '.'.join(map(str, octets)) ip_address = '.'.join(map(str, octets))
if value is not None: if value is not None:
return self.set_string(SETTINGS.HTTPD_LISTEN, ip_address) if not self.set_string(SETTINGS.HTTPD_LISTEN, ip_address):
return False
return ip_address return ip_address
def httpd_whitelist(self): def httpd_whitelist(self):

View file

@ -18,6 +18,8 @@ class DataCache(Storage):
_table_updated = False _table_updated = False
_sql = {} _sql = {}
memory_store = {}
def __init__(self, filepath, max_file_size_mb=5): def __init__(self, filepath, max_file_size_mb=5):
max_file_size_kb = max_file_size_mb * 1024 max_file_size_kb = max_file_size_mb * 1024
super(DataCache, self).__init__(filepath, super(DataCache, self).__init__(filepath,
@ -27,25 +29,11 @@ class DataCache(Storage):
content_ids, content_ids,
seconds=None, seconds=None,
as_dict=True, as_dict=True,
values_only=True, values_only=True):
memory_store=None):
if memory_store:
in_memory_result = {}
_content_ids = []
for key in content_ids:
if key in memory_store:
in_memory_result[key] = memory_store[key]
else:
_content_ids.append(key)
content_ids = _content_ids
else:
in_memory_result = None
result = self._get_by_ids(content_ids, result = self._get_by_ids(content_ids,
seconds=seconds, seconds=seconds,
as_dict=as_dict, as_dict=as_dict,
values_only=values_only) values_only=values_only)
if in_memory_result:
result.update(in_memory_result)
return result return result
def get_items_like(self, content_id, seconds=None): def get_items_like(self, content_id, seconds=None):
@ -70,12 +58,11 @@ class DataCache(Storage):
result = self._get(content_id, seconds=seconds, as_dict=as_dict) result = self._get(content_id, seconds=seconds, as_dict=as_dict)
return result return result
def set_item(self, content_id, item): def set_item(self, content_id, item, defer=False, flush=False):
self._set(content_id, item) self._set(content_id, item, defer=defer, flush=flush)
def set_items(self, items): def set_items(self, items, defer=False, flush=False):
self._set_many(items) self._set_many(items, defer=defer, flush=flush)
self._optimize_file_size()
def del_item(self, content_id): def del_item(self, content_id):
self._remove(content_id) self._remove(content_id)

View file

@ -22,6 +22,8 @@ class FunctionCache(Storage):
_table_updated = False _table_updated = False
_sql = {} _sql = {}
memory_store = {}
_BUILTIN = str.__module__ _BUILTIN = str.__module__
SCOPE_NONE = 0 SCOPE_NONE = 0
SCOPE_BUILTINS = 1 SCOPE_BUILTINS = 1
@ -134,7 +136,7 @@ class FunctionCache(Storage):
if callable(process): if callable(process):
data = process(data, _data) data = process(data, _data)
if data != ignore_value: if data != ignore_value:
self._set(cache_id, data) self._set(cache_id, data, defer=True)
elif oneshot: elif oneshot:
self._remove(cache_id) self._remove(cache_id)

View file

@ -40,8 +40,8 @@ class PlaybackHistory(Storage):
result = self._get(key, process=self._add_last_played) result = self._get(key, process=self._add_last_played)
return result return result
def set_item(self, video_id, play_data, timestamp=None): def set_item(self, video_id, play_data):
self._set(video_id, play_data, timestamp) self._set(video_id, play_data)
def del_item(self, video_id): def del_item(self, video_id):
self._remove(video_id) self._remove(video_id)

View file

@ -18,6 +18,8 @@ class RequestCache(Storage):
_table_updated = False _table_updated = False
_sql = {} _sql = {}
memory_store = {}
def __init__(self, filepath, max_file_size_mb=20): def __init__(self, filepath, max_file_size_mb=20):
max_file_size_kb = max_file_size_mb * 1024 max_file_size_kb = max_file_size_mb * 1024
super(RequestCache, self).__init__(filepath, super(RequestCache, self).__init__(filepath,
@ -36,8 +38,7 @@ class RequestCache(Storage):
if timestamp: if timestamp:
self._update(request_id, item, timestamp) self._update(request_id, item, timestamp)
else: else:
self._set(request_id, item) self._set(request_id, item, defer=True)
self._optimize_file_size()
else: else:
self._refresh(request_id, timestamp) self._refresh(request_id, timestamp)

View file

@ -13,12 +13,14 @@ from __future__ import absolute_import, division, unicode_literals
import os import os
import sqlite3 import sqlite3
import time import time
from atexit import register as atexit_register
from threading import RLock, Timer from threading import RLock, Timer
from .. import logging from .. import logging
from ..compatibility import pickle, to_str from ..compatibility import pickle, to_str
from ..utils.datetime import fromtimestamp, since_epoch from ..utils.datetime import fromtimestamp, since_epoch
from ..utils.file_system import make_dirs from ..utils.file_system import make_dirs
from ..utils.system_version import current_system_version
class StorageLock(object): class StorageLock(object):
@ -27,11 +29,18 @@ class StorageLock(object):
self._num_accessing = 0 self._num_accessing = 0
self._num_waiting = 0 self._num_waiting = 0
def __enter__(self): if current_system_version.compatible(19):
self._num_waiting += 1 def __enter__(self):
locked = not self._lock.acquire(timeout=3) self._num_waiting += 1
self._num_waiting -= 1 locked = not self._lock.acquire(timeout=3)
return locked self._num_waiting -= 1
return locked
else:
def __enter__(self):
self._num_waiting += 1
locked = not self._lock.acquire(blocking=False)
self._num_waiting -= 1
return locked
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
try: try:
@ -40,16 +49,13 @@ class StorageLock(object):
pass pass
def accessing(self, start=False, done=False): def accessing(self, start=False, done=False):
if start:
self._num_accessing += 1
elif done:
self._num_accessing -= 1
num = self._num_accessing num = self._num_accessing
if num > 0: if start:
return True num += 1
if num < 0: elif done and num > 0:
self._num_accessing = 0 num -= 1
return False self._num_accessing = num
return num > 0
def waiting(self): def waiting(self):
return self._num_waiting > 0 return self._num_waiting > 0
@ -225,8 +231,10 @@ class Storage(object):
self._db = None self._db = None
self._lock = StorageLock() self._lock = StorageLock()
self._close_timer = None self._close_timer = None
self._close_actions = False
self._max_item_count = -1 if migrate else max_item_count self._max_item_count = -1 if migrate else max_item_count
self._max_file_size_kb = -1 if migrate else max_file_size_kb self._max_file_size_kb = -1 if migrate else max_file_size_kb
atexit_register(self._close, event='shutdown')
if migrate: if migrate:
self._base = self self._base = self
@ -264,14 +272,22 @@ class Storage(object):
def set_max_file_size_kb(self, max_file_size_kb): def set_max_file_size_kb(self, max_file_size_kb):
self._max_file_size_kb = max_file_size_kb self._max_file_size_kb = max_file_size_kb
def __del__(self):
self._close(event='deleted')
def __enter__(self): def __enter__(self):
self._lock.accessing(start=True)
close_timer = self._close_timer close_timer = self._close_timer
if close_timer: if close_timer:
close_timer.cancel() close_timer.cancel()
self._close_timer = None
self._lock.accessing(start=True)
db = self._db or self._open() db = self._db or self._open()
cursor = db.cursor() try:
cursor = db.cursor()
except (AttributeError, sqlite3.ProgrammingError):
db = self._open()
cursor = db.cursor()
cursor.arraysize = 100 cursor.arraysize = 100
return db, cursor return db, cursor
@ -279,13 +295,16 @@ class Storage(object):
close_timer = self._close_timer close_timer = self._close_timer
if close_timer: if close_timer:
close_timer.cancel() close_timer.cancel()
if not self._lock.accessing(done=True) and not self._lock.waiting():
if self._lock.accessing(done=True) or self._lock.waiting():
return
with self._lock as locked:
if locked or self._close_timer:
return
close_timer = Timer(5, self._close) close_timer = Timer(5, self._close)
close_timer.daemon = True
close_timer.start() close_timer.start()
self._close_timer = close_timer self._close_timer = close_timer
else:
self._close_timer = None
def _open(self): def _open(self):
statements = [] statements = []
@ -299,7 +318,7 @@ class Storage(object):
for attempt in range(1, 4): for attempt in range(1, 4):
try: try:
db = sqlite3.connect(self._filepath, db = sqlite3.connect(self._filepath,
# cached_statements=0, cached_statements=0,
check_same_thread=False, check_same_thread=False,
isolation_level=None) isolation_level=None)
break break
@ -354,16 +373,38 @@ class Storage(object):
self._db = db self._db = db
return db return db
def _close(self, commit=False): def _close(self, commit=False, event=None):
db = self._db close_timer = self._close_timer
if not db or self._lock.accessing() or self._lock.waiting(): if close_timer:
close_timer.cancel()
if self._lock.accessing() or self._lock.waiting():
return False return False
db = self._db
if not db and self._close_actions:
db = self._open()
else:
return None
if self._close_actions:
memory_store = getattr(self, 'memory_store', None)
if memory_store:
self._set_many(items=None, memory_store=memory_store)
self._optimize_item_count()
self._optimize_file_size()
self._close_actions = False
self._execute(db.cursor(), 'PRAGMA optimize') self._execute(db.cursor(), 'PRAGMA optimize')
# Not needed if using db as a context manager # Not needed if using db as a context manager
if commit: if commit:
db.commit() db.commit()
db.close()
self._db = None if event:
db.close()
self._db = None
self._close_timer = None
return True return True
def _execute(self, cursor, query, values=None, many=False, script=False): def _execute(self, cursor, query, values=None, many=False, script=False):
@ -393,9 +434,9 @@ class Storage(object):
else: else:
self.log.exception('Failed') self.log.exception('Failed')
break break
self.log.warning('Attempt %d of 3', self.log.warning_trace('Attempt %d of 3',
attempt, attempt,
exc_info=True) exc_info=True)
else: else:
self.log.exception('Failed') self.log.exception('Failed')
return [] return []
@ -424,12 +465,18 @@ class Storage(object):
query = self._sql['prune_by_size'].format(prune_size) query = self._sql['prune_by_size'].format(prune_size)
if defer: if defer:
return query return query
with self._lock as locked, self as (db, cursor), db: with self as (db, cursor), db:
if locked: self._execute(
return False cursor,
self._execute(cursor, query) '\n'.join((
self._execute(cursor, 'VACUUM') 'BEGIN IMMEDIATE;',
return True query,
'COMMIT;',
'VACUUM;',
)),
script=True,
)
return None
def _optimize_item_count(self, limit=-1, defer=False): def _optimize_item_count(self, limit=-1, defer=False):
# do nothing - optimize only if max item limit has been set # do nothing - optimize only if max item limit has been set
@ -447,26 +494,71 @@ class Storage(object):
) )
if defer: if defer:
return query return query
with self._lock as locked, self as (db, cursor), db: with self as (db, cursor), db:
if locked: self._execute(
cursor,
'\n'.join((
'BEGIN IMMEDIATE;',
query,
'COMMIT;',
'VACUUM;',
)),
script=True,
)
return None
def _set(self, item_id, item, defer=False, flush=False, memory_store=None):
if memory_store is None:
memory_store = getattr(self, 'memory_store', None)
if memory_store is not None:
if defer:
memory_store[item_id] = item
self._close_actions = True
return None
if flush:
memory_store.clear()
return False return False
self._execute(cursor, query) if memory_store:
self._execute(cursor, 'VACUUM') memory_store[item_id] = item
return self._set_many(items=None, memory_store=memory_store)
values = self._encode(item_id, item)
with self as (db, cursor), db:
self._execute(
cursor,
'\n'.join((
'BEGIN IMMEDIATE;',
self._sql['set'],
'COMMIT;',
)),
values,
script=True,
)
self._close_actions = True
return True return True
def _set(self, item_id, item, timestamp=None): def _set_many(self,
values = self._encode(item_id, item, timestamp) items,
optimize_query = self._optimize_item_count(1, defer=True) flatten=False,
with self._lock as locked, self as (db, cursor), db: defer=False,
if locked: flush=False,
memory_store=None):
if memory_store is None:
memory_store = getattr(self, 'memory_store', None)
if memory_store is not None:
if defer:
memory_store.update(items)
self._close_actions = True
return None
if flush:
memory_store.clear()
return False return False
if optimize_query: if memory_store:
self._execute(cursor, 'BEGIN') if items:
self._execute(cursor, optimize_query) memory_store.update(items)
self._execute(cursor, self._sql['set'], values=values) items = memory_store
return True flush = True
def _set_many(self, items, flatten=False):
now = since_epoch() now = since_epoch()
num_items = len(items) num_items = len(items)
@ -482,42 +574,73 @@ class Storage(object):
for item in items.items()] for item in items.items()]
query = self._sql['set'] query = self._sql['set']
optimize_query = self._optimize_item_count(num_items, defer=True) with self as (db, cursor), db:
with self._lock as locked, self as (db, cursor), db: if flatten:
if locked: self._execute(
return False cursor,
if optimize_query: '\n'.join((
self._execute(cursor, 'BEGIN') 'BEGIN IMMEDIATE;',
self._execute(cursor, optimize_query) query,
self._execute(cursor, query, many=(not flatten), values=values) 'COMMIT;',
)),
values,
script=True,
)
else:
self._execute(cursor, 'BEGIN IMMEDIATE')
self._execute(cursor, query, many=True, values=values)
self._close_actions = True
if flush:
memory_store.clear()
return True return True
def _refresh(self, item_id, timestamp=None): def _refresh(self, item_id, timestamp=None):
values = (timestamp or since_epoch(), to_str(item_id)) values = (timestamp or since_epoch(), to_str(item_id))
with self._lock as locked, self as (db, cursor), db: with self as (db, cursor), db:
if locked: self._execute(
return False cursor,
self._execute(cursor, self._sql['refresh'], values=values) '\n'.join((
'BEGIN IMMEDIATE;',
self._sql['refresh'],
'COMMIT;',
)),
values,
script=True,
)
return True return True
def _update(self, item_id, item, timestamp=None): def _update(self, item_id, item, timestamp=None):
values = self._encode(item_id, item, timestamp, for_update=True) values = self._encode(item_id, item, timestamp, for_update=True)
with self._lock as locked, self as (db, cursor), db: with self as (db, cursor), db:
if locked: self._execute(
return False cursor,
self._execute(cursor, self._sql['update'], values=values) '\n'.join((
'BEGIN IMMEDIATE;',
self._sql['update'],
'COMMIT;',
)),
values,
script=True,
)
return True return True
def clear(self, defer=False): def clear(self, defer=False):
query = self._sql['clear'] query = self._sql['clear']
if defer: if defer:
return query return query
with self._lock as locked, self as (db, cursor), db: with self as (db, cursor), db:
if locked: self._execute(
return False cursor,
self._execute(cursor, query) '\n'.join((
self._execute(cursor, 'VACUUM') 'BEGIN IMMEDIATE;',
return True query,
'COMMIT;',
'VACUUM;',
)),
script=True,
)
return None
def is_empty(self): def is_empty(self):
with self as (db, cursor): with self as (db, cursor):
@ -531,7 +654,10 @@ class Storage(object):
@staticmethod @staticmethod
def _decode(obj, process=None, item=None): def _decode(obj, process=None, item=None):
decoded_obj = pickle.loads(obj) if item and item[3] is None:
decoded_obj = obj
else:
decoded_obj = pickle.loads(obj)
if process: if process:
return process(decoded_obj, item) return process(decoded_obj, item)
return decoded_obj return decoded_obj
@ -579,6 +705,10 @@ class Storage(object):
def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1, def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1,
wildcard=False, seconds=None, process=None, wildcard=False, seconds=None, process=None,
as_dict=False, values_only=True, excluding=None): as_dict=False, values_only=True, excluding=None):
epoch = since_epoch()
cut_off = epoch - seconds if seconds else 0
in_memory_result = None
if not item_ids: if not item_ids:
if oldest_first: if oldest_first:
query = self._sql['get_many'] query = self._sql['get_many']
@ -599,58 +729,100 @@ class Storage(object):
) )
item_ids = tuple(item_ids) + tuple(excluding) item_ids = tuple(item_ids) + tuple(excluding)
else: else:
query = self._sql['get_by_key'].format( memory_store = getattr(self, 'memory_store', None)
'?,' * (len(item_ids) - 1) + '?' if memory_store:
) in_memory_result = []
item_ids = tuple(item_ids) _item_ids = []
for key in item_ids:
epoch = since_epoch() if key in memory_store:
cut_off = epoch - seconds if seconds else 0 in_memory_result.append((
with self as (db, cursor): key,
result = self._execute(cursor, query, item_ids) epoch,
if not result: memory_store[key],
pass None,
elif as_dict: ))
if values_only: else:
result = { _item_ids.append(key)
item[0]: self._decode(item[2], process, item) item_ids = _item_ids
for item in result if not cut_off or item[1] >= cut_off
}
else: else:
result = { in_memory_result = None
item[0]: {
'age': epoch - item[1], if item_ids:
'value': self._decode(item[2], process, item), query = self._sql['get_by_key'].format(
} '?,' * (len(item_ids) - 1) + '?'
for item in result if not cut_off or item[1] >= cut_off )
} item_ids = tuple(item_ids)
elif values_only: else:
result = [ query = None
self._decode(item[2], process, item)
if query:
with self as (db, cursor):
result = self._execute(cursor, query, item_ids)
if result:
result = result.fetchall()
else:
result = None
if in_memory_result:
if result:
in_memory_result.extend(result)
result = in_memory_result
if as_dict:
if values_only:
result = {
item[0]: self._decode(item[2], process, item)
for item in result if not cut_off or item[1] >= cut_off for item in result if not cut_off or item[1] >= cut_off
] }
else: else:
result = [ result = {
(item[0], item[0]: {
fromtimestamp(item[1]), 'age': epoch - item[1],
self._decode(item[2], process, item)) 'value': self._decode(item[2], process, item),
}
for item in result if not cut_off or item[1] >= cut_off for item in result if not cut_off or item[1] >= cut_off
] }
elif values_only:
result = [
self._decode(item[2], process, item)
for item in result if not cut_off or item[1] >= cut_off
]
else:
result = [
(item[0],
fromtimestamp(item[1]),
self._decode(item[2], process, item))
for item in result if not cut_off or item[1] >= cut_off
]
return result return result
def _remove(self, item_id): def _remove(self, item_id):
with self._lock as locked, self as (db, cursor), db: with self as (db, cursor), db:
if locked: self._execute(
return False cursor,
self._execute(cursor, self._sql['remove'], [item_id]) '\n'.join((
'BEGIN IMMEDIATE;',
self._sql['remove'],
'COMMIT;',
)),
[item_id],
script=True,
)
return True return True
def _remove_many(self, item_ids): def _remove_many(self, item_ids):
num_ids = len(item_ids) num_ids = len(item_ids)
query = self._sql['remove_by_key'].format('?,' * (num_ids - 1) + '?') query = self._sql['remove_by_key'].format('?,' * (num_ids - 1) + '?')
with self._lock as locked, self as (db, cursor), db: with self as (db, cursor), db:
if locked: self._execute(
return False cursor,
self._execute(cursor, query, tuple(item_ids)) '\n'.join((
self._execute(cursor, 'VACUUM') 'BEGIN IMMEDIATE;',
query,
'COMMIT;',
'VACUUM;',
)),
tuple(item_ids),
script=True,
)
return True return True

View file

@ -89,23 +89,33 @@ class YouTubeDataClient(YouTubeLoginClient):
'tvSurfaceContentRenderer', 'tvSurfaceContentRenderer',
'content', 'content',
'sectionListRenderer', 'sectionListRenderer',
'contents', (
0, (
'shelfRenderer', 'contents',
'content', slice(None),
'horizontalListRenderer', None,
'continuations', 'shelfRenderer',
0, 'content',
'nextContinuationData', ('horizontalListRenderer', 'verticalListRenderer'),
'continuations',
0,
'nextContinuationData',
),
(
'continuations',
0,
'nextContinuationData'
)
),
), ),
'continuation_items': ( 'continuation_items': (
'continuationContents', 'continuationContents',
'horizontalListContinuation', ('horizontalListContinuation', 'sectionListContinuation'),
'items', 'items',
), ),
'continuation_continuation': ( 'continuation_continuation': (
'continuationContents', 'continuationContents',
'horizontalListContinuation', ('horizontalListContinuation', 'sectionListContinuation'),
'continuations', 'continuations',
0, 0,
'nextContinuationData', 'nextContinuationData',
@ -200,7 +210,7 @@ class YouTubeDataClient(YouTubeLoginClient):
slice(None), slice(None),
'shelfRenderer', 'shelfRenderer',
'content', 'content',
'horizontalListRenderer', ('horizontalListRenderer', 'verticalListRenderer'),
'items', 'items',
), ),
'item_id': ( 'item_id': (
@ -244,23 +254,43 @@ class YouTubeDataClient(YouTubeLoginClient):
'tvSurfaceContentRenderer', 'tvSurfaceContentRenderer',
'content', 'content',
'sectionListRenderer', 'sectionListRenderer',
'contents', (
0, (
'shelfRenderer', 'contents',
'content', slice(None),
'horizontalListRenderer', None,
'continuations', 'shelfRenderer',
0, 'content',
'nextContinuationData', ('horizontalListRenderer', 'verticalListRenderer'),
'continuations',
0,
'nextContinuationData',
),
(
'continuations',
0,
'nextContinuationData'
)
),
), ),
'continuation_items': ( 'continuation_items': (
'continuationContents', 'continuationContents',
'horizontalListContinuation', ('horizontalListContinuation', 'sectionListContinuation'),
'items', (
('items',),
(
'contents',
slice(None),
'shelfRenderer',
'content',
('horizontalListRenderer', 'verticalListRenderer'),
'items',
),
),
), ),
'continuation_continuation': ( 'continuation_continuation': (
'continuationContents', 'continuationContents',
'horizontalListContinuation', ('horizontalListContinuation', 'sectionListContinuation'),
'continuations', 'continuations',
0, 0,
'nextContinuationData', 'nextContinuationData',
@ -282,7 +312,11 @@ class YouTubeDataClient(YouTubeLoginClient):
('horizontalListRenderer', 'verticalListRenderer'), ('horizontalListRenderer', 'verticalListRenderer'),
'items', 'items',
slice(None), slice(None),
('gridVideoRenderer', 'compactVideoRenderer'), (
'gridVideoRenderer',
'compactVideoRenderer',
'tileRenderer',
),
# 'videoId', # 'videoId',
), ),
'continuation': ( 'continuation': (
@ -307,7 +341,11 @@ class YouTubeDataClient(YouTubeLoginClient):
('horizontalListRenderer', 'verticalListRenderer'), ('horizontalListRenderer', 'verticalListRenderer'),
'items', 'items',
slice(None), slice(None),
('gridVideoRenderer', 'compactVideoRenderer'), (
'gridVideoRenderer',
'compactVideoRenderer',
'tileRenderer',
),
# 'videoId', # 'videoId',
), ),
'continuation_continuation': ( 'continuation_continuation': (
@ -1686,7 +1724,7 @@ class YouTubeDataClient(YouTubeLoginClient):
2, 2,
'shelfRenderer', 'shelfRenderer',
'content', 'content',
'horizontalListRenderer', ('horizontalListRenderer', 'verticalListRenderer'),
'items', 'items',
) if retry == 2 else ( ) if retry == 2 else (
'contents', 'contents',
@ -2956,22 +2994,34 @@ class YouTubeDataClient(YouTubeLoginClient):
message = strip_html_from_text(details.get('message', 'Unknown error')) message = strip_html_from_text(details.get('message', 'Unknown error'))
if getattr(exc, 'notify', True): if getattr(exc, 'notify', True):
context = self._context
ok_dialog = False ok_dialog = False
if reason in {'accessNotConfigured', 'forbidden'}: if reason in {'accessNotConfigured', 'forbidden'}:
notification = self._context.localize('key.requirement') notification = context.localize('key.requirement')
ok_dialog = True ok_dialog = True
elif reason == 'keyInvalid' and message == 'Bad Request': elif reason == 'keyInvalid' and message == 'Bad Request':
notification = self._context.localize('api.key.incorrect') notification = context.localize('api.key.incorrect')
elif reason in {'quotaExceeded', 'dailyLimitExceeded'}: elif reason in {'quotaExceeded', 'dailyLimitExceeded'}:
notification = message notification = message
elif reason == 'authError':
auth_type = kwargs.get('_auth_type')
if auth_type:
if auth_type in self._access_tokens:
self._access_tokens[auth_type] = None
self.set_access_token(self._access_tokens)
context.get_access_manager().update_access_token(
context.get_param('addon_id'),
access_token=self.convert_access_tokens(to_list=True),
)
notification = message
else: else:
notification = message notification = message
title = ': '.join((self._context.get_name(), reason)) title = ': '.join((context.get_name(), reason))
if ok_dialog: if ok_dialog:
self._context.get_ui().on_ok(title, notification) context.get_ui().on_ok(title, notification)
else: else:
self._context.get_ui().show_notification(notification, title) context.get_ui().show_notification(notification, title)
info = ( info = (
'Reason: {error_reason}', 'Reason: {error_reason}',

View file

@ -72,9 +72,37 @@ class YouTubeLoginClient(YouTubeRequestClient):
def reinit(self, **kwargs): def reinit(self, **kwargs):
super(YouTubeLoginClient, self).reinit(**kwargs) super(YouTubeLoginClient, self).reinit(**kwargs)
@classmethod
def convert_access_tokens(cls,
access_tokens=None,
to_dict=False,
to_list=False):
if access_tokens is None:
access_tokens = cls._access_tokens
if to_dict or isinstance(access_tokens, (list, tuple)):
access_tokens = {
cls.TOKEN_TYPES[token_idx]: token
for token_idx, token in enumerate(access_tokens)
if token and token_idx in cls.TOKEN_TYPES
}
elif to_list or isinstance(access_tokens, dict):
_access_tokens = [None, None, None, None]
for token_type, token in access_tokens.items():
token_idx = cls.TOKEN_TYPES.get(token_type)
if token_idx is None:
continue
_access_tokens[token_idx] = token
access_tokens = _access_tokens
return access_tokens
def set_access_token(self, access_tokens=None): def set_access_token(self, access_tokens=None):
existing_access_tokens = type(self)._access_tokens existing_access_tokens = type(self)._access_tokens
if access_tokens: if access_tokens:
if isinstance(access_tokens, (list, tuple)):
access_tokens = self.convert_access_tokens(
access_tokens,
to_dict=True,
)
token_status = 0 token_status = 0
for token_type, token in existing_access_tokens.items(): for token_type, token in existing_access_tokens.items():
if token_type in access_tokens: if token_type in access_tokens:

View file

@ -14,7 +14,7 @@ from base64 import urlsafe_b64encode
from json import dumps as json_dumps, loads as json_loads from json import dumps as json_dumps, loads as json_loads
from os import path as os_path from os import path as os_path
from random import choice as random_choice from random import choice as random_choice
from re import compile as re_compile from re import compile as re_compile, sub as re_sub
from .data_client import YouTubeDataClient from .data_client import YouTubeDataClient
from .subtitles import SUBTITLE_SELECTIONS, Subtitles from .subtitles import SUBTITLE_SELECTIONS, Subtitles
@ -852,7 +852,6 @@ class YouTubePlayerClient(YouTubeDataClient):
self._client_groups = ( self._client_groups = (
('custom', clients if clients else ()), ('custom', clients if clients else ()),
('auth_enabled|initial_request|no_playable_streams', ( ('auth_enabled|initial_request|no_playable_streams', (
'tv_embed',
'tv_unplugged', 'tv_unplugged',
'tv', 'tv',
)), )),
@ -1136,12 +1135,22 @@ class YouTubePlayerClient(YouTubeDataClient):
headers = response['client']['headers'] headers = response['client']['headers']
if '?' in url: url_components = urlsplit(url)
url += '&mpd_version=5' if url_components.query:
elif url.endswith('/'): params = dict(parse_qs(url_components.query))
url += 'mpd_version/5' params['mpd_version'] = ['7']
url = url_components._replace(
query=urlencode(params, doseq=True),
).geturl()
else: else:
url += '/mpd_version/5' path = re_sub(
r'/mpd_version/\d+|/?$',
'/mpd_version/7',
url_components.path,
)
url = url_components._replace(
path=path,
).geturl()
stream_list[itag] = self._get_stream_format( stream_list[itag] = self._get_stream_format(
itag=itag, itag=itag,
@ -1541,7 +1550,7 @@ class YouTubePlayerClient(YouTubeDataClient):
'_visitor_data': self._visitor_data[self._visitor_data_key], '_visitor_data': self._visitor_data[self._visitor_data_key],
} }
for client_name in ('tv_embed', 'web'): for client_name in ('tv_unplugged', 'web'):
client = self.build_client(client_name, client_data) client = self.build_client(client_name, client_data)
if not client: if not client:
continue continue

View file

@ -803,26 +803,21 @@ class YouTubeRequestClient(BaseRequestsClass):
if isinstance(keys, slice): if isinstance(keys, slice):
next_key = path[idx + 1] next_key = path[idx + 1]
parts = result[keys]
if next_key is None: if next_key is None:
for part in result[keys]: new_path = path[idx + 2:]
new_result = cls.json_traverse( for part in parts:
part, new_result = cls.json_traverse(part, new_path, default)
path[idx + 2:],
default=default,
)
if not new_result or new_result == default: if not new_result or new_result == default:
continue continue
return new_result return new_result
if isinstance(next_key, range_type): if isinstance(next_key, range_type):
results_limit = len(next_key) results_limit = len(next_key)
new_path = path[idx + 2:]
new_results = [] new_results = []
for part in result[keys]: for part in parts:
new_result = cls.json_traverse( new_result = cls.json_traverse(part, new_path, default)
part,
path[idx + 2:],
default=default,
)
if not new_result or new_result == default: if not new_result or new_result == default:
continue continue
new_results.append(new_result) new_results.append(new_result)
@ -831,9 +826,10 @@ class YouTubeRequestClient(BaseRequestsClass):
break break
results_limit -= 1 results_limit -= 1
else: else:
new_path = path[idx + 1:]
new_results = [ new_results = [
cls.json_traverse(part, path[idx + 1:], default=default) cls.json_traverse(part, new_path, default)
for part in result[keys] for part in parts
if part if part
] ]
return new_results return new_results
@ -843,7 +839,7 @@ class YouTubeRequestClient(BaseRequestsClass):
for key in keys: for key in keys:
if isinstance(key, tuple): if isinstance(key, tuple):
new_result = cls.json_traverse(result, key, default=default) new_result = cls.json_traverse(result, key, default)
if new_result: if new_result:
result = new_result result = new_result
break break

View file

@ -103,26 +103,33 @@ class Subtitles(YouTubeRequestClient):
use_isa = not self.pre_download and use_mpd use_isa = not self.pre_download and use_mpd
self.use_isa = use_isa self.use_isa = use_isa
default_format = None
fallback_format = None
if use_isa: if use_isa:
if ('ttml' in stream_features if ('ttml' in stream_features
and context.inputstream_adaptive_capabilities('ttml')): and context.inputstream_adaptive_capabilities('ttml')):
self.FORMATS['_default'] = 'ttml' default_format = 'ttml'
self.FORMATS['_fallback'] = 'ttml' fallback_format = 'ttml'
if context.inputstream_adaptive_capabilities('vtt'): if context.inputstream_adaptive_capabilities('vtt'):
if 'vtt' in stream_features: if 'vtt' in stream_features:
self.FORMATS.setdefault('_default', 'vtt') default_format = default_format or 'vtt'
self.FORMATS['_fallback'] = 'vtt' fallback_format = 'vtt'
else: else:
self.FORMATS.setdefault('_default', 'srt') default_format = default_format or 'srt'
self.FORMATS['_fallback'] = 'srt' fallback_format = 'srt'
else:
if not default_format or not use_isa:
if ('vtt' in stream_features if ('vtt' in stream_features
and context.get_system_version().compatible(20)): and context.get_system_version().compatible(20)):
self.FORMATS['_default'] = 'vtt' default_format = 'vtt'
self.FORMATS['_fallback'] = 'vtt' fallback_format = 'vtt'
else: else:
self.FORMATS['_default'] = 'srt' default_format = 'srt'
self.FORMATS['_fallback'] = 'srt' fallback_format = 'srt'
self.FORMATS['_default'] = default_format
self.FORMATS['_fallback'] = fallback_format
kodi_sub_lang = context.get_subtitle_language() kodi_sub_lang = context.get_subtitle_language()
plugin_lang = settings.get_language() plugin_lang = settings.get_language()
@ -451,7 +458,6 @@ class Subtitles(YouTubeRequestClient):
subtitle_url = self._set_query_param( subtitle_url = self._set_query_param(
base_url, base_url,
('type', 'track'),
('fmt', sub_format), ('fmt', sub_format),
('tlang', tlang), ('tlang', tlang),
('xosf', None), ('xosf', None),

View file

@ -100,7 +100,6 @@ class ResourceManager(object):
result = data_cache.get_items( result = data_cache.get_items(
ids, ids,
None if forced_cache else data_cache.ONE_DAY, None if forced_cache else data_cache.ONE_DAY,
memory_store=self.new_data,
) )
to_update = ( to_update = (
[] []
@ -194,7 +193,6 @@ class ResourceManager(object):
result.update(data_cache.get_items( result.update(data_cache.get_items(
to_check, to_check,
None if forced_cache else data_cache.ONE_MONTH, None if forced_cache else data_cache.ONE_MONTH,
memory_store=self.new_data,
)) ))
to_update = ( to_update = (
[] []
@ -305,7 +303,6 @@ class ResourceManager(object):
result = data_cache.get_items( result = data_cache.get_items(
ids, ids,
None if forced_cache else data_cache.ONE_DAY, None if forced_cache else data_cache.ONE_DAY,
memory_store=self.new_data,
) )
to_update = ( to_update = (
[] []
@ -578,7 +575,6 @@ class ResourceManager(object):
result = data_cache.get_items( result = data_cache.get_items(
ids, ids,
None if forced_cache else data_cache.ONE_MONTH, None if forced_cache else data_cache.ONE_MONTH,
memory_store=self.new_data,
) )
to_update = ( to_update = (
[] []
@ -658,33 +654,25 @@ class ResourceManager(object):
return result return result
def cache_data(self, data=None, defer=False): def cache_data(self, data=None, defer=False):
if defer: if not data:
if data: return None
self.new_data.update(data)
return
if self.new_data: incognito = self._incognito
flush = True if not defer and self.log.debugging:
if data: self.log.debug(
self.new_data.update(data) (
data = self.new_data 'Incognito mode active - discarded data for {num} item(s)',
else: 'IDs: {ids}'
flush = False ) if incognito else (
if data: 'Storing new data to cache for {num} item(s)',
if self._incognito: 'IDs: {ids}'
self.log.debugging and self.log.debug( ),
('Incognito mode active - discarded data for {num} item(s)', num=len(data),
'IDs: {ids}'), ids=list(data)
num=len(data), )
ids=list(data),
) return self._context.get_data_cache().set_items(
else: data,
self.log.debugging and self.log.debug( defer=defer,
('Storing new data to cache for {num} item(s)', flush=incognito,
'IDs: {ids}'), )
num=len(data),
ids=list(data),
)
self._context.get_data_cache().set_items(data)
if flush:
self.new_data = {}

View file

@ -120,15 +120,14 @@ def _process_list_response(provider,
item_params = yt_item.get('_params') or {} item_params = yt_item.get('_params') or {}
item_params.update(new_params) item_params.update(new_params)
item_id = None item_id = yt_item.get('id')
snippet = yt_item.get('snippet', {})
video_id = None video_id = None
playlist_id = None playlist_id = None
channel_id = None channel_id = None
if is_youtube: if is_youtube:
item_id = yt_item.get('id')
snippet = yt_item.get('snippet', {})
localised_info = snippet.get('localized') or {} localised_info = snippet.get('localized') or {}
title = (localised_info.get('title') title = (localised_info.get('title')
or snippet.get('title') or snippet.get('title')

View file

@ -111,8 +111,9 @@ def process_default_settings(context, step, steps, **_kwargs):
background=False, background=False,
) as progress_dialog: ) as progress_dialog:
progress_dialog.update() progress_dialog.update()
if settings.httpd_listen() == '0.0.0.0': ip_address = settings.httpd_listen()
settings.httpd_listen('127.0.0.1') if ip_address == '0.0.0.0':
ip_address = settings.httpd_listen('127.0.0.1')
if not httpd_status(context): if not httpd_status(context):
port = settings.httpd_port() port = settings.httpd_port()
addresses = get_listen_addresses() addresses = get_listen_addresses()
@ -120,13 +121,17 @@ def process_default_settings(context, step, steps, **_kwargs):
for address in addresses: for address in addresses:
progress_dialog.update() progress_dialog.update()
if httpd_status(context, (address, port)): if httpd_status(context, (address, port)):
settings.httpd_listen(address) ip_address = settings.httpd_listen(address)
break break
context.sleep(5) context.sleep(3)
else: else:
ui.show_notification(localize('httpd.connect.failed'), ui.show_notification(localize('httpd.connect.failed'),
header=localize('httpd')) header=localize('httpd'))
settings.httpd_listen('0.0.0.0') settings.httpd_listen('0.0.0.0')
ip_address = None
if ip_address:
ui.on_ok(context.get_name(),
context.localize('client.ip.is.x', ip_address))
return step return step

View file

@ -313,14 +313,7 @@ class Provider(AbstractProvider):
access_token='', access_token='',
refresh_token=refresh_token, refresh_token=refresh_token,
) )
client.set_access_token(access_tokens)
client.set_access_token({
client.TOKEN_TYPES[idx]: token
for idx, token in enumerate(access_tokens)
if token
})
return client return client
def get_resource_manager(self, context, progress_dialog=None): def get_resource_manager(self, context, progress_dialog=None):

View file

@ -243,6 +243,7 @@
</constraints> </constraints>
<control format="integer" type="slider"> <control format="integer" type="slider">
<popup>false</popup> <popup>false</popup>
<formatlabel>21436</formatlabel>
</control> </control>
</setting> </setting>
<setting id="youtube.view.hide_videos" type="list[string]" label="30808" help=""> <setting id="youtube.view.hide_videos" type="list[string]" label="30808" help="">
@ -757,6 +758,7 @@
</constraints> </constraints>
<control format="integer" type="slider"> <control format="integer" type="slider">
<popup>false</popup> <popup>false</popup>
<formatlabel>37122</formatlabel>
</control> </control>
</setting> </setting>
<setting id="kodion.search.size" type="integer" label="30023" help=""> <setting id="kodion.search.size" type="integer" label="30023" help="">
@ -769,6 +771,7 @@
</constraints> </constraints>
<control format="integer" type="slider"> <control format="integer" type="slider">
<popup>false</popup> <popup>false</popup>
<formatlabel>21436</formatlabel>
</control> </control>
</setting> </setting>
</group> </group>
@ -793,6 +796,7 @@
</constraints> </constraints>
<control format="integer" type="slider"> <control format="integer" type="slider">
<popup>false</popup> <popup>false</popup>
<formatlabel>14045</formatlabel>
</control> </control>
</setting> </setting>
<setting id="youtube.view.filter.list" type="string" label="587" help="30583"> <setting id="youtube.view.filter.list" type="string" label="587" help="30583">
@ -968,6 +972,7 @@
</constraints> </constraints>
<control format="integer" type="slider"> <control format="integer" type="slider">
<popup>false</popup> <popup>false</popup>
<formatlabel>14047</formatlabel>
</control> </control>
</setting> </setting>
<setting id="youtube.playlist.watchlater.autoremove" type="boolean" label="30515" help=""> <setting id="youtube.playlist.watchlater.autoremove" type="boolean" label="30515" help="">
@ -1023,6 +1028,7 @@
</constraints> </constraints>
<control format="integer" type="slider"> <control format="integer" type="slider">
<popup>false</popup> <popup>false</popup>
<formatlabel>37122</formatlabel>
</control> </control>
</setting> </setting>
<setting id="requests.proxy.source" type="integer" label="713" help="36380"> <setting id="requests.proxy.source" type="integer" label="713" help="36380">
@ -1110,14 +1116,14 @@
</setting> </setting>
</group> </group>
<group id="http_server" label="30628"> <group id="http_server" label="30628">
<setting id="kodion.http.listen" type="string" label="30643" help=""> <setting id="kodion.http.listen" type="string" label="1006" help="">
<level>0</level> <level>0</level>
<default>127.0.0.1</default> <default>127.0.0.1</default>
<control format="ip" type="edit"> <control format="ip" type="edit">
<heading>30643</heading> <heading>14068</heading>
</control> </control>
</setting> </setting>
<setting id="kodion.http.listen.select" type="action" label="30644" help=""> <setting id="kodion.http.listen.select" type="action" parent="kodion.view.override" label="30644" help="">
<level>0</level> <level>0</level>
<constraints> <constraints>
<allowempty>true</allowempty> <allowempty>true</allowempty>
@ -1127,7 +1133,15 @@
<close>true</close> <close>true</close>
</control> </control>
</setting> </setting>
<setting id="kodion.http.port" type="integer" label="730" help=""> <setting id="kodion.http.client.ip" type="action" parent="kodion.view.override" label="30698" help="">
<level>0</level>
<constraints>
<allowempty>true</allowempty>
</constraints>
<data>RunScript($ID,config/show_client_ip)</data>
<control format="action" type="button"/>
</setting>
<setting id="kodion.http.port" type="integer" label="1013" help="">
<level>0</level> <level>0</level>
<default>50152</default> <default>50152</default>
<constraints> <constraints>
@ -1135,7 +1149,7 @@
<maximum>65535</maximum> <maximum>65535</maximum>
</constraints> </constraints>
<control format="integer" type="edit"> <control format="integer" type="edit">
<heading>730</heading> <heading>1018</heading>
</control> </control>
</setting> </setting>
<setting id="kodion.http.ip.whitelist" type="string" label="30629" help=""> <setting id="kodion.http.ip.whitelist" type="string" label="30629" help="">
@ -1148,14 +1162,6 @@
<heading>30629</heading> <heading>30629</heading>
</control> </control>
</setting> </setting>
<setting id="kodion.http.client.ip" type="action" label="30698" help="">
<level>0</level>
<constraints>
<allowempty>true</allowempty>
</constraints>
<data>RunScript($ID,config/show_client_ip)</data>
<control format="action" type="button"/>
</setting>
<setting id="youtube.http.idle_sleep" type="boolean" label="13018" help=""> <setting id="youtube.http.idle_sleep" type="boolean" label="13018" help="">
<level>0</level> <level>0</level>
<default>true</default> <default>true</default>