mirror of
https://github.com/anxdpanic/plugin.video.youtube.git
synced 2025-12-06 02:30:50 -08:00
commit
ed3f53c60b
23 changed files with 582 additions and 307 deletions
|
|
@ -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"/>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 = []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}',
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue