Updated kodi settings on Lenovo
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,6 +16,7 @@ from re import (
|
||||
)
|
||||
|
||||
from . import logging
|
||||
from .compatibility import string_type
|
||||
from .constants import (
|
||||
CHECK_SETTINGS,
|
||||
CONTENT,
|
||||
@@ -376,7 +377,7 @@ class AbstractProvider(object):
|
||||
self.log.warning('Multiple busy dialogs active'
|
||||
' - Rerouting workaround')
|
||||
return UriItem('command://{0}'.format(action))
|
||||
context.sleep(1)
|
||||
context.sleep(0.1)
|
||||
else:
|
||||
context.execute(
|
||||
action,
|
||||
@@ -417,7 +418,8 @@ class AbstractProvider(object):
|
||||
fallback = options.setdefault(
|
||||
provider.FALLBACK, context.get_uri()
|
||||
)
|
||||
ui.set_property(provider.FALLBACK, fallback)
|
||||
if fallback and isinstance(fallback, string_type):
|
||||
ui.set_property(provider.FALLBACK, fallback)
|
||||
return result, options
|
||||
command = 'list'
|
||||
context.set_path(PATHS.SEARCH, command)
|
||||
|
||||
@@ -15,8 +15,6 @@ __all__ = (
|
||||
'available_cpu_count',
|
||||
'byte_string_type',
|
||||
'datetime_infolabel',
|
||||
'default_quote',
|
||||
'default_quote_plus',
|
||||
'entity_escape',
|
||||
'generate_hash',
|
||||
'parse_qs',
|
||||
@@ -120,70 +118,6 @@ try:
|
||||
for ordinal in range(128, 256)
|
||||
})
|
||||
|
||||
|
||||
def default_quote(string,
|
||||
safe='',
|
||||
encoding=None,
|
||||
errors=None,
|
||||
_encoding='utf-8',
|
||||
_errors='strict',
|
||||
_reserved=reserved,
|
||||
_non_ascii=non_ascii,
|
||||
_encode=str.encode,
|
||||
_is_ascii=str.isascii,
|
||||
_replace=str.replace,
|
||||
_old='\\x',
|
||||
_new='%',
|
||||
_slice=slice(2, -1),
|
||||
_str=str,
|
||||
_translate=str.translate):
|
||||
_string = _translate(string, _reserved)
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
_string = _str(_encode(_string, _encoding, _errors))[_slice]
|
||||
if _string == string:
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
return _translate(_string, _non_ascii)
|
||||
if _is_ascii(_string):
|
||||
return _replace(_string, _old, _new)
|
||||
return _translate(_replace(_string, _old, _new), _non_ascii)
|
||||
|
||||
|
||||
def default_quote_plus(string,
|
||||
safe='',
|
||||
encoding=None,
|
||||
errors=None,
|
||||
_encoding='utf-8',
|
||||
_errors='strict',
|
||||
_reserved=reserved_plus,
|
||||
_non_ascii=non_ascii,
|
||||
_encode=str.encode,
|
||||
_is_ascii=str.isascii,
|
||||
_replace=str.replace,
|
||||
_old='\\x',
|
||||
_new='%',
|
||||
_slice=slice(2, -1),
|
||||
_str=str,
|
||||
_translate=str.translate):
|
||||
if (not safe and encoding is None and errors is None
|
||||
and isinstance(string, str)):
|
||||
_string = _translate(string, _reserved)
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
_string = _str(_encode(_string, _encoding, _errors))[_slice]
|
||||
if _string == string:
|
||||
if _is_ascii(_string):
|
||||
return _string
|
||||
return _translate(_string, _non_ascii)
|
||||
if _is_ascii(_string):
|
||||
return _replace(_string, _old, _new)
|
||||
return _translate(_replace(_string, _old, _new), _non_ascii)
|
||||
return quote_plus(string, safe, encoding, errors)
|
||||
|
||||
|
||||
urlencode.__defaults__ = (False, '', None, None, default_quote_plus)
|
||||
|
||||
# Compatibility shims for Kodi v18 and Python v2.7
|
||||
except ImportError:
|
||||
import cPickle as pickle
|
||||
@@ -220,16 +154,10 @@ except ImportError:
|
||||
return _quote(to_str(data), *args, **kwargs)
|
||||
|
||||
|
||||
default_quote = quote
|
||||
|
||||
|
||||
def quote_plus(data, *args, **kwargs):
|
||||
return _quote_plus(to_str(data), *args, **kwargs)
|
||||
|
||||
|
||||
default_quote_plus = quote_plus
|
||||
|
||||
|
||||
def unquote(data):
|
||||
return _unquote(to_str(data))
|
||||
|
||||
|
||||
Binary file not shown.
@@ -77,6 +77,7 @@ FOLDER_URI = 'FolderPath'
|
||||
HAS_FILES = 'HasFiles'
|
||||
HAS_FOLDERS = 'HasFolders'
|
||||
HAS_PARENT = 'HasParent'
|
||||
NUM_ALL_ITEMS = 'NumAllItems'
|
||||
SCROLLING = 'Scrolling'
|
||||
UPDATING = 'IsUpdating'
|
||||
|
||||
@@ -245,6 +246,7 @@ __all__ = (
|
||||
'HAS_FILES',
|
||||
'HAS_FOLDERS',
|
||||
'HAS_PARENT',
|
||||
'NUM_ALL_ITEMS',
|
||||
'SCROLLING',
|
||||
'UPDATING',
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -33,6 +33,9 @@ PLAYLIST = '/playlist'
|
||||
SUBSCRIPTIONS = '/subscriptions'
|
||||
VIDEO = '/video'
|
||||
|
||||
SETTINGS = '/config/youtube'
|
||||
SETUP_WIZARD = '/config/setup_wizard'
|
||||
|
||||
SPECIAL = '/special'
|
||||
DESCRIPTION_LINKS = SPECIAL + '/description_links'
|
||||
DISLIKED_VIDEOS = SPECIAL + '/disliked_videos'
|
||||
|
||||
@@ -32,9 +32,10 @@ HIDE_VIDEOS = 'youtube.view.hide_videos' # (list[str])
|
||||
SHORTS_DURATION = 'youtube.view.shorts.duration' # (int)
|
||||
FILTER_LIST = 'youtube.view.filter.list' # (str)
|
||||
|
||||
SUBSCRIPTIONS_FILTER_ENABLED = 'youtube.folder.my_subscriptions_filtered.show' # (bool)
|
||||
SUBSCRIPTIONS_FILTER_BLACKLIST = 'youtube.filter.my_subscriptions_filtered.blacklist' # (bool)
|
||||
SUBSCRIPTIONS_FILTER_LIST = 'youtube.filter.my_subscriptions_filtered.list' # (str)
|
||||
MY_SUBSCRIPTIONS_FILTER_ENABLED = 'youtube.folder.my_subscriptions_filtered.show' # (bool)
|
||||
MY_SUBSCRIPTIONS_FILTER_BLACKLIST = 'youtube.filter.my_subscriptions_filtered.blacklist' # (bool)
|
||||
MY_SUBSCRIPTIONS_FILTER_LIST = 'youtube.filter.my_subscriptions_filtered.list' # (str)
|
||||
MY_SUBSCRIPTIONS_SOURCES = 'youtube.folder.my_subscriptions.sources' # (list[str])
|
||||
|
||||
SAFE_SEARCH = 'kodion.safe.search' # (int)
|
||||
AGE_GATE = 'kodion.age.gate' # (bool)
|
||||
@@ -112,8 +113,16 @@ PLAY_COUNT_MIN_PERCENT = 'kodion.play_count.percent' # (int)
|
||||
|
||||
RATE_VIDEOS = 'youtube.post.play.rate' # (bool)
|
||||
RATE_PLAYLISTS = 'youtube.post.play.rate.playlists' # (bool)
|
||||
PLAY_REFRESH = 'youtube.post.play.refresh' # (bool)
|
||||
|
||||
AUTO_LIKE = 'youtube.post.play.auto_like' # (bool)
|
||||
AUTO_LIKE_FILTER_LIST = 'youtube.post.play.auto_like.filter.list' # (str)
|
||||
AUTO_LIKE_FILTER_STATE = 'youtube.post.play.auto_like.filter.state' # (int)
|
||||
|
||||
FILTER_DISABLED = 0
|
||||
FILTER_ENABLED = 1
|
||||
FILTER_BLACKLIST = 2
|
||||
|
||||
PLAY_REFRESH = 'youtube.post.play.refresh' # (bool)
|
||||
WATCH_LATER_REMOVE = 'youtube.playlist.watchlater.autoremove' # (bool)
|
||||
|
||||
VERIFY_SSL = 'requests.ssl.verify' # (bool)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -14,8 +14,8 @@ import os
|
||||
|
||||
from .. import logging
|
||||
from ..compatibility import (
|
||||
default_quote,
|
||||
parse_qsl,
|
||||
quote,
|
||||
string_type,
|
||||
to_str,
|
||||
unquote,
|
||||
@@ -230,13 +230,16 @@ class AbstractContext(object):
|
||||
def get_language():
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_language_name(self, lang_id=None):
|
||||
@classmethod
|
||||
def get_language_name(cls, lang_id=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_player_language(self):
|
||||
@classmethod
|
||||
def get_player_language(cls):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_subtitle_language(self):
|
||||
@classmethod
|
||||
def get_subtitle_language(cls):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_region(self):
|
||||
@@ -368,7 +371,8 @@ class AbstractContext(object):
|
||||
run=False,
|
||||
play=None,
|
||||
window=None,
|
||||
command=False):
|
||||
command=False,
|
||||
**kwargs):
|
||||
if isinstance(path, (list, tuple)):
|
||||
uri = self.create_path(*path, is_uri=True)
|
||||
elif path:
|
||||
@@ -391,7 +395,7 @@ class AbstractContext(object):
|
||||
params = urlencode([
|
||||
(
|
||||
('%' + param,
|
||||
','.join([default_quote(item) for item in value]))
|
||||
','.join([quote(item) for item in value]))
|
||||
if len(value) > 1 else
|
||||
(param, value[0])
|
||||
)
|
||||
@@ -402,15 +406,7 @@ class AbstractContext(object):
|
||||
uri = '?'.join((uri, params))
|
||||
|
||||
command = 'command://' if command else ''
|
||||
if run:
|
||||
return ''.join((command, 'RunPlugin(', uri, ')'))
|
||||
if play is not None:
|
||||
return ''.join((
|
||||
command,
|
||||
'PlayMedia(',
|
||||
uri,
|
||||
',playlist_type_hint=', str(play), ')',
|
||||
))
|
||||
|
||||
if window:
|
||||
if not isinstance(window, dict):
|
||||
window = {}
|
||||
@@ -441,6 +437,35 @@ class AbstractContext(object):
|
||||
',replace' if history_replace else '',
|
||||
')'
|
||||
))
|
||||
|
||||
kwargs = ',' + ','.join([
|
||||
'%s=%s' % (kwarg, value)
|
||||
if value is not None else
|
||||
kwarg
|
||||
for kwarg, value in kwargs.items()
|
||||
]) if kwargs else ''
|
||||
|
||||
if run:
|
||||
return ''.join((
|
||||
command,
|
||||
'RunAddon('
|
||||
if run == 'addon' else
|
||||
'RunScript('
|
||||
if run == 'script' else
|
||||
'RunPlugin(',
|
||||
uri,
|
||||
kwargs,
|
||||
')'
|
||||
))
|
||||
if play is not None:
|
||||
return ''.join((
|
||||
command,
|
||||
'PlayMedia(',
|
||||
uri,
|
||||
kwargs,
|
||||
',playlist_type_hint=', str(play),
|
||||
')',
|
||||
))
|
||||
return uri
|
||||
|
||||
def get_parent_uri(self, **kwargs):
|
||||
@@ -479,7 +504,7 @@ class AbstractContext(object):
|
||||
return ('/', parts) if include_parts else '/'
|
||||
|
||||
if kwargs.get('is_uri'):
|
||||
path = default_quote(path)
|
||||
path = quote(path)
|
||||
return (path, parts) if include_parts else path
|
||||
|
||||
def get_path(self):
|
||||
@@ -688,11 +713,15 @@ class AbstractContext(object):
|
||||
def tear_down(self):
|
||||
pass
|
||||
|
||||
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
|
||||
def ipc_exec(self,
|
||||
target,
|
||||
timeout=None,
|
||||
payload=None,
|
||||
raise_exc=False,
|
||||
stacklevel=2):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def is_plugin_folder(folder_name=None):
|
||||
def is_plugin_folder(self, folder_name=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def refresh_requested(self, force=False, on=False, off=False, params=None):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -10,9 +10,9 @@
|
||||
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
import atexit
|
||||
import json
|
||||
import sys
|
||||
from atexit import register as atexit_register
|
||||
from timeit import default_timer
|
||||
from weakref import proxy
|
||||
|
||||
@@ -32,6 +32,7 @@ from ...constants import (
|
||||
CHANNEL_ID,
|
||||
CONTENT,
|
||||
FOLDER_NAME,
|
||||
FOLDER_URI,
|
||||
PLAYLIST_ID,
|
||||
PLAY_FORCE_AUDIO,
|
||||
SERVICE_IPC,
|
||||
@@ -203,6 +204,7 @@ class XbmcContext(AbstractContext):
|
||||
'httpd.connect.wait': 13028,
|
||||
'httpd.connect.failed': 1001,
|
||||
'inputstreamhelper.is_installed': 30625,
|
||||
'internet.connection.required': 21451,
|
||||
'isa.enable.check': 30579,
|
||||
'key.requirement': 30731,
|
||||
'liked.video': 30716,
|
||||
@@ -376,7 +378,7 @@ class XbmcContext(AbstractContext):
|
||||
'video.play.timeshift': 30819,
|
||||
'video.play.using': 15213,
|
||||
'video.play.with_subtitles': 30702,
|
||||
'video.queue': 30511,
|
||||
'video.queue': 13347,
|
||||
'video.rate': 30528,
|
||||
'video.rate.dislike': 30530,
|
||||
'video.rate.like': 30529,
|
||||
@@ -460,7 +462,7 @@ class XbmcContext(AbstractContext):
|
||||
self._ui = None
|
||||
self._playlist = None
|
||||
|
||||
atexit.register(self.tear_down)
|
||||
atexit_register(self.tear_down)
|
||||
|
||||
def init(self):
|
||||
num_args = len(sys.argv)
|
||||
@@ -540,46 +542,90 @@ class XbmcContext(AbstractContext):
|
||||
return time_obj.strftime(str_format)
|
||||
|
||||
@staticmethod
|
||||
def get_language():
|
||||
language = xbmc.getLanguage(format=xbmc.ISO_639_1, region=True)
|
||||
lang_code, separator, region = language.partition('-')
|
||||
if not lang_code:
|
||||
language = xbmc.getLanguage(format=xbmc.ISO_639_2, region=False)
|
||||
lang_code, separator, region = language.partition('-')
|
||||
if lang_code != 'fil':
|
||||
lang_code = lang_code[:2]
|
||||
region = region[:2]
|
||||
if not lang_code:
|
||||
return 'en-US'
|
||||
def get_language(region=True, separator='-', code_format=xbmc.ISO_639_1):
|
||||
_code_format = xbmc.ISO_639_1
|
||||
_language = xbmc.getLanguage(format=_code_format, region=region)
|
||||
if region:
|
||||
return separator.join((lang_code.lower(), region.upper()))
|
||||
return lang_code
|
||||
|
||||
def get_language_name(self, lang_id=None):
|
||||
if lang_id is None:
|
||||
lang_id = self.get_language()
|
||||
return xbmc.convertLanguage(lang_id, xbmc.ENGLISH_NAME).split(';')[0]
|
||||
|
||||
def get_player_language(self):
|
||||
language = get_kodi_setting_value('locale.audiolanguage')
|
||||
if language == 'default':
|
||||
language = get_kodi_setting_value('locale.language')
|
||||
language = language.replace('resource.language.', '').split('_')[0]
|
||||
elif language not in self._KODI_UI_PLAYER_LANGUAGE_OPTIONS:
|
||||
language = xbmc.convertLanguage(language, xbmc.ISO_639_1)
|
||||
return language, get_kodi_setting_bool('videoplayer.preferdefaultflag')
|
||||
|
||||
def get_subtitle_language(self):
|
||||
language = get_kodi_setting_value('locale.subtitlelanguage')
|
||||
if language == 'default':
|
||||
language = get_kodi_setting_value('locale.language')
|
||||
language = language.replace('resource.language.', '').split('_')[0]
|
||||
elif language in self._KODI_UI_SUBTITLE_LANGUAGE_OPTIONS:
|
||||
language = None
|
||||
language, _, _region = _language.partition('-')
|
||||
else:
|
||||
language = xbmc.convertLanguage(language, xbmc.ISO_639_1)
|
||||
language = _language
|
||||
_region = None
|
||||
|
||||
if not language:
|
||||
_code_format = xbmc.ISO_639_2
|
||||
_language = xbmc.getLanguage(format=_code_format, region=False)
|
||||
if region:
|
||||
language, _, _region = _language.partition('-')
|
||||
_region = _region[:2]
|
||||
else:
|
||||
language = _language
|
||||
_region = None
|
||||
|
||||
if language:
|
||||
if code_format is not None and _code_format != code_format:
|
||||
_language = xbmc.convertLanguage(language, code_format)
|
||||
if _language:
|
||||
language = _language
|
||||
elif code_format == xbmc.ISO_639_1 and language != 'fil':
|
||||
language = language[:2]
|
||||
elif code_format == xbmc.ISO_639_2:
|
||||
language = 'eng'
|
||||
else:
|
||||
language = 'en'
|
||||
|
||||
if region:
|
||||
_region = _region.upper() if _region else 'US'
|
||||
return separator.join((language, _region))
|
||||
|
||||
return language
|
||||
|
||||
@classmethod
|
||||
def get_language_name(cls, language=None):
|
||||
if language is None:
|
||||
language = cls.get_language(code_format=None)
|
||||
return xbmc.convertLanguage(language, xbmc.ENGLISH_NAME).split(';')[0]
|
||||
|
||||
@classmethod
|
||||
def get_player_language(cls):
|
||||
language = get_kodi_setting_value('locale.audiolanguage')
|
||||
prefer_default = get_kodi_setting_bool('videoplayer.preferdefaultflag')
|
||||
if not language or language == 'default':
|
||||
language = get_kodi_setting_value('locale.language')
|
||||
if language:
|
||||
code = language.replace('resource.language.', '').split('_')[0]
|
||||
else:
|
||||
code = None
|
||||
elif language not in cls._KODI_UI_PLAYER_LANGUAGE_OPTIONS:
|
||||
code = xbmc.convertLanguage(language, xbmc.ISO_639_1)
|
||||
else:
|
||||
return language, prefer_default
|
||||
if not code:
|
||||
code = cls.get_language(
|
||||
region=False,
|
||||
code_format=xbmc.ISO_639_1,
|
||||
)
|
||||
return code, prefer_default
|
||||
|
||||
@classmethod
|
||||
def get_subtitle_language(cls):
|
||||
language = get_kodi_setting_value('locale.subtitlelanguage')
|
||||
if not language or language == 'default':
|
||||
language = get_kodi_setting_value('locale.language')
|
||||
if language:
|
||||
code = language.replace('resource.language.', '').split('_')[0]
|
||||
else:
|
||||
code = None
|
||||
elif language not in cls._KODI_UI_SUBTITLE_LANGUAGE_OPTIONS:
|
||||
code = xbmc.convertLanguage(language, xbmc.ISO_639_1)
|
||||
else:
|
||||
return None
|
||||
if not code:
|
||||
code = cls.get_language(
|
||||
region=False,
|
||||
code_format=xbmc.ISO_639_1,
|
||||
)
|
||||
return code
|
||||
|
||||
def reload_access_manager(self):
|
||||
access_manager = AccessManager(proxy(self))
|
||||
self._access_manager = access_manager
|
||||
@@ -683,8 +729,9 @@ class XbmcContext(AbstractContext):
|
||||
return result % _args
|
||||
except TypeError:
|
||||
self.log.exception(('Localization error',
|
||||
'text_id: {text_id!r}',
|
||||
'args: {original_args!r}'),
|
||||
'String: {result!r} ({text_id!r})',
|
||||
'args: {original_args!r}'),
|
||||
result=result,
|
||||
text_id=text_id,
|
||||
original_args=args)
|
||||
return result
|
||||
@@ -711,31 +758,30 @@ class XbmcContext(AbstractContext):
|
||||
xbmcplugin.setPluginCategory(self._plugin_handle, category_label)
|
||||
|
||||
detailed_labels = self.get_settings().show_detailed_labels()
|
||||
if content_type == CONTENT.VIDEO_CONTENT:
|
||||
if sub_type == CONTENT.HISTORY:
|
||||
self.add_sort_method(
|
||||
SORT.HISTORY_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.HISTORY_CONTENT_SIMPLE
|
||||
)
|
||||
elif sub_type == CONTENT.COMMENTS:
|
||||
self.add_sort_method(
|
||||
SORT.COMMENTS_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.COMMENTS_CONTENT_SIMPLE
|
||||
)
|
||||
elif sub_type == CONTENT.PLAYLIST:
|
||||
self.add_sort_method(
|
||||
SORT.PLAYLIST_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.PLAYLIST_CONTENT_SIMPLE
|
||||
)
|
||||
else:
|
||||
self.add_sort_method(
|
||||
SORT.VIDEO_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.VIDEO_CONTENT_SIMPLE
|
||||
)
|
||||
if sub_type == CONTENT.HISTORY:
|
||||
self.add_sort_method(
|
||||
SORT.HISTORY_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.HISTORY_CONTENT_SIMPLE
|
||||
)
|
||||
elif sub_type == CONTENT.COMMENTS:
|
||||
self.add_sort_method(
|
||||
SORT.COMMENTS_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.COMMENTS_CONTENT_SIMPLE
|
||||
)
|
||||
elif sub_type == CONTENT.PLAYLIST:
|
||||
self.add_sort_method(
|
||||
SORT.PLAYLIST_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.PLAYLIST_CONTENT_SIMPLE
|
||||
)
|
||||
elif content_type == CONTENT.VIDEO_CONTENT:
|
||||
self.add_sort_method(
|
||||
SORT.VIDEO_CONTENT_DETAILED
|
||||
if detailed_labels else
|
||||
SORT.VIDEO_CONTENT_SIMPLE
|
||||
)
|
||||
else:
|
||||
self.add_sort_method(
|
||||
SORT.LIST_CONTENT_DETAILED
|
||||
@@ -744,13 +790,19 @@ class XbmcContext(AbstractContext):
|
||||
)
|
||||
|
||||
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:
|
||||
xbmcplugin.addSortMethod(self._plugin_handle, *sort_method)
|
||||
_add_sort_method(handle, *sort_method)
|
||||
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:
|
||||
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):
|
||||
if not new_path:
|
||||
@@ -975,13 +1027,18 @@ class XbmcContext(AbstractContext):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
|
||||
def ipc_exec(self,
|
||||
target,
|
||||
timeout=None,
|
||||
payload=None,
|
||||
raise_exc=False,
|
||||
stacklevel=2):
|
||||
if not XbmcContextUI.get_property(SERVICE_RUNNING_FLAG, as_bool=True):
|
||||
msg = 'Service IPC - Monitor has not started'
|
||||
XbmcContextUI.set_property(SERVICE_RUNNING_FLAG, BUSY_FLAG)
|
||||
if raise_exc:
|
||||
raise RuntimeError(msg)
|
||||
self.log.warning_trace(msg)
|
||||
self.log.warning_trace(msg, stacklevel=stacklevel)
|
||||
return None
|
||||
|
||||
data = {'target': target, 'response_required': bool(timeout)}
|
||||
@@ -997,32 +1054,50 @@ class XbmcContext(AbstractContext):
|
||||
response = IPCMonitor(target, timeout)
|
||||
if response.received:
|
||||
value = response.value
|
||||
if value:
|
||||
self.log.debug(('Service IPC - Responded',
|
||||
'Procedure: {target!r}',
|
||||
'Latency: {latency:.2f}ms'),
|
||||
target=target,
|
||||
latency=response.latency)
|
||||
elif value is False:
|
||||
self.log.error_trace(('Service IPC - Failed',
|
||||
'Procedure: {target!r}',
|
||||
'Latency: {latency:.2f}ms'),
|
||||
target=target,
|
||||
latency=response.latency)
|
||||
if value is False:
|
||||
log_level = logging.ERROR
|
||||
log_value = 'FAILED'
|
||||
stack_info = True
|
||||
else:
|
||||
log_level = logging.DEBUG
|
||||
log_value = value
|
||||
stack_info = False
|
||||
self.log.log(
|
||||
level=log_level,
|
||||
msg='Service IPC <{target}({payload})>:'
|
||||
' {value} (in {time_ms:.2f}ms)',
|
||||
target=target,
|
||||
payload=payload,
|
||||
value=log_value,
|
||||
time_ms=response.latency,
|
||||
stack_info=stack_info,
|
||||
stacklevel=stacklevel,
|
||||
)
|
||||
else:
|
||||
value = None
|
||||
self.log.error_trace(('Service IPC - Timed out',
|
||||
'Procedure: {target!r}',
|
||||
'Timeout: {timeout:.2f}s'),
|
||||
target=target,
|
||||
timeout=timeout)
|
||||
self.log.error_trace(
|
||||
'Service IPC <{target}({payload})>:'
|
||||
' TIMED OUT (in {time_s:.2f}s)',
|
||||
target=target,
|
||||
payload=payload,
|
||||
time_s=timeout,
|
||||
stacklevel=stacklevel,
|
||||
)
|
||||
return value
|
||||
|
||||
def is_plugin_folder(self, folder_name=None):
|
||||
if folder_name is None:
|
||||
folder_name = XbmcContextUI.get_container_info(FOLDER_NAME,
|
||||
container_id=False)
|
||||
return folder_name == self._plugin_name
|
||||
def is_plugin_folder(self, folder_path='', name=False):
|
||||
if name:
|
||||
return XbmcContextUI.get_container_info(
|
||||
FOLDER_NAME,
|
||||
container_id=None,
|
||||
) == self._plugin_name
|
||||
return self.is_plugin_path(
|
||||
XbmcContextUI.get_container_info(
|
||||
FOLDER_URI,
|
||||
container_id=None,
|
||||
),
|
||||
folder_path,
|
||||
)
|
||||
|
||||
def refresh_requested(self, force=False, on=False, off=False, params=None):
|
||||
if params is None:
|
||||
|
||||
@@ -18,7 +18,7 @@ from cProfile import Profile
|
||||
from functools import wraps
|
||||
from inspect import getargvalues
|
||||
from os.path import normpath
|
||||
from pstats import Stats
|
||||
import pstats
|
||||
from traceback import extract_stack, format_list
|
||||
from weakref import ref
|
||||
|
||||
@@ -99,7 +99,6 @@ class Profiler(object):
|
||||
'_print_callees',
|
||||
'_profiler',
|
||||
'_reuse',
|
||||
'_sort_by',
|
||||
'_timer',
|
||||
)
|
||||
|
||||
@@ -121,14 +120,12 @@ class Profiler(object):
|
||||
num_lines=20,
|
||||
print_callees=False,
|
||||
reuse=False,
|
||||
sort_by=('cumulative', 'time'),
|
||||
timer=None):
|
||||
self._enabled = enabled
|
||||
self._num_lines = num_lines
|
||||
self._print_callees = print_callees
|
||||
self._profiler = None
|
||||
self._reuse = reuse
|
||||
self._sort_by = sort_by
|
||||
self._timer = timer
|
||||
|
||||
if enabled and not lazy:
|
||||
@@ -205,8 +202,7 @@ class Profiler(object):
|
||||
flush=True,
|
||||
num_lines=20,
|
||||
print_callees=False,
|
||||
reuse=False,
|
||||
sort_by=('cumulative', 'time')):
|
||||
reuse=False):
|
||||
if not (self._enabled and self._profiler):
|
||||
return None
|
||||
|
||||
@@ -218,10 +214,14 @@ class Profiler(object):
|
||||
self._profiler,
|
||||
stream=output_stream
|
||||
)
|
||||
stats.strip_dirs().sort_stats(*sort_by)
|
||||
stats.strip_dirs()
|
||||
if print_callees:
|
||||
stats.sort_stats('cumulative')
|
||||
stats.print_callees(num_lines)
|
||||
else:
|
||||
stats.sort_stats('cumpercall')
|
||||
stats.print_stats(num_lines)
|
||||
stats.sort_stats('totalpercall')
|
||||
stats.print_stats(num_lines)
|
||||
output = output_stream.getvalue()
|
||||
# Occurs when no stats were able to be generated from profiler
|
||||
@@ -242,7 +242,6 @@ class Profiler(object):
|
||||
num_lines=self._num_lines,
|
||||
print_callees=self._print_callees,
|
||||
reuse=self._reuse,
|
||||
sort_by=self._sort_by,
|
||||
),
|
||||
stacklevel=3)
|
||||
|
||||
@@ -250,6 +249,84 @@ class Profiler(object):
|
||||
self.__class__._instances.discard(self)
|
||||
|
||||
|
||||
class Stats(pstats.Stats):
|
||||
"""
|
||||
Custom Stats class that adds functionality to sort by
|
||||
- Cumulative time per call ("cumpercall")
|
||||
- Total time per call ("totalpercall")
|
||||
Code by alexnvdias from https://bugs.python.org/issue18795
|
||||
"""
|
||||
|
||||
_SortKey = getattr(pstats, 'SortKey', None)
|
||||
|
||||
sort_arg_dict_default = {
|
||||
"calls" : (((1,-1), ), "call count"),
|
||||
"ncalls" : (((1,-1), ), "call count"),
|
||||
"cumtime" : (((4,-1), ), "cumulative time"),
|
||||
"cumulative" : (((4,-1), ), "cumulative time"),
|
||||
"filename" : (((6, 1), ), "file name"),
|
||||
"line" : (((7, 1), ), "line number"),
|
||||
"module" : (((6, 1), ), "file name"),
|
||||
"name" : (((8, 1), ), "function name"),
|
||||
"nfl" : (((8, 1),(6, 1),(7, 1),), "name/file/line"),
|
||||
"pcalls" : (((0,-1), ), "primitive call count"),
|
||||
"stdname" : (((9, 1), ), "standard name"),
|
||||
"time" : (((2,-1), ), "internal time"),
|
||||
"tottime" : (((2,-1), ), "internal time"),
|
||||
"cumpercall" : (((5,-1), ), "cumulative time per call"),
|
||||
"totalpercall": (((3,-1), ), "total time per call"),
|
||||
}
|
||||
|
||||
def sort_stats(self, *field):
|
||||
if not field:
|
||||
self.fcn_list = 0
|
||||
return self
|
||||
if len(field) == 1 and isinstance(field[0], int):
|
||||
# Be compatible with old profiler
|
||||
field = [{-1: "stdname",
|
||||
0: "calls",
|
||||
1: "time",
|
||||
2: "cumulative"}[field[0]]]
|
||||
elif len(field) >= 2:
|
||||
for arg in field[1:]:
|
||||
if type(arg) != type(field[0]):
|
||||
raise TypeError("Can't have mixed argument type")
|
||||
|
||||
sort_arg_defs = self.get_sort_arg_defs()
|
||||
|
||||
sort_tuple = ()
|
||||
self.sort_type = ""
|
||||
connector = ""
|
||||
for word in field:
|
||||
if self._SortKey and isinstance(word, self._SortKey):
|
||||
word = word.value
|
||||
sort_tuple = sort_tuple + sort_arg_defs[word][0]
|
||||
self.sort_type += connector + sort_arg_defs[word][1]
|
||||
connector = ", "
|
||||
|
||||
stats_list = []
|
||||
for func, (cc, nc, tt, ct, callers) in self.stats.items():
|
||||
if nc == 0:
|
||||
npc = 0
|
||||
else:
|
||||
npc = float(tt) / nc
|
||||
|
||||
if cc == 0:
|
||||
cpc = 0
|
||||
else:
|
||||
cpc = float(ct) / cc
|
||||
|
||||
stats_list.append((cc, nc, tt, npc, ct, cpc) + func +
|
||||
(pstats.func_std_string(func), func))
|
||||
|
||||
stats_list.sort(key=pstats.cmp_to_key(pstats.TupleComp(sort_tuple).compare))
|
||||
|
||||
self.fcn_list = fcn_list = []
|
||||
for tuple in stats_list:
|
||||
fcn_list.append(tuple[-1])
|
||||
return self
|
||||
|
||||
|
||||
class ExecTimeout(object):
|
||||
log = logging.getLogger('__name__')
|
||||
src_file = None
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -28,7 +28,7 @@ class BaseItem(object):
|
||||
_version = 3
|
||||
_playable = False
|
||||
|
||||
def __init__(self, name, uri, image=None, fanart=None, **kwargs):
|
||||
def __init__(self, name, uri, image=None, fanart=None, **_kwargs):
|
||||
super(BaseItem, self).__init__()
|
||||
self._name = None
|
||||
self.set_name(name)
|
||||
@@ -348,12 +348,15 @@ class _Encoder(json.JSONEncoder):
|
||||
def encode(self, obj, nested=False):
|
||||
if isinstance(obj, (date, datetime)):
|
||||
class_name = obj.__class__.__name__
|
||||
if 'fromisoformat' in dir(obj):
|
||||
obj = {
|
||||
'__class__': class_name,
|
||||
'__isoformat__': obj.isoformat(),
|
||||
}
|
||||
else:
|
||||
try:
|
||||
if obj.fromisoformat:
|
||||
obj = {
|
||||
'__class__': class_name,
|
||||
'__isoformat__': obj.isoformat(),
|
||||
}
|
||||
else:
|
||||
raise AttributeError
|
||||
except AttributeError:
|
||||
if class_name == 'datetime':
|
||||
if obj.tzinfo:
|
||||
format_string = '%Y-%m-%dT%H:%M:%S%z'
|
||||
|
||||
@@ -459,6 +459,9 @@ class VideoItem(MediaItem):
|
||||
)
|
||||
self._directors = None
|
||||
self._imdb_id = None
|
||||
self._video_aspect = None
|
||||
self._video_height = None
|
||||
self._video_width = None
|
||||
|
||||
def add_directors(self, director):
|
||||
if director:
|
||||
@@ -481,3 +484,44 @@ class VideoItem(MediaItem):
|
||||
|
||||
def get_imdb_id(self):
|
||||
return self._imdb_id
|
||||
|
||||
def set_stream_details_from_player(self, player):
|
||||
height = player.get('embedHeight')
|
||||
width = player.get('embedWidth')
|
||||
if not height or not width:
|
||||
return
|
||||
|
||||
height = int(height)
|
||||
width = int(width)
|
||||
aspect_ratio = round(width / height, 2)
|
||||
|
||||
self._video_aspect = aspect_ratio
|
||||
self._video_height = height
|
||||
self._video_width = width
|
||||
|
||||
def get_stream_details(self):
|
||||
if self._video_aspect is None:
|
||||
return None
|
||||
return {
|
||||
'aspect': self._video_aspect,
|
||||
'height': self._video_height,
|
||||
'width': self._video_width,
|
||||
}
|
||||
|
||||
def set_aspect_ratio(self, aspect_ratio):
|
||||
self._video_aspect = round(aspect_ratio, 2)
|
||||
|
||||
def get_aspect_ratio(self):
|
||||
return self._video_aspect
|
||||
|
||||
def set_video_width(self, width):
|
||||
self._video_width = int(width)
|
||||
|
||||
def get_video_width(self):
|
||||
return self._video_width
|
||||
|
||||
def set_video_height(self, height):
|
||||
self._video_height = int(height)
|
||||
|
||||
def get_video_height(self):
|
||||
return self._video_height
|
||||
|
||||
@@ -46,12 +46,12 @@ URI_INFOLABEL = PROPERTY_AS_LABEL % URI
|
||||
VIDEO_ID_INFOLABEL = PROPERTY_AS_LABEL % VIDEO_ID
|
||||
|
||||
|
||||
def context_menu_uri(context, path, params=None):
|
||||
def context_menu_uri(context, path, params=None, run=True, play=False):
|
||||
if params is None:
|
||||
params = {CONTEXT_MENU: True}
|
||||
else:
|
||||
params[CONTEXT_MENU] = True
|
||||
return context.create_uri(path, params, run=True)
|
||||
return context.create_uri(path, params, run=run, play=play)
|
||||
|
||||
|
||||
def video_more_for(context,
|
||||
@@ -178,10 +178,16 @@ def folder_play(context, path, order='normal'):
|
||||
)
|
||||
|
||||
|
||||
def media_play(context):
|
||||
def media_play(context, video_id=VIDEO_ID_INFOLABEL):
|
||||
return (
|
||||
context.localize('video.play'),
|
||||
'Action(Play)'
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.PLAY,),
|
||||
{
|
||||
VIDEO_ID: video_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -901,3 +907,23 @@ def goto_page(context, params=None):
|
||||
params or context.get_params(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def open_settings(context):
|
||||
return (
|
||||
context.localize('settings'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
PATHS.SETTINGS,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def open_setup_wizard(context):
|
||||
return (
|
||||
context.localize('setup_wizard'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
PATHS.SETUP_WIZARD,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -71,6 +71,7 @@ class NextPageItem(DirectoryItem):
|
||||
image=image,
|
||||
fanart=fanart,
|
||||
category_label='__inherit__',
|
||||
special_sort='bottom',
|
||||
)
|
||||
|
||||
self.next_page = page
|
||||
|
||||
@@ -95,7 +95,7 @@ class NewSearchItem(DirectoryItem):
|
||||
channel_id='',
|
||||
addon_id='',
|
||||
location=False,
|
||||
**_kwargs):
|
||||
**kwargs):
|
||||
if not name:
|
||||
name = context.get_ui().bold(
|
||||
title or context.localize('search.new')
|
||||
@@ -120,7 +120,8 @@ class NewSearchItem(DirectoryItem):
|
||||
params=params,
|
||||
),
|
||||
image=image,
|
||||
fanart=fanart)
|
||||
fanart=fanart,
|
||||
**kwargs)
|
||||
|
||||
if context.is_plugin_path(context.get_uri(), ((PATHS.SEARCH, 'list'),)):
|
||||
context_menu = [
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -26,8 +26,8 @@ from ...constants import (
|
||||
BOOKMARK_ID,
|
||||
CHANNEL_ID,
|
||||
PATHS,
|
||||
PLAYLIST_ITEM_ID,
|
||||
PLAYLIST_ID,
|
||||
PLAYLIST_ITEM_ID,
|
||||
PLAY_COUNT_PROP,
|
||||
PLAY_STRM,
|
||||
PLAY_TIMESHIFT,
|
||||
@@ -37,14 +37,13 @@ from ...constants import (
|
||||
VIDEO_ID,
|
||||
)
|
||||
from ...utils.datetime import datetime_to_since, utc_to_local
|
||||
from ...utils.redact import redact_ip_in_uri
|
||||
from ...utils.system_version import current_system_version
|
||||
|
||||
|
||||
def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
stream_details = {}
|
||||
if not current_system_version.compatible(20):
|
||||
info_labels = {}
|
||||
info_type = None
|
||||
|
||||
if isinstance(item, MediaItem):
|
||||
if isinstance(item, VideoItem):
|
||||
@@ -58,6 +57,10 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
if value is not None:
|
||||
info_labels['season'] = value
|
||||
|
||||
value = item.get_aspect_ratio()
|
||||
if value is not None:
|
||||
stream_details['aspect'] = value
|
||||
|
||||
elif isinstance(item, AudioItem):
|
||||
info_type = 'music'
|
||||
|
||||
@@ -117,7 +120,7 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
if duration > 0:
|
||||
properties['TotalTime'] = str(duration)
|
||||
if info_type == 'video':
|
||||
list_item.addStreamInfo(info_type, {'duration': duration})
|
||||
stream_details['duration'] = duration
|
||||
info_labels['duration'] = duration
|
||||
|
||||
elif isinstance(item, DirectoryItem):
|
||||
@@ -177,8 +180,11 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
if properties:
|
||||
list_item.setProperties(properties)
|
||||
|
||||
if info_labels and info_type:
|
||||
list_item.setInfo(info_type, info_labels)
|
||||
if info_type:
|
||||
if info_labels:
|
||||
list_item.setInfo(info_type, info_labels)
|
||||
if stream_details:
|
||||
list_item.addStreamInfo(info_type, stream_details)
|
||||
return
|
||||
|
||||
if isinstance(item, MediaItem):
|
||||
@@ -229,6 +235,15 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
# if value is not None:
|
||||
# info_tag.setIMDBNumber(value)
|
||||
|
||||
# video width x height is not accurate, use aspect ratio only
|
||||
# value = item.get_stream_details()
|
||||
# if value is not None:
|
||||
# stream_details = value
|
||||
|
||||
value = item.get_aspect_ratio()
|
||||
if value is not None:
|
||||
stream_details['aspect'] = value
|
||||
|
||||
elif isinstance(item, AudioItem):
|
||||
info_tag = list_item.getMusicInfoTag()
|
||||
info_type = 'music'
|
||||
@@ -316,9 +331,7 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
else:
|
||||
info_tag.setResumePoint(resume_time)
|
||||
if duration > 0:
|
||||
info_tag.addVideoStream(xbmc.VideoStreamDetail(
|
||||
duration=duration,
|
||||
))
|
||||
stream_details['duration'] = duration
|
||||
elif info_type == 'music':
|
||||
# These properties are deprecated but there is no other way to set
|
||||
# these details for a ListItem with a MusicInfoTag
|
||||
@@ -409,12 +422,15 @@ def set_info(list_item, item, properties, set_play_count=True, resume=True):
|
||||
if properties:
|
||||
list_item.setProperties(properties)
|
||||
|
||||
if stream_details:
|
||||
info_tag.addVideoStream(xbmc.VideoStreamDetail(**stream_details))
|
||||
|
||||
|
||||
def playback_item(context, media_item, show_fanart=None, **_kwargs):
|
||||
uri = media_item.get_uri()
|
||||
logging.debug('Converting %s for playback: %r',
|
||||
media_item.__class__.__name__,
|
||||
redact_ip_in_uri(uri))
|
||||
logging.debug('Converting {type} for playback: {uri!u}',
|
||||
type=media_item.__class__.__name__,
|
||||
uri=uri)
|
||||
|
||||
params = context.get_params()
|
||||
settings = context.get_settings()
|
||||
@@ -439,6 +455,12 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs):
|
||||
}
|
||||
props = {
|
||||
'isPlayable': VALUE_TO_STR[media_item.playable],
|
||||
# ForceResolvePlugin was broken in Kodi v21+ after being added in
|
||||
# Kodi v20.
|
||||
# Set to false and use other workarounds as listitem is otherwise
|
||||
# resolved twice when using PlayMedia, Player.Open, etc. leading to
|
||||
# crashes or busy dialog workaround loops in Kodi 20.
|
||||
'ForceResolvePlugin': 'false',
|
||||
'playlist_type_hint': (
|
||||
xbmc.PLAYLIST_MUSIC
|
||||
if isinstance(media_item, AudioItem) else
|
||||
@@ -462,7 +484,8 @@ def playback_item(context, media_item, show_fanart=None, **_kwargs):
|
||||
props['inputstream.adaptive.stream_selection_type'] = 'manual-osd'
|
||||
elif 'auto' in stream_select:
|
||||
props['inputstream.adaptive.stream_selection_type'] = 'adaptive'
|
||||
props['inputstream.adaptive.chooser_resolution_max'] = 'auto'
|
||||
if current_system_version.compatible(21):
|
||||
props['inputstream.adaptive.chooser_resolution_max'] = 'auto'
|
||||
|
||||
if current_system_version.compatible(19):
|
||||
props['inputstream'] = 'inputstream.adaptive'
|
||||
@@ -562,32 +585,39 @@ def directory_listitem(context, directory_item, show_fanart=None, **_kwargs):
|
||||
if directory_item.next_page:
|
||||
props['specialSort'] = 'bottom'
|
||||
else:
|
||||
special_sort = directory_item.get_special_sort()
|
||||
if special_sort is None:
|
||||
_special_sort = directory_item.get_special_sort()
|
||||
if _special_sort is None:
|
||||
special_sort = 'top'
|
||||
elif special_sort is False:
|
||||
elif _special_sort is False:
|
||||
special_sort = None
|
||||
else:
|
||||
special_sort = _special_sort
|
||||
|
||||
prop_value = directory_item.subscription_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
special_sort = _special_sort
|
||||
props[SUBSCRIPTION_ID] = prop_value
|
||||
|
||||
prop_value = directory_item.channel_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
special_sort = _special_sort
|
||||
props[CHANNEL_ID] = prop_value
|
||||
|
||||
prop_value = directory_item.playlist_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
special_sort = _special_sort
|
||||
props[PLAYLIST_ID] = prop_value
|
||||
|
||||
prop_value = directory_item.bookmark_id
|
||||
if prop_value:
|
||||
special_sort = None
|
||||
special_sort = _special_sort
|
||||
props[BOOKMARK_ID] = prop_value
|
||||
|
||||
prop_value = is_action and getattr(directory_item, VIDEO_ID, None)
|
||||
if prop_value:
|
||||
special_sort = _special_sort
|
||||
props[VIDEO_ID] = prop_value
|
||||
|
||||
if special_sort:
|
||||
props['specialSort'] = special_sort
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
import errno
|
||||
from io import open
|
||||
|
||||
from .. import logging
|
||||
@@ -28,6 +29,7 @@ class JSONStore(object):
|
||||
_process_data = None
|
||||
|
||||
def __init__(self, filename, context):
|
||||
self._filename = filename
|
||||
if self.BASE_PATH:
|
||||
self.filepath = os.path.join(self.BASE_PATH, filename)
|
||||
else:
|
||||
@@ -43,34 +45,49 @@ class JSONStore(object):
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
if self.load(stacklevel=4):
|
||||
self._loaded = True
|
||||
self.set_defaults()
|
||||
else:
|
||||
self.set_defaults(reset=True)
|
||||
return self._loaded
|
||||
loaded = self.load(stacklevel=4, ipc=False)
|
||||
self.set_defaults(reset=(not loaded))
|
||||
return loaded
|
||||
|
||||
def set_defaults(self, reset=False):
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, data, update=False, process=True, ipc=True, stacklevel=2):
|
||||
loaded = self._loaded
|
||||
filepath = self.filepath
|
||||
if not filepath:
|
||||
return False
|
||||
try:
|
||||
if not filepath:
|
||||
raise IOError
|
||||
|
||||
if update:
|
||||
data = merge_dicts(self._data, data)
|
||||
if data == self._data:
|
||||
self.log.debug(('Data unchanged', 'File: %s'),
|
||||
self.log.debug(('Saving', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
self.log.debug(('Saving', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
try:
|
||||
|
||||
_data = self._data
|
||||
if loaded is False:
|
||||
loaded = self.load(stacklevel=4)
|
||||
if loaded:
|
||||
self.log.warning(('File state out of sync - data discarded',
|
||||
'File: {file}',
|
||||
'Old data: {old_data!p}',
|
||||
'New data: {new_data!p}'),
|
||||
file=filepath,
|
||||
old_data=_data,
|
||||
new_data=data,
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
|
||||
if update and _data:
|
||||
data = merge_dicts(_data, data)
|
||||
if not data:
|
||||
raise ValueError
|
||||
|
||||
if data == _data:
|
||||
self.log.debug(('Data unchanged', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
|
||||
_data = json.dumps(
|
||||
data, ensure_ascii=False, indent=4, sort_keys=True
|
||||
)
|
||||
@@ -79,11 +96,17 @@ class JSONStore(object):
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
|
||||
if loaded is False:
|
||||
self.log.debug(('File write deferred', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
|
||||
if ipc:
|
||||
self._context.get_ui().set_property(
|
||||
'-'.join((FILE_WRITE, filepath)),
|
||||
to_unicode(_data),
|
||||
log_value='<redacted>',
|
||||
log_redact='REDACTED',
|
||||
)
|
||||
response = self._context.ipc_exec(
|
||||
FILE_WRITE,
|
||||
@@ -103,7 +126,7 @@ class JSONStore(object):
|
||||
file.write(to_unicode(_data))
|
||||
except (RuntimeError, IOError, OSError):
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
filepath or self._filename,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
@@ -115,14 +138,17 @@ class JSONStore(object):
|
||||
return True
|
||||
|
||||
def load(self, process=True, ipc=True, stacklevel=2):
|
||||
loaded = False
|
||||
filepath = self.filepath
|
||||
if not filepath:
|
||||
return False
|
||||
|
||||
self.log.debug(('Loading', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
data = ''
|
||||
try:
|
||||
if not filepath:
|
||||
raise IOError
|
||||
|
||||
self.log.debug(('Loading', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
|
||||
if ipc:
|
||||
if self._context.ipc_exec(
|
||||
FILE_READ,
|
||||
@@ -132,7 +158,7 @@ class JSONStore(object):
|
||||
) is not False:
|
||||
data = self._context.get_ui().get_property(
|
||||
'-'.join((FILE_READ, filepath)),
|
||||
log_value='<redacted>',
|
||||
log_redact='REDACTED',
|
||||
)
|
||||
else:
|
||||
raise IOError
|
||||
@@ -145,17 +171,21 @@ class JSONStore(object):
|
||||
data,
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
except (RuntimeError, IOError, OSError):
|
||||
loaded = True
|
||||
except (RuntimeError, EnvironmentError, IOError, OSError) as exc:
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
filepath or self._filename,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
if exc.errno == errno.ENOENT:
|
||||
loaded = None
|
||||
except (TypeError, ValueError):
|
||||
self.log.exception(('Invalid data', 'Data: {data!r}'),
|
||||
data=data,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
return True
|
||||
loaded = None
|
||||
|
||||
self._loaded = loaded
|
||||
return loaded
|
||||
|
||||
def get_data(self, process=True, fallback=True, stacklevel=2):
|
||||
if not self._loaded:
|
||||
|
||||
@@ -20,7 +20,12 @@ from traceback import extract_stack, format_list
|
||||
|
||||
from .compatibility import StringIO, string_type, to_str, xbmc
|
||||
from .constants import ADDON_ID
|
||||
from .utils.convert_format import to_unicode
|
||||
from .utils.convert_format import to_unicode, urls_in_text
|
||||
from .utils.redact import (
|
||||
parse_and_redact_uri,
|
||||
redact_auth_header,
|
||||
redact_params,
|
||||
)
|
||||
from .utils.system_version import current_system_version
|
||||
|
||||
|
||||
@@ -45,7 +50,10 @@ __all__ = (
|
||||
|
||||
class RecordFormatter(logging.Formatter):
|
||||
def formatMessage(self, record):
|
||||
record.__dict__['__sep__'] = '\n' if '\n' in record.message else ' - '
|
||||
record.__dict__.setdefault(
|
||||
'__sep__',
|
||||
'\n' if record.stack_info or '\n' in record.message else ' - ',
|
||||
)
|
||||
try:
|
||||
return self._style.format(record)
|
||||
except AttributeError:
|
||||
@@ -66,24 +74,32 @@ class RecordFormatter(logging.Formatter):
|
||||
|
||||
def format(self, record):
|
||||
record.message = to_unicode(record.getMessage())
|
||||
|
||||
if self.usesTime():
|
||||
record.asctime = self.formatTime(record, self.datefmt)
|
||||
|
||||
s = self.formatMessage(record)
|
||||
|
||||
if record.stack_info:
|
||||
if not record.stack_text:
|
||||
stack_text = self.formatStack(record.stack_info)
|
||||
if getattr(record, '__redact_stack__', False):
|
||||
stack_text = urls_in_text(stack_text, parse_and_redact_uri)
|
||||
record.stack_text = stack_text
|
||||
if s[-1:] != '\n':
|
||||
s += '\n\n'
|
||||
s += record.stack_text
|
||||
|
||||
if record.exc_info:
|
||||
if not record.exc_text:
|
||||
record.exc_text = self.formatException(record.exc_info)
|
||||
if record.exc_text:
|
||||
if record.stack_info:
|
||||
if s[-1:] != '\n':
|
||||
s += '\n\n'
|
||||
s += self.formatStack(record.stack_info)
|
||||
exc_text = self.formatException(record.exc_info)
|
||||
if getattr(record, '__redact_exc__', False):
|
||||
exc_text = urls_in_text(exc_text, parse_and_redact_uri)
|
||||
record.exc_text = exc_text
|
||||
if s[-1:] != '\n':
|
||||
s += '\n\n'
|
||||
s += record.exc_text
|
||||
elif record.stack_info:
|
||||
if s[-1:] != '\n':
|
||||
s += '\n\n'
|
||||
s += self.formatStack(record.stack_info)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
@@ -153,9 +169,19 @@ class PrettyPrintFormatter(Formatter):
|
||||
_pretty_printer = VariableWidthPrettyPrinter(indent=4, width=160)
|
||||
|
||||
def convert_field(self, value, conversion):
|
||||
# redact headers
|
||||
if conversion == 'h':
|
||||
return self._pretty_printer.pformat(redact_auth_header(value))
|
||||
# redact setting
|
||||
if conversion == 'q':
|
||||
return self._pretty_printer.pformat(redact_params(value))[1:-1]
|
||||
# pretty printed repr
|
||||
if conversion == 'r':
|
||||
return self._pretty_printer.pformat(value)
|
||||
if conversion in {'d', 'e', 't', 'w'}:
|
||||
# redact params
|
||||
if conversion == 'p':
|
||||
return self._pretty_printer.pformat(redact_params(value))
|
||||
if conversion in {'d', 'e', 't', 'u', 'w'}:
|
||||
_sort_dicts = sort_dicts = getattr(self._pretty_printer,
|
||||
'_sort_dicts',
|
||||
None)
|
||||
@@ -187,6 +213,11 @@ class PrettyPrintFormatter(Formatter):
|
||||
_sort_dicts = False
|
||||
except AttributeError:
|
||||
pass
|
||||
# redact uri
|
||||
elif conversion == 'u':
|
||||
value = parse_and_redact_uri(value, redact_only=True)
|
||||
if sort_dicts:
|
||||
_sort_dicts = False
|
||||
# wide output
|
||||
elif conversion == 'w':
|
||||
self._pretty_printer._width = 2 * width
|
||||
@@ -251,7 +282,7 @@ class Handler(logging.Handler):
|
||||
)
|
||||
DEBUG_FORMATTER = RecordFormatter(
|
||||
fmt='[%(addon_id)s] %(module)s, line %(lineno)d, in %(funcName)s'
|
||||
'\n%(message)s',
|
||||
'%(__sep__)s%(message)s',
|
||||
)
|
||||
|
||||
_stack_info = False
|
||||
@@ -285,8 +316,7 @@ class Handler(logging.Handler):
|
||||
|
||||
class LogRecord(logging.LogRecord):
|
||||
def __init__(self, name, level, pathname, lineno, msg, args, exc_info,
|
||||
func=None, **kwargs):
|
||||
stack_info = kwargs.pop('sinfo', None)
|
||||
func=None, sinfo=None, extra=None, **kwargs):
|
||||
super(LogRecord, self).__init__(name,
|
||||
level,
|
||||
pathname,
|
||||
@@ -296,7 +326,21 @@ class LogRecord(logging.LogRecord):
|
||||
exc_info,
|
||||
func=func,
|
||||
**kwargs)
|
||||
self.stack_info = stack_info
|
||||
self.message = None
|
||||
self.asctime = None
|
||||
self.stack_info = sinfo
|
||||
self.stack_text = None
|
||||
if extra is not None:
|
||||
attrs = self.__dict__
|
||||
duplicate_attrs = set(extra).intersection(attrs.keys())
|
||||
if duplicate_attrs:
|
||||
raise KeyError(
|
||||
"Attempt to overwrite LogRecord attributes: ('%s',)" % (
|
||||
"', '".join(duplicate_attrs)
|
||||
)
|
||||
)
|
||||
else:
|
||||
attrs.update(extra)
|
||||
|
||||
if not current_system_version.compatible(19):
|
||||
def getMessage(self):
|
||||
@@ -337,7 +381,13 @@ class KodiLogger(logging.Logger):
|
||||
msg = MessageFormatter(msg, *args[1:-1], **kwargs)
|
||||
args = ()
|
||||
|
||||
stack_info = stack_info and (exc_info or self.stack_info)
|
||||
if stack_info:
|
||||
if exc_info or self.stack_info:
|
||||
pass
|
||||
elif stack_info == 'forced':
|
||||
stack_info = True
|
||||
else:
|
||||
stack_info = False
|
||||
sinfo = None
|
||||
if _srcfiles:
|
||||
try:
|
||||
@@ -394,21 +444,18 @@ class KodiLogger(logging.Logger):
|
||||
|
||||
def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
|
||||
func=None, extra=None, sinfo=None):
|
||||
rv = LogRecord(name,
|
||||
level,
|
||||
fn,
|
||||
lno,
|
||||
msg,
|
||||
args,
|
||||
exc_info,
|
||||
func=func,
|
||||
sinfo=sinfo)
|
||||
if extra is not None:
|
||||
for key in extra:
|
||||
if (key in ["message", "asctime"]) or (key in rv.__dict__):
|
||||
raise KeyError("Attempt to overwrite %r in LogRecord" % key)
|
||||
rv.__dict__[key] = extra[key]
|
||||
return rv
|
||||
return LogRecord(
|
||||
name,
|
||||
level,
|
||||
fn,
|
||||
lno,
|
||||
msg,
|
||||
args,
|
||||
exc_info,
|
||||
func=func,
|
||||
sinfo=sinfo,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def exception(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(ERROR):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -27,9 +27,10 @@ from ..constants import (
|
||||
VIDEO_ID,
|
||||
)
|
||||
from ..utils.redact import redact_params
|
||||
from ..utils.convert_format import channel_filter_split
|
||||
|
||||
|
||||
class PlayerMonitorThread(threading.Thread):
|
||||
class PlayerMonitorThread(object):
|
||||
def __init__(self, player, provider, context, monitor, player_data):
|
||||
self.player_data = player_data
|
||||
video_id = player_data.get(VIDEO_ID)
|
||||
@@ -53,11 +54,13 @@ class PlayerMonitorThread(threading.Thread):
|
||||
class_name=self.__class__.__name__,
|
||||
video_id=video_id,
|
||||
)
|
||||
self.name = name
|
||||
self.log = logging.getLogger(name)
|
||||
|
||||
super(PlayerMonitorThread, self).__init__(name=name)
|
||||
self.daemon = True
|
||||
self.start()
|
||||
thread = threading.Thread(name=name, target=self.run)
|
||||
self._thread = thread
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def abort_now(self):
|
||||
return (not self._player.isPlaying()
|
||||
@@ -298,22 +301,45 @@ class PlayerMonitorThread(threading.Thread):
|
||||
if history_id and history_id.lower() != 'hl':
|
||||
client.add_video_to_playlist(history_id, video_id)
|
||||
|
||||
# rate video
|
||||
if (settings.get_bool(settings.RATE_VIDEOS) and
|
||||
(settings.get_bool(settings.RATE_PLAYLISTS)
|
||||
or xbmc.PlayList(xbmc.PLAYLIST_VIDEO).size() < 2)):
|
||||
new_rating = False
|
||||
|
||||
# Auto like video
|
||||
if settings.auto_like_enabled():
|
||||
filter_state = settings.auto_like_filter_state()
|
||||
if filter_state == settings.FILTER_DISABLED:
|
||||
new_rating = 'like'
|
||||
else:
|
||||
_, filters_set, _ = channel_filter_split(
|
||||
settings.auto_like_filter()
|
||||
)
|
||||
if filters_set and self.channel_id and client.channel_match(
|
||||
identifier=self.channel_id,
|
||||
identifiers=filters_set,
|
||||
exclude=filter_state == settings.FILTER_BLACKLIST,
|
||||
):
|
||||
new_rating = 'like'
|
||||
|
||||
# Otherwise manually rate video
|
||||
if (not new_rating
|
||||
and settings.get_bool(settings.RATE_VIDEOS)
|
||||
and (settings.get_bool(settings.RATE_PLAYLISTS)
|
||||
or xbmc.PlayList(xbmc.PLAYLIST_VIDEO).size() < 2)):
|
||||
json_data = client.get_video_rating(video_id)
|
||||
if json_data:
|
||||
items = json_data.get('items', [{'rating': 'none'}])
|
||||
rating = items[0].get('rating', 'none')
|
||||
if rating == 'none':
|
||||
provider.on_video_x(
|
||||
provider,
|
||||
context,
|
||||
command='rate',
|
||||
video_id=video_id,
|
||||
current_rating=rating,
|
||||
)
|
||||
new_rating = None
|
||||
|
||||
if new_rating is not False:
|
||||
provider.on_video_x(
|
||||
provider,
|
||||
context,
|
||||
command='rate',
|
||||
video_id=video_id,
|
||||
current_rating='none',
|
||||
new_rating=new_rating,
|
||||
)
|
||||
|
||||
if settings.get_bool(settings.PLAY_REFRESH):
|
||||
context.send_notification(REFRESH_CONTAINER)
|
||||
@@ -334,6 +360,9 @@ class PlayerMonitorThread(threading.Thread):
|
||||
def ended(self):
|
||||
return self._ended.is_set()
|
||||
|
||||
def join(self, timeout=None):
|
||||
return self._thread.join(timeout)
|
||||
|
||||
|
||||
class PlayerMonitor(xbmc.Player):
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -410,7 +439,7 @@ class PlayerMonitor(xbmc.Player):
|
||||
|
||||
player_data = ui.pop_property(PLAYER_DATA,
|
||||
process=json.loads,
|
||||
log_process=redact_params)
|
||||
log_redact=True)
|
||||
if not player_data:
|
||||
return
|
||||
self.cleanup_threads()
|
||||
|
||||
@@ -14,7 +14,7 @@ from io import open
|
||||
from threading import Event, Lock, Thread
|
||||
|
||||
from .. import logging
|
||||
from ..compatibility import urlsplit, xbmc, xbmcgui
|
||||
from ..compatibility import urlsplit, xbmc
|
||||
from ..constants import (
|
||||
ACTION,
|
||||
ADDON_ID,
|
||||
@@ -22,10 +22,8 @@ from ..constants import (
|
||||
CONTAINER_FOCUS,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_POSITION,
|
||||
CURRENT_ITEM,
|
||||
FILE_READ,
|
||||
FILE_WRITE,
|
||||
HAS_PARENT,
|
||||
MARK_AS_LABEL,
|
||||
PATHS,
|
||||
PLAYBACK_STOPPED,
|
||||
@@ -88,28 +86,6 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
'message': method,
|
||||
'data': data})
|
||||
|
||||
def set_property(self,
|
||||
property_id,
|
||||
value='true',
|
||||
stacklevel=2,
|
||||
process=None,
|
||||
log_value=None,
|
||||
log_process=None,
|
||||
raw=False):
|
||||
if log_value is None:
|
||||
log_value = value
|
||||
if log_process:
|
||||
log_value = log_process(log_value)
|
||||
self.log.debug_trace('Set property {property_id!r}: {value!r}',
|
||||
property_id=property_id,
|
||||
value=log_value,
|
||||
stacklevel=stacklevel)
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
if process:
|
||||
value = process(value)
|
||||
xbmcgui.Window(10000).setProperty(_property_id, value)
|
||||
return value
|
||||
|
||||
def refresh_container(self, force=False):
|
||||
if force:
|
||||
self.refresh = False
|
||||
@@ -145,8 +121,7 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
if playing_file.path in {PATHS.MPD,
|
||||
PATHS.PLAY,
|
||||
PATHS.REDIRECT}:
|
||||
if not self.httpd:
|
||||
self.start_httpd()
|
||||
self.start_httpd()
|
||||
if self.httpd_sleep_allowed:
|
||||
self.httpd_sleep_allowed = None
|
||||
except RuntimeError:
|
||||
@@ -168,7 +143,7 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
'Params: {params}'),
|
||||
path=path,
|
||||
params=params)
|
||||
self.set_property(PLAY_FORCED)
|
||||
context.get_ui().set_property(PLAY_FORCED)
|
||||
elif params.get(ACTION) == 'list':
|
||||
playlist_player.stop()
|
||||
playlist_player.clear()
|
||||
@@ -177,7 +152,7 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
'Params: {params}'),
|
||||
path=path,
|
||||
params=params)
|
||||
self.set_property(PLAY_CANCELLED)
|
||||
context.get_ui().set_property(PLAY_CANCELLED)
|
||||
|
||||
return
|
||||
|
||||
@@ -201,7 +176,7 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
response = True
|
||||
|
||||
elif target == SERVER_WAKEUP:
|
||||
if not self.httpd and self.httpd_required():
|
||||
if self.httpd_required():
|
||||
response = self.start_httpd()
|
||||
else:
|
||||
response = bool(self.httpd)
|
||||
@@ -235,10 +210,10 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
with open(filepath, mode='r',
|
||||
encoding='utf-8') as file:
|
||||
read_access.wait()
|
||||
self.set_property(
|
||||
self._context.get_ui().set_property(
|
||||
'-'.join((FILE_READ, filepath)),
|
||||
file.read(),
|
||||
log_value='<redacted>',
|
||||
log_redact='REDACTED',
|
||||
)
|
||||
response = True
|
||||
except (IOError, OSError):
|
||||
@@ -247,7 +222,7 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
with write_access:
|
||||
content = self._context.get_ui().pop_property(
|
||||
'-'.join((FILE_WRITE, filepath)),
|
||||
log_value='<redacted>',
|
||||
log_redact='REDACTED',
|
||||
)
|
||||
response = None
|
||||
if content:
|
||||
@@ -292,7 +267,10 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
return
|
||||
|
||||
if data.get('play_data', {}).get('play_count'):
|
||||
self.set_property(PLAYER_VIDEO_ID, data.get(VIDEO_ID))
|
||||
self._context.get_ui().set_property(
|
||||
PLAYER_VIDEO_ID,
|
||||
data.get(VIDEO_ID),
|
||||
)
|
||||
|
||||
elif event == SYNC_LISTITEM:
|
||||
video_ids = json.loads(data) if data else None
|
||||
@@ -313,10 +291,12 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
play_count = ui.get_listitem_info(PLAY_COUNT)
|
||||
resumable = ui.get_listitem_bool(RESUMABLE)
|
||||
|
||||
self.set_property(MARK_AS_LABEL,
|
||||
context.localize('history.mark.unwatched')
|
||||
if play_count else
|
||||
context.localize('history.mark.watched'))
|
||||
ui.set_property(
|
||||
MARK_AS_LABEL,
|
||||
context.localize('history.mark.unwatched')
|
||||
if play_count else
|
||||
context.localize('history.mark.watched'),
|
||||
)
|
||||
|
||||
item_history = playback_history.get_item(video_id)
|
||||
if item_history:
|
||||
@@ -358,18 +338,21 @@ class ServiceMonitor(xbmc.Monitor):
|
||||
log_level = settings.log_level()
|
||||
if log_level:
|
||||
self.log.debugging = True
|
||||
# Verbose
|
||||
if log_level & 2:
|
||||
self.log.stack_info = True
|
||||
self.log.verbose_logging = True
|
||||
# Enabled or Auto on
|
||||
else:
|
||||
self.log.stack_info = False
|
||||
self.log.verbose_logging = False
|
||||
# Disabled or Auto off
|
||||
else:
|
||||
self.log.debugging = False
|
||||
self.log.stack_info = False
|
||||
self.log.verbose_logging = False
|
||||
|
||||
self.set_property(CHECK_SETTINGS)
|
||||
context.get_ui().set_property(CHECK_SETTINGS)
|
||||
self.refresh_container()
|
||||
|
||||
httpd_started = bool(self.httpd)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,14 +13,7 @@ import os
|
||||
import re
|
||||
import socket
|
||||
from collections import deque
|
||||
from errno import (
|
||||
ECONNABORTED,
|
||||
ECONNREFUSED,
|
||||
ECONNRESET,
|
||||
EPIPE,
|
||||
EPROTOTYPE,
|
||||
ESHUTDOWN,
|
||||
)
|
||||
from errno import errorcode
|
||||
from functools import partial
|
||||
from io import open
|
||||
from json import dumps as json_dumps, loads as json_loads
|
||||
@@ -39,11 +32,9 @@ from ..compatibility import (
|
||||
urlsplit,
|
||||
urlunsplit,
|
||||
xbmc,
|
||||
xbmcgui,
|
||||
xbmcvfs,
|
||||
)
|
||||
from ..constants import (
|
||||
ADDON_ID,
|
||||
LICENSE_TOKEN,
|
||||
LICENSE_URL,
|
||||
PATHS,
|
||||
@@ -78,9 +69,8 @@ class HTTPServer(ThreadingMixIn, TCPServer):
|
||||
and not wfile.closed
|
||||
and not select((), (wfile,), (), 0.1)[1]):
|
||||
pass
|
||||
if handler._close_all or wfile.closed:
|
||||
return
|
||||
handler.finish()
|
||||
if not handler._close_all and not wfile.closed:
|
||||
handler.finish()
|
||||
|
||||
def server_close(self):
|
||||
request_handler = self.RequestHandlerClass
|
||||
@@ -131,12 +121,17 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
}
|
||||
|
||||
SWALLOWED_ERRORS = {
|
||||
ECONNABORTED,
|
||||
ECONNREFUSED,
|
||||
ECONNRESET,
|
||||
EPIPE,
|
||||
EPROTOTYPE,
|
||||
ESHUTDOWN,
|
||||
'ECONNABORTED',
|
||||
'ECONNREFUSED',
|
||||
'ECONNRESET',
|
||||
'EPIPE',
|
||||
'EPROTOTYPE',
|
||||
'ESHUTDOWN',
|
||||
'WSAECONNABORTED',
|
||||
'WSAECONNREFUSED',
|
||||
'WSAECONNRESET',
|
||||
'WSAEPROTOTYPE',
|
||||
'WSAESHUTDOWN',
|
||||
}
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
@@ -179,10 +174,25 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
try:
|
||||
super(RequestHandler, self).handle_one_request()
|
||||
return
|
||||
except (HTTPError, OSError) as exc:
|
||||
except Exception as exc:
|
||||
self.close_connection = True
|
||||
if exc.errno not in self.SWALLOWED_ERRORS:
|
||||
raise exc
|
||||
self.log.exception('Request failed')
|
||||
if isinstance(exc, HTTPError):
|
||||
return
|
||||
error = getattr(exc, 'errno', None)
|
||||
if error and errorcode.get(error) in self.SWALLOWED_ERRORS:
|
||||
return
|
||||
raise exc
|
||||
|
||||
def finish(self):
|
||||
try:
|
||||
super(RequestHandler, self).finish()
|
||||
except Exception as exc:
|
||||
self.log.exception('File object failed to close cleanly')
|
||||
error = getattr(exc, 'errno', None)
|
||||
if error and errorcode.get(error) in self.SWALLOWED_ERRORS:
|
||||
return
|
||||
raise exc
|
||||
|
||||
def ip_address_status(self, ip_address):
|
||||
is_whitelisted = ip_address in self.whitelist_ips
|
||||
@@ -217,20 +227,22 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
client_ip = self.client_address[0]
|
||||
ip_allowed, is_local, is_whitelisted = self.ip_address_status(client_ip)
|
||||
|
||||
parts, params, log_uri, log_params = parse_and_redact_uri(self.path)
|
||||
uri = self.path
|
||||
parts, params, log_uri, log_params, log_path = parse_and_redact_uri(uri)
|
||||
path = {
|
||||
'full': self.path,
|
||||
'uri': uri,
|
||||
'path': parts.path,
|
||||
'query': parts.query,
|
||||
'params': params,
|
||||
'log_params': log_params,
|
||||
'log_uri': log_uri,
|
||||
'log_path': log_path,
|
||||
'log_params': log_params,
|
||||
}
|
||||
|
||||
if not path['path'].startswith(PATHS.PING):
|
||||
if not path['path'].startswith(PATHS.PING) and self.log.verbose_logging:
|
||||
self.log.debug(('{status}',
|
||||
'Method: {method!r}',
|
||||
'Path: {path[path]!r}',
|
||||
'Path: {path[log_path]!r}',
|
||||
'Params: {path[log_params]!r}',
|
||||
'Address: {client_ip!r}',
|
||||
'Whitelisted: {is_whitelisted}',
|
||||
@@ -382,13 +394,14 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
params = path['params']
|
||||
original_path = params.pop('__path', empty)[0] or '/videoplayback'
|
||||
request_servers = params.pop('__host', empty)
|
||||
stream_id = params.pop('__id', empty)[0]
|
||||
stream_id = params.pop('__id', empty)
|
||||
method = params.pop('__method', empty)[0] or 'POST'
|
||||
if original_path == '/videoplayback':
|
||||
stream_id = (stream_id, params.get('itag', empty)[0])
|
||||
stream_id += params.get('itag', empty)
|
||||
stream_id = tuple(stream_id)
|
||||
stream_type = params.get('mime', empty)[0]
|
||||
if stream_type:
|
||||
stream_type = stream_type.split('/')
|
||||
stream_type = tuple(stream_type.split('/'))
|
||||
else:
|
||||
stream_type = (None, None)
|
||||
ids = self.server_priority_list['stream_ids']
|
||||
@@ -413,11 +426,13 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
'list': priority_list,
|
||||
}
|
||||
elif original_path == '/api/timedtext':
|
||||
stream_type = (params.get('type', empty)[0],
|
||||
stream_id = tuple(stream_id)
|
||||
stream_type = (params.get('type', ['track'])[0],
|
||||
params.get('fmt', empty)[0],
|
||||
params.get('kind', empty)[0])
|
||||
priority_list = []
|
||||
else:
|
||||
stream_id = tuple(stream_id)
|
||||
stream_type = (None, None)
|
||||
priority_list = []
|
||||
|
||||
@@ -429,15 +444,51 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
else:
|
||||
headers = self.headers
|
||||
|
||||
byte_range = headers.get('Range')
|
||||
client = headers.get('X-YouTube-Client-Name')
|
||||
if self.log.debugging:
|
||||
if 'c' in params:
|
||||
if client:
|
||||
client = '%s (%s)' % (
|
||||
client,
|
||||
params.get('c', empty)[0],
|
||||
)
|
||||
else:
|
||||
client = params.get('c', empty)[0]
|
||||
|
||||
clen = params.get('clen', empty)[0]
|
||||
duration = params.get('dur', empty)[0]
|
||||
if (not byte_range
|
||||
or not clen
|
||||
or not duration
|
||||
or not byte_range.startswith('bytes=')):
|
||||
timestamp = ''
|
||||
else:
|
||||
try:
|
||||
timestamp = ' (~%.2fs)' % (
|
||||
float(duration)
|
||||
*
|
||||
int(byte_range[6:].split('-')[0])
|
||||
/
|
||||
int(clen)
|
||||
)
|
||||
except ValueError:
|
||||
timestamp = ''
|
||||
else:
|
||||
timestamp = ''
|
||||
|
||||
original_query_str = urlencode(params, doseq=True)
|
||||
|
||||
stream_redirect = settings.httpd_stream_redirect()
|
||||
|
||||
log_msg = ('Stream proxy response {success}',
|
||||
'Stream: {stream_id} - {stream_type}',
|
||||
'Method: {method!r}',
|
||||
'Server: {server!r}',
|
||||
'Target: {target!r}',
|
||||
'Status: {status} {reason}')
|
||||
'Status: {status} {reason}',
|
||||
'Client: {client}',
|
||||
'Range: {byte_range!r}{timestamp}')
|
||||
|
||||
response = None
|
||||
server = None
|
||||
@@ -487,11 +538,16 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
level=logging.WARNING,
|
||||
msg=log_msg,
|
||||
success='not OK',
|
||||
stream_id=stream_id,
|
||||
stream_type=stream_type,
|
||||
method=method,
|
||||
server=server,
|
||||
target=target,
|
||||
status=-1,
|
||||
reason='Failed',
|
||||
client=client,
|
||||
byte_range=byte_range,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
break
|
||||
with response:
|
||||
@@ -541,11 +597,16 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
level=log_level,
|
||||
msg=log_msg,
|
||||
success=('OK' if success else 'not OK'),
|
||||
stream_id=stream_id,
|
||||
stream_type=stream_type,
|
||||
method=method,
|
||||
server=server,
|
||||
target=target,
|
||||
status=status,
|
||||
reason=reason,
|
||||
client=client,
|
||||
byte_range=byte_range,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
if not success:
|
||||
@@ -631,17 +692,15 @@ class RequestHandler(BaseHTTPRequestHandler, object):
|
||||
self.send_error(403)
|
||||
return
|
||||
|
||||
empty = [None]
|
||||
|
||||
if path['path'].startswith(PATHS.DRM):
|
||||
home = xbmcgui.Window(10000)
|
||||
ui = self._context.get_ui()
|
||||
|
||||
lic_url = home.getProperty('-'.join((ADDON_ID, LICENSE_URL)))
|
||||
lic_url = ui.get_property(LICENSE_URL)
|
||||
if not lic_url:
|
||||
self.send_error(404)
|
||||
return
|
||||
|
||||
lic_token = home.getProperty('-'.join((ADDON_ID, LICENSE_TOKEN)))
|
||||
lic_token = ui.get_property(LICENSE_TOKEN)
|
||||
if not lic_token:
|
||||
self.send_error(403)
|
||||
return
|
||||
@@ -974,11 +1033,7 @@ def get_http_server(address, port, context):
|
||||
'Address: {address}:{port}'),
|
||||
address=address,
|
||||
port=port)
|
||||
xbmcgui.Dialog().notification(context.get_name(),
|
||||
str(exc),
|
||||
context.get_icon(),
|
||||
time=5000,
|
||||
sound=False)
|
||||
context.get_ui().show_notification(str(exc), audible=False)
|
||||
return None
|
||||
|
||||
|
||||
@@ -993,10 +1048,9 @@ def httpd_status(context, address=None):
|
||||
))
|
||||
if not RequestHandler.requests:
|
||||
RequestHandler.requests = BaseRequestsClass(context=context)
|
||||
result = None
|
||||
response = RequestHandler.requests.request(url, cache=False)
|
||||
if response is None:
|
||||
result = None
|
||||
else:
|
||||
if response is not None:
|
||||
with response:
|
||||
result = response.status_code
|
||||
if result == 204:
|
||||
|
||||
@@ -9,13 +9,22 @@
|
||||
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
import atexit
|
||||
import socket
|
||||
from atexit import register as atexit_register
|
||||
from collections import OrderedDict
|
||||
from os.path import exists, isdir
|
||||
|
||||
from requests import Request, Session
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
from requests.exceptions import InvalidJSONError, RequestException, URLRequired
|
||||
from requests.utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths
|
||||
from requests.hooks import default_hooks
|
||||
from requests.models import DEFAULT_REDIRECT_LIMIT, Request
|
||||
from requests.sessions import Session
|
||||
from requests.utils import (
|
||||
DEFAULT_CA_BUNDLE_PATH,
|
||||
cookiejar_from_dict,
|
||||
default_headers,
|
||||
extract_zipped_paths,
|
||||
)
|
||||
from urllib3.util.ssl_ import create_urllib3_context
|
||||
|
||||
from .. import logging
|
||||
@@ -44,13 +53,18 @@ class SSLHTTPAdapter(HTTPAdapter):
|
||||
(socket.IPPROTO_TCP, getattr(socket, 'TCP_USER_TIMEOUT', None), 600),
|
||||
)
|
||||
|
||||
_ssl_context = create_urllib3_context()
|
||||
_ssl_context.load_verify_locations(
|
||||
capath=extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
|
||||
)
|
||||
_SSL_CONTEXT = create_urllib3_context()
|
||||
_CA_PATH = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
|
||||
if not _CA_PATH or not exists(_CA_PATH):
|
||||
_SSL_CONTEXT.load_default_certs()
|
||||
else:
|
||||
if isdir(_CA_PATH):
|
||||
_SSL_CONTEXT.load_verify_locations(capath=_CA_PATH)
|
||||
else:
|
||||
_SSL_CONTEXT.load_verify_locations(cafile=_CA_PATH)
|
||||
|
||||
def init_poolmanager(self, *args, **kwargs):
|
||||
kwargs['ssl_context'] = self._ssl_context
|
||||
kwargs['ssl_context'] = self._SSL_CONTEXT
|
||||
|
||||
kwargs['socket_options'] = [
|
||||
socket_option for socket_option in self._SOCKET_OPTIONS
|
||||
@@ -60,26 +74,94 @@ class SSLHTTPAdapter(HTTPAdapter):
|
||||
return super(SSLHTTPAdapter, self).init_poolmanager(*args, **kwargs)
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
self._ssl_context.check_hostname = bool(verify)
|
||||
return super(SSLHTTPAdapter, self).cert_verify(conn, url, verify, cert)
|
||||
if verify:
|
||||
self._SSL_CONTEXT.check_hostname = True
|
||||
conn.cert_reqs = str('CERT_REQUIRED')
|
||||
else:
|
||||
self._SSL_CONTEXT.check_hostname = False
|
||||
conn.cert_reqs = str('CERT_NONE')
|
||||
conn.ca_certs = None
|
||||
conn.ca_cert_dir = None
|
||||
|
||||
|
||||
class CustomSession(Session):
|
||||
def __init__(self):
|
||||
#: A case-insensitive dictionary of headers to be sent on each
|
||||
#: :class:`Request <Request>` sent from this
|
||||
#: :class:`Session <Session>`.
|
||||
self.headers = default_headers()
|
||||
|
||||
#: Default Authentication tuple or object to attach to
|
||||
#: :class:`Request <Request>`.
|
||||
self.auth = None
|
||||
|
||||
#: Dictionary mapping protocol or protocol and host to the URL of the proxy
|
||||
#: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
|
||||
#: be used on each :class:`Request <Request>`.
|
||||
self.proxies = {}
|
||||
|
||||
#: Event-handling hooks.
|
||||
self.hooks = default_hooks()
|
||||
|
||||
#: Dictionary of querystring data to attach to each
|
||||
#: :class:`Request <Request>`. The dictionary values may be lists for
|
||||
#: representing multivalued query parameters.
|
||||
self.params = {}
|
||||
|
||||
#: Stream response content default.
|
||||
self.stream = False
|
||||
|
||||
#: SSL Verification default.
|
||||
#: Defaults to `True`, requiring requests to verify the TLS certificate at the
|
||||
#: remote end.
|
||||
#: If verify is set to `False`, requests will accept any TLS certificate
|
||||
#: presented by the server, and will ignore hostname mismatches and/or
|
||||
#: expired certificates, which will make your application vulnerable to
|
||||
#: man-in-the-middle (MitM) attacks.
|
||||
#: Only set this to `False` for testing.
|
||||
self.verify = True
|
||||
|
||||
#: SSL client certificate default, if String, path to ssl client
|
||||
#: cert file (.pem). If Tuple, ('cert', 'key') pair.
|
||||
self.cert = None
|
||||
|
||||
#: Maximum number of redirects allowed. If the request exceeds this
|
||||
#: limit, a :class:`TooManyRedirects` exception is raised.
|
||||
#: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is
|
||||
#: 30.
|
||||
self.max_redirects = DEFAULT_REDIRECT_LIMIT
|
||||
|
||||
#: Trust environment settings for proxy configuration, default
|
||||
#: authentication and similar.
|
||||
#: CustomSession.trust_env is False
|
||||
self.trust_env = False
|
||||
|
||||
#: A CookieJar containing all currently outstanding cookies set on this
|
||||
#: session. By default it is a
|
||||
#: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but
|
||||
#: may be any other ``cookielib.CookieJar`` compatible object.
|
||||
self.cookies = cookiejar_from_dict({})
|
||||
|
||||
# Default connection adapters.
|
||||
self.adapters = OrderedDict()
|
||||
self.mount('https://', SSLHTTPAdapter(
|
||||
pool_maxsize=20,
|
||||
pool_block=True,
|
||||
max_retries=Retry(
|
||||
total=3,
|
||||
backoff_factor=0.1,
|
||||
status_forcelist={500, 502, 503, 504},
|
||||
allowed_methods=None,
|
||||
)
|
||||
))
|
||||
self.mount('http://', HTTPAdapter())
|
||||
|
||||
|
||||
class BaseRequestsClass(object):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_session = Session()
|
||||
_session.trust_env = False
|
||||
_session.mount('https://', SSLHTTPAdapter(
|
||||
pool_maxsize=10,
|
||||
pool_block=True,
|
||||
max_retries=Retry(
|
||||
total=3,
|
||||
backoff_factor=0.1,
|
||||
status_forcelist={500, 502, 503, 504},
|
||||
allowed_methods=None,
|
||||
)
|
||||
))
|
||||
atexit.register(_session.close)
|
||||
_session = CustomSession()
|
||||
atexit_register(_session.close)
|
||||
|
||||
_context = None
|
||||
_verify = True
|
||||
@@ -246,15 +328,19 @@ class BaseRequestsClass(object):
|
||||
cookies=cookies,
|
||||
hooks=hooks,
|
||||
))
|
||||
elif prepared_request:
|
||||
method = prepared_request.method
|
||||
url = prepared_request.url
|
||||
headers = prepared_request.headers
|
||||
|
||||
if stream:
|
||||
cache = False
|
||||
if cache is not False:
|
||||
if prepared_request:
|
||||
method = prepared_request.method
|
||||
if cache is True or method in self.METHODS_TO_CACHE:
|
||||
headers = prepared_request.headers
|
||||
request_id = generate_hash(
|
||||
method,
|
||||
prepared_request.url,
|
||||
url,
|
||||
headers,
|
||||
prepared_request.body,
|
||||
)
|
||||
@@ -335,7 +421,7 @@ class BaseRequestsClass(object):
|
||||
|
||||
log_msg = [
|
||||
'{title}',
|
||||
'URL: {method} {url}',
|
||||
'URL: {method} {url!u}',
|
||||
'Status: {response_status} - {response_reason}',
|
||||
'Response: {response_text}',
|
||||
]
|
||||
@@ -377,6 +463,7 @@ class BaseRequestsClass(object):
|
||||
response_reason=response_reason,
|
||||
response_text=response_text,
|
||||
stacklevel=stacklevel,
|
||||
extra={'__redact_exc__': True},
|
||||
**kwargs)
|
||||
|
||||
if raise_exc:
|
||||
@@ -390,27 +477,28 @@ class BaseRequestsClass(object):
|
||||
raise raise_exc
|
||||
raise exc
|
||||
|
||||
if cache:
|
||||
if cached_response is not None:
|
||||
self.log.debug(('Using cached response',
|
||||
'Request ID: {request_id}',
|
||||
'Etag: {etag}',
|
||||
'Modified: {timestamp}'),
|
||||
request_id=request_id,
|
||||
etag=etag,
|
||||
timestamp=timestamp,
|
||||
stacklevel=stacklevel)
|
||||
cache.set(request_id)
|
||||
response = cached_response
|
||||
else:
|
||||
self.log.debug(('Saving response to cache',
|
||||
'Request ID: {request_id}',
|
||||
'Etag: {etag}',
|
||||
'Modified: {timestamp}'),
|
||||
request_id=request_id,
|
||||
etag=etag,
|
||||
timestamp=timestamp,
|
||||
stacklevel=stacklevel)
|
||||
cache.set(request_id, response, etag)
|
||||
if not cache:
|
||||
pass
|
||||
elif cached_response is not None:
|
||||
self.log.debug(('Using cached response',
|
||||
'Request ID: {request_id}',
|
||||
'Etag: {etag}',
|
||||
'Modified: {timestamp}'),
|
||||
request_id=request_id,
|
||||
etag=etag,
|
||||
timestamp=timestamp,
|
||||
stacklevel=stacklevel)
|
||||
cache.set(request_id)
|
||||
response = cached_response
|
||||
elif response is not None:
|
||||
self.log.debug(('Saving response to cache',
|
||||
'Request ID: {request_id}',
|
||||
'Etag: {etag}',
|
||||
'Modified: {timestamp}'),
|
||||
request_id=request_id,
|
||||
etag=etag,
|
||||
timestamp=timestamp,
|
||||
stacklevel=stacklevel)
|
||||
cache.set(request_id, response, etag)
|
||||
|
||||
return response
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,7 +19,6 @@ from ...constants import (
|
||||
CONTAINER_FOCUS,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_POSITION,
|
||||
FOLDER_URI,
|
||||
FORCE_PLAY_PARAMS,
|
||||
PATHS,
|
||||
PLAYBACK_FAILED,
|
||||
@@ -80,6 +79,15 @@ class XbmcPlugin(AbstractPlugin):
|
||||
def __init__(self):
|
||||
super(XbmcPlugin, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def end(handle, succeeded=True, update_listing=False, cache_to_disc=True):
|
||||
xbmcplugin.endOfDirectory(
|
||||
handle=handle,
|
||||
succeeded=succeeded,
|
||||
updateListing=update_listing,
|
||||
cacheToDisc=cache_to_disc,
|
||||
)
|
||||
|
||||
def run(self,
|
||||
provider,
|
||||
context,
|
||||
@@ -209,6 +217,8 @@ class XbmcPlugin(AbstractPlugin):
|
||||
)
|
||||
else:
|
||||
result, options = provider.navigate(context)
|
||||
logging.debug('Plugin runner options: {options!r}',
|
||||
options=options)
|
||||
if ui.get_property(REROUTE_PATH):
|
||||
xbmcplugin.endOfDirectory(
|
||||
handle,
|
||||
@@ -381,9 +391,7 @@ class XbmcPlugin(AbstractPlugin):
|
||||
},
|
||||
)
|
||||
else:
|
||||
if context.is_plugin_path(
|
||||
ui.get_container_info(FOLDER_URI, container_id=None)
|
||||
):
|
||||
if context.is_plugin_folder():
|
||||
_, _post_run_action = self.uri_action(
|
||||
context,
|
||||
context.get_parent_uri(params={
|
||||
@@ -466,8 +474,9 @@ class XbmcPlugin(AbstractPlugin):
|
||||
def post_run(context, ui, *actions, **kwargs):
|
||||
timeout = kwargs.get('timeout', 30)
|
||||
interval = kwargs.get('interval', 0.1)
|
||||
busy = True
|
||||
for action in actions:
|
||||
while not ui.get_container(container_type=None, check_ready=True):
|
||||
while not ui.get_container(container_type=False, check_ready=True):
|
||||
timeout -= interval
|
||||
if timeout < 0:
|
||||
logging.error('Container not ready'
|
||||
@@ -475,12 +484,17 @@ class XbmcPlugin(AbstractPlugin):
|
||||
break
|
||||
context.sleep(interval)
|
||||
else:
|
||||
if busy:
|
||||
busy = ui.clear_property(BUSY_FLAG)
|
||||
if isinstance(action, tuple):
|
||||
action, action_kwargs = action
|
||||
else:
|
||||
action_kwargs = None
|
||||
logging.debug('Executing queued post-run action: {action}',
|
||||
logging.debug(('Executing queued post-run action',
|
||||
'Action: {action}',
|
||||
'Arguments: {action_kwargs!p}'),
|
||||
action=action,
|
||||
action_kwargs=action_kwargs,
|
||||
stacklevel=2)
|
||||
if callable(action):
|
||||
if action_kwargs:
|
||||
@@ -508,7 +522,7 @@ class XbmcPlugin(AbstractPlugin):
|
||||
result = True
|
||||
|
||||
elif uri.startswith('PlayMedia('):
|
||||
log_action = 'Redirect for playback queued'
|
||||
log_action = 'PlayMedia queued'
|
||||
log_uri = uri[len('PlayMedia('):-1].split(',')
|
||||
log_uri[0] = parse_and_redact_uri(
|
||||
log_uri[0],
|
||||
@@ -575,20 +589,32 @@ class XbmcPlugin(AbstractPlugin):
|
||||
action = uri
|
||||
result = False
|
||||
|
||||
elif context.is_plugin_path(uri, PATHS.PLAY):
|
||||
parts, params, log_uri, _ = parse_and_redact_uri(uri)
|
||||
if params.get(ACTION, [None])[0] == 'list':
|
||||
elif context.is_plugin_path(uri):
|
||||
parts, params, log_uri, _, _ = parse_and_redact_uri(uri)
|
||||
path = parts.path.rstrip('/')
|
||||
|
||||
if path != PATHS.PLAY:
|
||||
log_action = 'Redirect queued'
|
||||
action = context.create_uri(
|
||||
(PATHS.ROUTE, parts.path.rstrip('/')),
|
||||
(PATHS.ROUTE, path or PATHS.HOME),
|
||||
params,
|
||||
run=True,
|
||||
)
|
||||
result = False
|
||||
|
||||
elif params.get(ACTION, [None])[0] == 'list':
|
||||
log_action = 'Redirect for listing queued'
|
||||
action = context.create_uri(
|
||||
(PATHS.ROUTE, path),
|
||||
params,
|
||||
run=True,
|
||||
)
|
||||
result = False
|
||||
|
||||
else:
|
||||
log_action = 'Redirect for playback queued'
|
||||
action = context.create_uri(
|
||||
(parts.path.rstrip('/'),),
|
||||
path,
|
||||
params,
|
||||
play=(xbmc.PLAYLIST_MUSIC
|
||||
if (context.get_ui().get_property(PLAY_FORCE_AUDIO)
|
||||
@@ -597,16 +623,6 @@ class XbmcPlugin(AbstractPlugin):
|
||||
)
|
||||
result = True
|
||||
|
||||
elif context.is_plugin_path(uri):
|
||||
log_action = 'Redirect queued'
|
||||
parts, params, log_uri, _ = parse_and_redact_uri(uri)
|
||||
action = context.create_uri(
|
||||
(PATHS.ROUTE, parts.path.rstrip('/') or PATHS.HOME),
|
||||
params,
|
||||
run=True,
|
||||
)
|
||||
result = False
|
||||
|
||||
else:
|
||||
action = None
|
||||
result = False
|
||||
|
||||
@@ -14,11 +14,14 @@ import gc
|
||||
|
||||
from . import logging
|
||||
from .constants import (
|
||||
BUSY_FLAG,
|
||||
CHECK_SETTINGS,
|
||||
FOLDER_URI,
|
||||
FORCE_PLAY_PARAMS,
|
||||
PATHS,
|
||||
SORT_DIR,
|
||||
SORT_METHOD,
|
||||
TRAKT_PAUSE_FLAG,
|
||||
)
|
||||
from .context import XbmcContext
|
||||
from .debug import Profiler
|
||||
@@ -43,8 +46,6 @@ def run(context=_context,
|
||||
plugin=_plugin,
|
||||
provider=_provider,
|
||||
profiler=_profiler):
|
||||
gc.disable()
|
||||
|
||||
ui = context.get_ui()
|
||||
|
||||
if ui.pop_property(CHECK_SETTINGS):
|
||||
@@ -56,13 +57,16 @@ def run(context=_context,
|
||||
log_level = settings.log_level()
|
||||
if log_level:
|
||||
log.debugging = True
|
||||
# Verbose
|
||||
if log_level & 2:
|
||||
log.stack_info = True
|
||||
log.verbose_logging = True
|
||||
# Enabled or Auto on
|
||||
else:
|
||||
log.stack_info = False
|
||||
log.verbose_logging = False
|
||||
profiler.enable(flush=True)
|
||||
# Disabled or Auto off
|
||||
else:
|
||||
log.debugging = False
|
||||
log.stack_info = False
|
||||
@@ -71,6 +75,7 @@ def run(context=_context,
|
||||
|
||||
old_path = context.get_path().rstrip('/')
|
||||
old_uri = ui.get_container_info(FOLDER_URI, container_id=None)
|
||||
old_handle = context.get_handle()
|
||||
context.init()
|
||||
current_path = context.get_path().rstrip('/')
|
||||
current_params = context.get_original_params()
|
||||
@@ -82,7 +87,7 @@ def run(context=_context,
|
||||
params = context.get_params()
|
||||
refresh = context.refresh_requested(params=params)
|
||||
was_playing = old_path == PATHS.PLAY
|
||||
is_same_path = current_path == old_path
|
||||
is_same_path = current_path == old_path and old_handle != -1
|
||||
|
||||
if was_playing or is_same_path or refresh:
|
||||
old_path, old_params = context.parse_uri(
|
||||
@@ -121,18 +126,13 @@ def run(context=_context,
|
||||
if new_params:
|
||||
context.set_params(**new_params)
|
||||
|
||||
log_params = params.copy()
|
||||
for key in ('api_key', 'client_id', 'client_secret'):
|
||||
if key in log_params:
|
||||
log_params[key] = '<redacted>'
|
||||
|
||||
system_version = context.get_system_version()
|
||||
log.info(('Running v{version}',
|
||||
log.info(('Running v{version} (unofficial)',
|
||||
'Kodi: v{kodi}',
|
||||
'Python: v{python}',
|
||||
'Handle: {handle}',
|
||||
'Path: {path!r} ({path_link})',
|
||||
'Params: {params!r}',
|
||||
'Params: {params!p}',
|
||||
'Forced: {forced!r}'),
|
||||
version=context.get_version(),
|
||||
kodi=str(system_version),
|
||||
@@ -140,17 +140,31 @@ def run(context=_context,
|
||||
handle=current_handle,
|
||||
path=current_path,
|
||||
path_link='linked' if is_same_path else 'new',
|
||||
params=log_params,
|
||||
params=params,
|
||||
forced=forced)
|
||||
|
||||
plugin.run(provider,
|
||||
context,
|
||||
forced=forced,
|
||||
is_same_path=is_same_path,
|
||||
**new_kwargs)
|
||||
|
||||
if log_level:
|
||||
profiler.print_stats()
|
||||
|
||||
gc.enable()
|
||||
gc.collect()
|
||||
gc_threshold = gc.get_threshold()
|
||||
gc.set_threshold(0)
|
||||
try:
|
||||
plugin.run(provider,
|
||||
context,
|
||||
forced=forced,
|
||||
is_same_path=is_same_path,
|
||||
**new_kwargs)
|
||||
except Exception:
|
||||
log.exception('Error')
|
||||
ui.clear_property(BUSY_FLAG)
|
||||
ui.clear_property(TRAKT_PAUSE_FLAG, raw=True)
|
||||
for param in FORCE_PLAY_PARAMS:
|
||||
ui.clear_property(param)
|
||||
plugin.end(
|
||||
context.get_handle(),
|
||||
succeeded=False,
|
||||
update_listing=True,
|
||||
cache_to_disc=False,
|
||||
)
|
||||
finally:
|
||||
if log_level:
|
||||
profiler.print_stats()
|
||||
gc.collect()
|
||||
gc.set_threshold(*gc_threshold)
|
||||
|
||||
@@ -481,19 +481,22 @@ def run(argv):
|
||||
log_level = context.get_settings().log_level()
|
||||
if log_level:
|
||||
log.debugging = True
|
||||
# Verbose
|
||||
if log_level & 2:
|
||||
log.stack_info = True
|
||||
log.verbose_logging = True
|
||||
# Enabled or Auto on
|
||||
else:
|
||||
log.stack_info = False
|
||||
log.verbose_logging = False
|
||||
# Disabled or Auto off
|
||||
else:
|
||||
log.debugging = False
|
||||
log.stack_info = False
|
||||
log.verbose_logging = False
|
||||
|
||||
system_version = context.get_system_version()
|
||||
log.info(('Running v{version}',
|
||||
log.info(('Running v{version} (unofficial)',
|
||||
'Kodi: v{kodi}',
|
||||
'Python: v{python}',
|
||||
'Category: {category!r}',
|
||||
|
||||
@@ -51,7 +51,7 @@ def run():
|
||||
monitor=monitor)
|
||||
|
||||
system_version = context.get_system_version()
|
||||
logging.info(('Starting v{version}',
|
||||
logging.info(('Starting v{version} (unofficial)',
|
||||
'Kodi: v{kodi}',
|
||||
'Python: v{python}'),
|
||||
version=context.get_version(),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -110,8 +110,9 @@ class AbstractSettings(object):
|
||||
def setup_wizard_enabled(self, value=None):
|
||||
# Set run_required to release date (as Unix timestamp in seconds)
|
||||
# to enable oneshot on first run
|
||||
# Tuesday, 8 April 2025 12:00:00 AM = 1744070400
|
||||
run_required = 1744070400
|
||||
# 2026/01/10 @ 12:00 AM
|
||||
# datetime(2026,01,10,0,0).timestamp() = 1767970800
|
||||
run_required = 1767970800
|
||||
|
||||
if value is False:
|
||||
self.set_int(SETTINGS.SETUP_WIZARD_RUNS, run_required)
|
||||
@@ -163,6 +164,8 @@ class AbstractSettings(object):
|
||||
return self.get_bool(SETTINGS.SUBTITLE_DOWNLOAD, False)
|
||||
|
||||
def audio_only(self):
|
||||
if self.ask_for_video_quality():
|
||||
return False
|
||||
return self.get_bool(SETTINGS.AUDIO_ONLY, False)
|
||||
|
||||
def get_subtitle_selection(self):
|
||||
@@ -464,7 +467,8 @@ class AbstractSettings(object):
|
||||
ip_address = '.'.join(map(str, octets))
|
||||
|
||||
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
|
||||
|
||||
def httpd_whitelist(self):
|
||||
@@ -573,10 +577,19 @@ class AbstractSettings(object):
|
||||
reverse=True)
|
||||
if value >= key]
|
||||
|
||||
def stream_features(self, value=None):
|
||||
def max_video_height(self):
|
||||
if self.use_mpd_videos():
|
||||
qualities = self.mpd_video_qualities()
|
||||
return qualities[0]['nom_height']
|
||||
return self.fixed_video_quality()
|
||||
|
||||
def stream_features(self, value=None, raw_values=False):
|
||||
if value is not None:
|
||||
return self.set_string_list(SETTINGS.MPD_STREAM_FEATURES, value)
|
||||
return frozenset(self.get_string_list(SETTINGS.MPD_STREAM_FEATURES))
|
||||
stream_features = self.get_string_list(SETTINGS.MPD_STREAM_FEATURES)
|
||||
if raw_values:
|
||||
return stream_features
|
||||
return frozenset(stream_features)
|
||||
|
||||
_STREAM_SELECT = {
|
||||
1: 'auto',
|
||||
@@ -673,22 +686,74 @@ class AbstractSettings(object):
|
||||
|
||||
return filter_types
|
||||
|
||||
def subscriptions_sources(self,
|
||||
value=None,
|
||||
default=('subscriptions',
|
||||
'saved_playlists',
|
||||
'bookmark_channels',
|
||||
'bookmark_playlists'),
|
||||
match_values=True,
|
||||
raw_values=False):
|
||||
if value is not None:
|
||||
return self.set_string_list(SETTINGS.MY_SUBSCRIPTIONS_SOURCES,
|
||||
value)
|
||||
sources = self.get_string_list(SETTINGS.MY_SUBSCRIPTIONS_SOURCES)
|
||||
if default:
|
||||
if not sources:
|
||||
sources = default
|
||||
if match_values and not raw_values:
|
||||
return tuple([
|
||||
source in sources
|
||||
for source in default
|
||||
])
|
||||
if raw_values:
|
||||
return sources
|
||||
return frozenset(sources)
|
||||
|
||||
def subscriptions_filter_enabled(self, value=None):
|
||||
if value is not None:
|
||||
return self.set_bool(SETTINGS.SUBSCRIPTIONS_FILTER_ENABLED, value)
|
||||
return self.get_bool(SETTINGS.SUBSCRIPTIONS_FILTER_ENABLED, True)
|
||||
return self.set_bool(
|
||||
SETTINGS.MY_SUBSCRIPTIONS_FILTER_ENABLED, value
|
||||
)
|
||||
return self.get_bool(SETTINGS.MY_SUBSCRIPTIONS_FILTER_ENABLED, True)
|
||||
|
||||
def subscriptions_filter_blacklist(self, value=None):
|
||||
if value is not None:
|
||||
return self.set_bool(SETTINGS.SUBSCRIPTIONS_FILTER_BLACKLIST, value)
|
||||
return self.get_bool(SETTINGS.SUBSCRIPTIONS_FILTER_BLACKLIST, True)
|
||||
return self.set_bool(
|
||||
SETTINGS.MY_SUBSCRIPTIONS_FILTER_BLACKLIST, value
|
||||
)
|
||||
return self.get_bool(SETTINGS.MY_SUBSCRIPTIONS_FILTER_BLACKLIST, True)
|
||||
|
||||
def subscriptions_filter(self, value=None):
|
||||
if value is not None:
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
value = ','.join(value).lstrip(',')
|
||||
return self.set_string(SETTINGS.SUBSCRIPTIONS_FILTER_LIST, value)
|
||||
return self.get_string(SETTINGS.SUBSCRIPTIONS_FILTER_LIST).replace(
|
||||
return self.set_string(SETTINGS.MY_SUBSCRIPTIONS_FILTER_LIST, value)
|
||||
return self.get_string(SETTINGS.MY_SUBSCRIPTIONS_FILTER_LIST).replace(
|
||||
', ', ','
|
||||
)
|
||||
|
||||
def auto_like_enabled(self, value=None):
|
||||
if value is not None:
|
||||
return self.set_bool(
|
||||
SETTINGS.AUTO_LIKE, value
|
||||
)
|
||||
return self.get_bool(SETTINGS.AUTO_LIKE, False)
|
||||
|
||||
def auto_like_filter_state(self, value=None):
|
||||
default = SETTINGS.FILTER_DISABLED
|
||||
if value is not None:
|
||||
return self.set_int(
|
||||
SETTINGS.AUTO_LIKE_FILTER_STATE, value
|
||||
)
|
||||
return self.get_int(SETTINGS.AUTO_LIKE_FILTER_STATE, default)
|
||||
|
||||
def auto_like_filter(self, value=None):
|
||||
if value is not None:
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
value = ','.join(value).lstrip(',')
|
||||
return self.set_string(SETTINGS.AUTO_LIKE_FILTER_LIST, value)
|
||||
return self.get_string(SETTINGS.AUTO_LIKE_FILTER_LIST).replace(
|
||||
', ', ','
|
||||
)
|
||||
|
||||
@@ -760,8 +825,12 @@ class AbstractSettings(object):
|
||||
def log_level(self, value=None):
|
||||
if value is not None:
|
||||
return self.set_int(SETTINGS.LOG_LEVEL, value)
|
||||
return (self.get_int(SETTINGS.LOG_LEVEL, 0)
|
||||
or get_kodi_setting_bool('debug.showloginfo'))
|
||||
log_level = self.get_int(SETTINGS.LOG_LEVEL, 0)
|
||||
if not log_level:
|
||||
return get_kodi_setting_bool('debug.showloginfo')
|
||||
if log_level & 4:
|
||||
log_level = 0
|
||||
return log_level
|
||||
|
||||
def exec_limit(self, value=None):
|
||||
if value is not None:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -68,7 +68,8 @@ class SettingsProxy(object):
|
||||
return self.ref.setSettingString(*args, **kwargs)
|
||||
|
||||
def get_str_list(self, setting):
|
||||
return self.ref.getSetting(setting).split(',')
|
||||
value = self.ref.getSetting(setting)
|
||||
return value.split(',') if value else []
|
||||
|
||||
def set_str_list(self, setting, value):
|
||||
value = ','.join(value)
|
||||
@@ -248,22 +249,9 @@ class XbmcPluginSettings(AbstractSettings):
|
||||
value = default
|
||||
|
||||
if echo_level and self._echo_level:
|
||||
if setting == self.LOCATION:
|
||||
log_value = 'xx.xxxx,xx.xxxx'
|
||||
elif setting == self.API_ID:
|
||||
log_value = ('...'.join((value[:3], value[-5:]))
|
||||
if len(value) > 11 else
|
||||
'...')
|
||||
elif setting in {self.API_KEY, self.API_SECRET}:
|
||||
log_value = ('...'.join((value[:3], value[-3:]))
|
||||
if len(value) > 9 else
|
||||
'...')
|
||||
else:
|
||||
log_value = value
|
||||
self.log.debug_trace('Get setting {name!r}:'
|
||||
' {value!r} (str, {state})',
|
||||
name=setting,
|
||||
value=log_value,
|
||||
log_setting = {setting: value}
|
||||
self.log.debug_trace('Get setting {setting!q} (str, {state})',
|
||||
setting=log_setting,
|
||||
state=(error if error else 'success'),
|
||||
stacklevel=echo_level)
|
||||
self._cache[setting] = value
|
||||
@@ -281,22 +269,9 @@ class XbmcPluginSettings(AbstractSettings):
|
||||
error = exc
|
||||
|
||||
if echo_level and self._echo_level:
|
||||
if setting == self.LOCATION:
|
||||
log_value = 'xx.xxxx,xx.xxxx'
|
||||
elif setting == self.API_ID:
|
||||
log_value = ('...'.join((value[:3], value[-5:]))
|
||||
if len(value) > 11 else
|
||||
'...')
|
||||
elif setting in {self.API_KEY, self.API_SECRET}:
|
||||
log_value = ('...'.join((value[:3], value[-3:]))
|
||||
if len(value) > 9 else
|
||||
'...')
|
||||
else:
|
||||
log_value = value
|
||||
self.log.debug_trace('Set setting {name!r}:'
|
||||
' {value!r} (str, {state})',
|
||||
name=setting,
|
||||
value=log_value,
|
||||
log_setting = {setting: value}
|
||||
self.log.debug_trace('Set setting {setting!q} (str, {state})',
|
||||
setting=log_setting,
|
||||
state=(error if error else 'success'),
|
||||
stacklevel=echo_level)
|
||||
return not error
|
||||
@@ -308,7 +283,7 @@ class XbmcPluginSettings(AbstractSettings):
|
||||
error = False
|
||||
try:
|
||||
value = self._proxy.get_str_list(setting)
|
||||
if not value:
|
||||
if not isinstance(value, list):
|
||||
value = [] if default is None else default
|
||||
except (RuntimeError, TypeError) as exc:
|
||||
error = exc
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -38,5 +38,5 @@ class BookmarksList(Storage):
|
||||
def _optimize_item_count(self, limit=-1, defer=False):
|
||||
return False
|
||||
|
||||
def _optimize_file_size(self, limit=-1, defer=False):
|
||||
def _optimize_file_size(self, defer=False, db=None):
|
||||
return False
|
||||
|
||||
@@ -18,6 +18,8 @@ class DataCache(Storage):
|
||||
_table_updated = False
|
||||
_sql = {}
|
||||
|
||||
_memory_store = {}
|
||||
|
||||
def __init__(self, filepath, max_file_size_mb=5):
|
||||
max_file_size_kb = max_file_size_mb * 1024
|
||||
super(DataCache, self).__init__(filepath,
|
||||
@@ -27,25 +29,11 @@ class DataCache(Storage):
|
||||
content_ids,
|
||||
seconds=None,
|
||||
as_dict=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
|
||||
values_only=True):
|
||||
result = self._get_by_ids(content_ids,
|
||||
seconds=seconds,
|
||||
as_dict=as_dict,
|
||||
values_only=values_only)
|
||||
if in_memory_result:
|
||||
result.update(in_memory_result)
|
||||
return result
|
||||
|
||||
def get_items_like(self, content_id, seconds=None):
|
||||
@@ -70,11 +58,11 @@ class DataCache(Storage):
|
||||
result = self._get(content_id, seconds=seconds, as_dict=as_dict)
|
||||
return result
|
||||
|
||||
def set_item(self, content_id, item):
|
||||
self._set(content_id, item)
|
||||
def set_item(self, content_id, item, defer=False, flush=False):
|
||||
self._set(content_id, item, defer=defer, flush=flush)
|
||||
|
||||
def set_items(self, items):
|
||||
self._set_many(items)
|
||||
def set_items(self, items, defer=False, flush=False):
|
||||
self._set_many(items, defer=defer, flush=flush)
|
||||
|
||||
def del_item(self, content_id):
|
||||
self._remove(content_id)
|
||||
|
||||
@@ -17,6 +17,8 @@ class FeedHistory(Storage):
|
||||
_table_updated = False
|
||||
_sql = {}
|
||||
|
||||
_memory_store = {}
|
||||
|
||||
def __init__(self, filepath):
|
||||
super(FeedHistory, self).__init__(filepath)
|
||||
|
||||
@@ -32,10 +34,10 @@ class FeedHistory(Storage):
|
||||
return result
|
||||
|
||||
def set_items(self, items):
|
||||
self._set_many(items)
|
||||
self._set_many(items, defer=True)
|
||||
|
||||
def _optimize_item_count(self, limit=-1, defer=False):
|
||||
return False
|
||||
|
||||
def _optimize_file_size(self, limit=-1, defer=False):
|
||||
def _optimize_file_size(self, defer=False, db=None):
|
||||
return False
|
||||
|
||||
@@ -25,10 +25,10 @@ class PlaybackHistory(Storage):
|
||||
value['last_played'] = fromtimestamp(item[1])
|
||||
return value
|
||||
|
||||
def get_items(self, keys=None, limit=-1, process=None, excluding=None):
|
||||
def get_items(self, item_ids=(), limit=-1, process=None, excluding=None):
|
||||
if process is None:
|
||||
process = self._add_last_played
|
||||
result = self._get_by_ids(keys,
|
||||
result = self._get_by_ids(item_ids=item_ids,
|
||||
excluding=excluding,
|
||||
oldest_first=False,
|
||||
process=process,
|
||||
@@ -40,8 +40,8 @@ class PlaybackHistory(Storage):
|
||||
result = self._get(key, process=self._add_last_played)
|
||||
return result
|
||||
|
||||
def set_item(self, video_id, play_data, timestamp=None):
|
||||
self._set(video_id, play_data, timestamp)
|
||||
def set_item(self, video_id, play_data):
|
||||
self._set(video_id, play_data)
|
||||
|
||||
def del_item(self, video_id):
|
||||
self._remove(video_id)
|
||||
@@ -52,5 +52,5 @@ class PlaybackHistory(Storage):
|
||||
def _optimize_item_count(self, limit=-1, defer=False):
|
||||
return False
|
||||
|
||||
def _optimize_file_size(self, limit=-1, defer=False):
|
||||
def _optimize_file_size(self, defer=False, db=None):
|
||||
return False
|
||||
|
||||
@@ -18,6 +18,8 @@ class RequestCache(Storage):
|
||||
_table_updated = False
|
||||
_sql = {}
|
||||
|
||||
_memory_store = {}
|
||||
|
||||
def __init__(self, filepath, max_file_size_mb=20):
|
||||
max_file_size_kb = max_file_size_mb * 1024
|
||||
super(RequestCache, self).__init__(filepath,
|
||||
@@ -34,11 +36,11 @@ class RequestCache(Storage):
|
||||
if response:
|
||||
item = (etag, response)
|
||||
if timestamp:
|
||||
self._update(request_id, item, timestamp)
|
||||
self._update(request_id, item, timestamp, defer=True)
|
||||
else:
|
||||
self._set(request_id, item)
|
||||
self._set(request_id, item, defer=True)
|
||||
else:
|
||||
self._refresh(request_id, timestamp)
|
||||
self._refresh(request_id, timestamp, defer=True)
|
||||
|
||||
def _optimize_item_count(self, limit=-1, defer=False):
|
||||
return False
|
||||
|
||||
@@ -13,31 +13,66 @@ from __future__ import absolute_import, division, unicode_literals
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
from atexit import register as atexit_register
|
||||
from threading import RLock, Timer
|
||||
|
||||
from .. import logging
|
||||
from ..compatibility import pickle, to_str
|
||||
from ..utils.datetime import fromtimestamp, since_epoch
|
||||
from ..utils.file_system import make_dirs
|
||||
from ..utils.system_version import current_system_version
|
||||
|
||||
|
||||
class StorageLock(object):
|
||||
def __init__(self):
|
||||
self._lock = RLock()
|
||||
self._num_accessing = 0
|
||||
self._num_waiting = 0
|
||||
|
||||
def __enter__(self):
|
||||
self._num_waiting += 1
|
||||
self._lock.acquire()
|
||||
self._num_waiting -= 1
|
||||
if current_system_version.compatible(19):
|
||||
def __enter__(self):
|
||||
self._num_waiting += 1
|
||||
locked = not self._lock.acquire(timeout=3)
|
||||
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):
|
||||
self._lock.release()
|
||||
try:
|
||||
self._lock.release()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def accessing(self, start=False, done=False):
|
||||
num = self._num_accessing
|
||||
if start:
|
||||
num += 1
|
||||
elif done and num > 0:
|
||||
num -= 1
|
||||
self._num_accessing = num
|
||||
return num > 0
|
||||
|
||||
def waiting(self):
|
||||
return self._num_waiting > 0
|
||||
|
||||
|
||||
class ExistingDBConnection(object):
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def __enter__(self):
|
||||
db = self._db
|
||||
return db, db.cursor() if db else None
|
||||
|
||||
def __exit__(self, *excinfo):
|
||||
pass
|
||||
|
||||
|
||||
class Storage(object):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -162,6 +197,11 @@ class Storage(object):
|
||||
' ) <= {{0}}'
|
||||
' );'
|
||||
),
|
||||
'prune_invalid': (
|
||||
'DELETE'
|
||||
' FROM {table}'
|
||||
' WHERE key IS NULL;'
|
||||
),
|
||||
'refresh': (
|
||||
'UPDATE'
|
||||
' {table}'
|
||||
@@ -206,11 +246,13 @@ class Storage(object):
|
||||
self.uuid = filepath[1]
|
||||
self._filepath = os.path.join(*filepath)
|
||||
self._db = None
|
||||
self._cursor = None
|
||||
self._lock = StorageLock()
|
||||
self._memory_store = getattr(self.__class__, '_memory_store', None)
|
||||
self._close_timer = None
|
||||
self._close_actions = False
|
||||
self._max_item_count = -1 if migrate else max_item_count
|
||||
self._max_file_size_kb = -1 if migrate else max_file_size_kb
|
||||
atexit_register(self._close, event='shutdown')
|
||||
|
||||
if migrate:
|
||||
self._base = self
|
||||
@@ -248,59 +290,88 @@ class Storage(object):
|
||||
def set_max_file_size_kb(self, max_file_size_kb):
|
||||
self._max_file_size_kb = max_file_size_kb
|
||||
|
||||
if current_system_version.compatible(19):
|
||||
def __del__(self):
|
||||
self._close(event='deleted')
|
||||
|
||||
def __enter__(self):
|
||||
self._lock.accessing(start=True)
|
||||
|
||||
close_timer = self._close_timer
|
||||
if close_timer:
|
||||
close_timer.cancel()
|
||||
self._close_timer = None
|
||||
if self._db and self._cursor:
|
||||
return self._db, self._cursor
|
||||
return self._open()
|
||||
|
||||
db = self._db or self._open()
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
except (AttributeError, sqlite3.ProgrammingError):
|
||||
db = self._open()
|
||||
cursor = db.cursor()
|
||||
cursor.arraysize = 100
|
||||
return db, cursor
|
||||
|
||||
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
close_timer = self._close_timer
|
||||
if close_timer:
|
||||
close_timer.cancel()
|
||||
if self._lock.waiting():
|
||||
self._close_timer = None
|
||||
|
||||
if self._lock.accessing(done=True) or self._lock.waiting():
|
||||
return
|
||||
close_timer = Timer(5, self._close)
|
||||
close_timer.daemon = True
|
||||
close_timer.start()
|
||||
self._close_timer = close_timer
|
||||
|
||||
with self._lock as locked:
|
||||
if locked or self._close_timer:
|
||||
return
|
||||
close_timer = Timer(5, self._close)
|
||||
close_timer.start()
|
||||
self._close_timer = close_timer
|
||||
|
||||
def _open(self):
|
||||
statements = []
|
||||
table_queries = []
|
||||
if not os.path.exists(self._filepath):
|
||||
make_dirs(os.path.dirname(self._filepath))
|
||||
statements.extend((
|
||||
table_queries.extend((
|
||||
self._sql['create_table'],
|
||||
))
|
||||
self._base._table_updated = True
|
||||
|
||||
abort = False
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
db = sqlite3.connect(self._filepath,
|
||||
cached_statements=0,
|
||||
check_same_thread=False,
|
||||
isolation_level=None)
|
||||
cursor = db.cursor()
|
||||
break
|
||||
except (sqlite3.Error, sqlite3.OperationalError) as exc:
|
||||
if attempt < 3 and isinstance(exc, sqlite3.OperationalError):
|
||||
self.log.warning('Retry, attempt %d of 3',
|
||||
attempt,
|
||||
exc_info=True)
|
||||
time.sleep(0.1)
|
||||
except Exception as exc:
|
||||
if isinstance(exc, sqlite3.OperationalError):
|
||||
pass
|
||||
else:
|
||||
self.log.exception('Failed')
|
||||
return None, None
|
||||
abort = True
|
||||
|
||||
if abort or attempt >= 3:
|
||||
abort = True
|
||||
log_level = logging.ERROR
|
||||
status = 'Failed'
|
||||
else:
|
||||
log_level = logging.WARNING
|
||||
status = 'Retry'
|
||||
self.log.log(
|
||||
level=log_level,
|
||||
msg='{status} - Attempt {attempt} of 3',
|
||||
exc_info=True,
|
||||
status=status,
|
||||
attempt=attempt,
|
||||
)
|
||||
if abort:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
return None, None
|
||||
abort = True
|
||||
if abort:
|
||||
return None
|
||||
|
||||
cursor = db.cursor()
|
||||
cursor.arraysize = 100
|
||||
|
||||
sql_script = [
|
||||
queries = [
|
||||
'PRAGMA busy_timeout = 1000;',
|
||||
'PRAGMA read_uncommitted = TRUE;',
|
||||
'PRAGMA secure_delete = FALSE;',
|
||||
@@ -320,76 +391,162 @@ class Storage(object):
|
||||
if not self._table_updated:
|
||||
for result in self._execute(cursor, self._sql['has_old_table']):
|
||||
if result[0] == 1:
|
||||
statements.extend((
|
||||
table_queries.extend((
|
||||
'PRAGMA writable_schema = 1;',
|
||||
self._sql['drop_old_table'],
|
||||
'PRAGMA writable_schema = 0;',
|
||||
))
|
||||
break
|
||||
|
||||
if statements:
|
||||
transaction_begin = len(sql_script) + 1
|
||||
sql_script.extend(('BEGIN;', 'COMMIT;', 'VACUUM;'))
|
||||
sql_script[transaction_begin:transaction_begin] = statements
|
||||
self._execute(cursor, '\n'.join(sql_script), script=True)
|
||||
if table_queries:
|
||||
transaction_begin = len(queries) + 1
|
||||
queries.extend(('BEGIN IMMEDIATE;', 'COMMIT;', 'VACUUM;'))
|
||||
queries[transaction_begin:transaction_begin] = table_queries
|
||||
self._execute(cursor, queries)
|
||||
|
||||
self._base._table_updated = True
|
||||
self._db = db
|
||||
self._cursor = cursor
|
||||
return db, cursor
|
||||
return db
|
||||
|
||||
def _close(self, commit=False, event=None):
|
||||
close_timer = self._close_timer
|
||||
if close_timer:
|
||||
close_timer.cancel()
|
||||
|
||||
if self._lock.accessing() or self._lock.waiting():
|
||||
return False
|
||||
|
||||
def _close(self):
|
||||
cursor = self._cursor
|
||||
if cursor:
|
||||
self._execute(cursor, 'PRAGMA optimize')
|
||||
cursor.close()
|
||||
self._cursor = None
|
||||
db = self._db
|
||||
if db:
|
||||
# Not needed if using db as a context manager
|
||||
# db.commit()
|
||||
if not db:
|
||||
if self._close_actions:
|
||||
db = self._open()
|
||||
else:
|
||||
return None
|
||||
|
||||
if event or self._close_actions:
|
||||
if not event:
|
||||
queries = (
|
||||
'BEGIN IMMEDIATE;',
|
||||
self._set_many(items=None, defer=True, flush=True),
|
||||
'COMMIT;',
|
||||
'BEGIN IMMEDIATE;',
|
||||
self._optimize_item_count(defer=True),
|
||||
self._optimize_file_size(defer=True, db=db),
|
||||
'COMMIT;',
|
||||
'VACUUM;',
|
||||
)
|
||||
elif self._close_actions:
|
||||
queries = (
|
||||
'BEGIN IMMEDIATE;',
|
||||
self._set_many(items=None, defer=True, flush=True),
|
||||
'COMMIT;',
|
||||
'BEGIN IMMEDIATE;',
|
||||
self._sql['prune_invalid'],
|
||||
self._optimize_item_count(defer=True),
|
||||
self._optimize_file_size(defer=True, db=db),
|
||||
'COMMIT;',
|
||||
'VACUUM;',
|
||||
'PRAGMA optimize;',
|
||||
)
|
||||
else:
|
||||
queries = (
|
||||
'BEGIN IMMEDIATE;',
|
||||
self._sql['prune_invalid'],
|
||||
'COMMIT;',
|
||||
'VACUUM;',
|
||||
'PRAGMA optimize;',
|
||||
)
|
||||
self._execute(db.cursor(), queries)
|
||||
|
||||
# Not needed if using db as a context manager
|
||||
if commit:
|
||||
db.commit()
|
||||
|
||||
if event:
|
||||
db.close()
|
||||
self._db = None
|
||||
self._close_actions = False
|
||||
self._close_timer = None
|
||||
return True
|
||||
|
||||
def _execute(self, cursor, query, values=None, many=False, script=False):
|
||||
def _execute(self, cursor, queries, values=(), many=False, script=False):
|
||||
result = []
|
||||
if not cursor:
|
||||
self.log.error_trace('Database not available')
|
||||
return []
|
||||
if values is None:
|
||||
values = ()
|
||||
"""
|
||||
Tests revealed that sqlite has problems to release the database in time
|
||||
This happens no so often, but just to be sure, we try at least 3 times
|
||||
to execute our statement.
|
||||
"""
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
if many:
|
||||
return cursor.executemany(query, values)
|
||||
if script:
|
||||
return cursor.executescript(query)
|
||||
return cursor.execute(query, values)
|
||||
except (sqlite3.Error, sqlite3.OperationalError) as exc:
|
||||
if attempt < 3 and isinstance(exc, sqlite3.OperationalError):
|
||||
self.log.warning('Retry, attempt %d of 3',
|
||||
attempt,
|
||||
exc_info=True)
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
self.log.exception('Failed')
|
||||
return []
|
||||
return []
|
||||
return result
|
||||
|
||||
def _optimize_file_size(self, defer=False):
|
||||
if isinstance(queries, (list, tuple)):
|
||||
if script:
|
||||
queries = ('\n'.join(queries),)
|
||||
else:
|
||||
queries = (queries,)
|
||||
|
||||
for query in queries:
|
||||
if not query:
|
||||
continue
|
||||
if isinstance(query, tuple):
|
||||
query, _values, _many = query
|
||||
else:
|
||||
_many = many
|
||||
_values = values
|
||||
|
||||
# Retry DB operation 3 times in case DB is locked or busy
|
||||
abort = False
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
if _many:
|
||||
result = cursor.executemany(query, _values)
|
||||
elif script:
|
||||
result = cursor.executescript(query)
|
||||
else:
|
||||
result = cursor.execute(query, _values)
|
||||
break
|
||||
except Exception as exc:
|
||||
if isinstance(exc, sqlite3.OperationalError):
|
||||
pass
|
||||
elif isinstance(exc, sqlite3.InterfaceError):
|
||||
cursor = self._db.cursor()
|
||||
else:
|
||||
abort = True
|
||||
|
||||
if abort or attempt >= 3:
|
||||
abort = True
|
||||
log_level = logging.ERROR
|
||||
status = 'Failed'
|
||||
else:
|
||||
log_level = logging.WARNING
|
||||
status = 'Retry'
|
||||
self.log.log(
|
||||
level=log_level,
|
||||
msg=('{status} - Attempt {attempt} of 3',
|
||||
'Query: {query!r}',
|
||||
'Values: {values!r}'),
|
||||
exc_info=True,
|
||||
status=status,
|
||||
attempt=attempt,
|
||||
query=query,
|
||||
values=values,
|
||||
)
|
||||
if abort:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
abort = True
|
||||
if abort:
|
||||
break
|
||||
return result
|
||||
|
||||
def _optimize_file_size(self, defer=False, db=None):
|
||||
# do nothing - optimize only if max size limit has been set
|
||||
if self._max_file_size_kb <= 0:
|
||||
return False
|
||||
|
||||
with self._lock, self as (db, cursor), db:
|
||||
with ExistingDBConnection(db) if db else self as (db, cursor):
|
||||
result = self._execute(cursor, self._sql['get_total_data_size'])
|
||||
|
||||
if result:
|
||||
size_kb = result.fetchone()[0] // 1024
|
||||
result = result.fetchone() if result else None
|
||||
result = result[0] if result else None
|
||||
if result is not None:
|
||||
size_kb = result // 1024
|
||||
else:
|
||||
try:
|
||||
size_kb = (os.path.getsize(self._filepath) // 1024)
|
||||
@@ -403,10 +560,17 @@ class Storage(object):
|
||||
query = self._sql['prune_by_size'].format(prune_size)
|
||||
if defer:
|
||||
return query
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, query)
|
||||
self._execute(cursor, 'VACUUM')
|
||||
return True
|
||||
with self as (db, cursor):
|
||||
self._execute(
|
||||
cursor,
|
||||
(
|
||||
'BEGIN IMMEDIATE;',
|
||||
query,
|
||||
'COMMIT;',
|
||||
'VACUUM;',
|
||||
),
|
||||
)
|
||||
return None
|
||||
|
||||
def _optimize_item_count(self, limit=-1, defer=False):
|
||||
# do nothing - optimize only if max item limit has been set
|
||||
@@ -424,66 +588,204 @@ class Storage(object):
|
||||
)
|
||||
if defer:
|
||||
return query
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, query)
|
||||
self._execute(cursor, 'VACUUM')
|
||||
with self as (db, cursor):
|
||||
self._execute(
|
||||
cursor,
|
||||
(
|
||||
'BEGIN IMMEDIATE;',
|
||||
query,
|
||||
'COMMIT;',
|
||||
'VACUUM;',
|
||||
),
|
||||
)
|
||||
return None
|
||||
|
||||
def _set(self, item_id, item, defer=False, flush=False):
|
||||
memory_store = self._memory_store
|
||||
if memory_store is not None:
|
||||
key = to_str(item_id)
|
||||
if defer:
|
||||
memory_store[key] = (
|
||||
item_id,
|
||||
since_epoch(),
|
||||
item,
|
||||
)
|
||||
self._close_actions = True
|
||||
return None
|
||||
if flush:
|
||||
memory_store.clear()
|
||||
return False
|
||||
if memory_store:
|
||||
memory_store[key] = (
|
||||
item_id,
|
||||
since_epoch(),
|
||||
item,
|
||||
)
|
||||
return self._set_many(items=None)
|
||||
|
||||
with self as (db, cursor), db:
|
||||
self._execute(
|
||||
cursor,
|
||||
self._sql['set'],
|
||||
self._encode(item_id, item),
|
||||
)
|
||||
return True
|
||||
|
||||
def _set(self, item_id, item, timestamp=None):
|
||||
values = self._encode(item_id, item, timestamp)
|
||||
optimize_query = self._optimize_item_count(1, defer=True)
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, 'BEGIN')
|
||||
if optimize_query:
|
||||
self._execute(cursor, optimize_query)
|
||||
self._execute(cursor, self._sql['set'], values=values)
|
||||
def _set_many(self, items, flatten=False, defer=False, flush=False):
|
||||
memory_store = self._memory_store
|
||||
if memory_store is not None:
|
||||
if defer and not flush:
|
||||
now = since_epoch()
|
||||
memory_store.update({
|
||||
to_str(item_id): (
|
||||
item_id,
|
||||
now,
|
||||
item,
|
||||
)
|
||||
for item_id, item in items.items()
|
||||
})
|
||||
self._close_actions = True
|
||||
return None
|
||||
if flush and not defer:
|
||||
memory_store.clear()
|
||||
return False
|
||||
if memory_store:
|
||||
flush = True
|
||||
|
||||
def _set_many(self, items, flatten=False):
|
||||
now = since_epoch()
|
||||
num_items = len(items)
|
||||
values = []
|
||||
|
||||
if flatten:
|
||||
values = [enc_part
|
||||
for item in items.items()
|
||||
for enc_part in self._encode(*item, timestamp=now)]
|
||||
num_item = 0
|
||||
if items:
|
||||
values.extend([
|
||||
part
|
||||
for item_id, item in items.items()
|
||||
for part in self._encode(item_id, item, now)
|
||||
])
|
||||
num_item += len(items)
|
||||
if memory_store:
|
||||
values.extend([
|
||||
part
|
||||
for item_id, timestamp, item in memory_store.values()
|
||||
for part in self._encode(item_id, item, timestamp)
|
||||
])
|
||||
num_item += len(memory_store)
|
||||
query = self._sql['set_flat'].format(
|
||||
'(?,?,?,?),' * (num_items - 1) + '(?,?,?,?)'
|
||||
'(?,?,?,?),' * (num_item - 1) + '(?,?,?,?)'
|
||||
)
|
||||
many = False
|
||||
else:
|
||||
values = [self._encode(*item, timestamp=now)
|
||||
for item in items.items()]
|
||||
if items:
|
||||
values.extend([
|
||||
self._encode(item_id, item, now)
|
||||
for item_id, item in items.items()
|
||||
])
|
||||
if memory_store:
|
||||
values.extend([
|
||||
self._encode(item_id, item, timestamp)
|
||||
for item_id, timestamp, item in memory_store.values()
|
||||
])
|
||||
query = self._sql['set']
|
||||
many = True
|
||||
|
||||
optimize_query = self._optimize_item_count(num_items, defer=True)
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, 'BEGIN')
|
||||
if optimize_query:
|
||||
self._execute(cursor, optimize_query)
|
||||
self._execute(cursor, query, many=(not flatten), values=values)
|
||||
self._execute(cursor, 'COMMIT')
|
||||
self._optimize_file_size()
|
||||
if flush and memory_store:
|
||||
memory_store.clear()
|
||||
|
||||
def _refresh(self, item_id, timestamp=None):
|
||||
values = (timestamp or since_epoch(), to_str(item_id))
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, self._sql['refresh'], values=values)
|
||||
if values:
|
||||
if defer:
|
||||
return query, values, many
|
||||
|
||||
with self as (db, cursor):
|
||||
self._execute(
|
||||
cursor,
|
||||
(
|
||||
'BEGIN IMMEDIATE;',
|
||||
(query, values, many),
|
||||
'COMMIT;',
|
||||
),
|
||||
)
|
||||
self._close_actions = True
|
||||
return None
|
||||
|
||||
def _refresh(self, item_id, timestamp=None, defer=False):
|
||||
key = to_str(item_id)
|
||||
if not timestamp:
|
||||
timestamp = since_epoch()
|
||||
|
||||
memory_store = self._memory_store
|
||||
if memory_store and key in memory_store:
|
||||
if defer:
|
||||
item = memory_store[key]
|
||||
memory_store[key] = (
|
||||
item_id,
|
||||
timestamp,
|
||||
item[2],
|
||||
)
|
||||
self._close_actions = True
|
||||
return None
|
||||
del memory_store[key]
|
||||
|
||||
values = (timestamp, key)
|
||||
with self as (db, cursor):
|
||||
self._execute(
|
||||
cursor,
|
||||
(
|
||||
'BEGIN IMMEDIATE;',
|
||||
(self._sql['refresh'], values, False),
|
||||
'COMMIT;',
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
def _update(self, item_id, item, timestamp=None, defer=False):
|
||||
key = to_str(item_id)
|
||||
if not timestamp:
|
||||
timestamp = since_epoch()
|
||||
|
||||
memory_store = self._memory_store
|
||||
if memory_store and key in memory_store:
|
||||
if defer:
|
||||
memory_store[key] = (
|
||||
item_id,
|
||||
timestamp,
|
||||
item,
|
||||
)
|
||||
self._close_actions = True
|
||||
return None
|
||||
del memory_store[key]
|
||||
|
||||
def _update(self, item_id, item, timestamp=None):
|
||||
values = self._encode(item_id, item, timestamp, for_update=True)
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, self._sql['update'], values=values)
|
||||
with self as (db, cursor):
|
||||
self._execute(
|
||||
cursor,
|
||||
(
|
||||
'BEGIN IMMEDIATE;',
|
||||
(self._sql['update'], values, False),
|
||||
'COMMIT;',
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
def clear(self, defer=False):
|
||||
memory_store = self._memory_store
|
||||
if memory_store:
|
||||
memory_store.clear()
|
||||
|
||||
query = self._sql['clear']
|
||||
if defer:
|
||||
return query
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, query)
|
||||
self._execute(cursor, 'VACUUM')
|
||||
return True
|
||||
|
||||
with self as (db, cursor), db:
|
||||
self._execute(
|
||||
cursor,
|
||||
query,
|
||||
)
|
||||
self._close_actions = True
|
||||
return None
|
||||
|
||||
def is_empty(self):
|
||||
with self as (db, cursor), db:
|
||||
with self as (db, cursor):
|
||||
result = self._execute(cursor, self._sql['is_empty'])
|
||||
for item in result:
|
||||
is_empty = item[0] == 0
|
||||
@@ -494,7 +796,10 @@ class Storage(object):
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
return process(decoded_obj, item)
|
||||
return decoded_obj
|
||||
@@ -520,11 +825,27 @@ class Storage(object):
|
||||
seconds=None,
|
||||
as_dict=False,
|
||||
with_timestamp=False):
|
||||
with self._lock, self as (db, cursor), db:
|
||||
result = self._execute(cursor, self._sql['get'], [to_str(item_id)])
|
||||
item = result.fetchone() if result else None
|
||||
if not item:
|
||||
return None
|
||||
key = to_str(item_id)
|
||||
memory_store = self._memory_store
|
||||
if memory_store and key in memory_store:
|
||||
item = memory_store[key]
|
||||
item = (
|
||||
item_id,
|
||||
item[1], # timestamp from memory store item
|
||||
item[2], # object from memory store item
|
||||
None,
|
||||
)
|
||||
else:
|
||||
with self as (db, cursor):
|
||||
result = self._execute(
|
||||
cursor,
|
||||
self._sql['get'],
|
||||
(key,),
|
||||
)
|
||||
item = result.fetchone() if result else None
|
||||
if not item or not all(item):
|
||||
return None
|
||||
|
||||
cut_off = since_epoch() - seconds if seconds else 0
|
||||
if not cut_off or item[1] >= cut_off:
|
||||
if as_dict:
|
||||
@@ -539,9 +860,12 @@ class Storage(object):
|
||||
return self._decode(item[2], process, item)
|
||||
return None
|
||||
|
||||
def _get_by_ids(self, item_ids=None, oldest_first=True, limit=-1,
|
||||
def _get_by_ids(self, item_ids=(), oldest_first=True, limit=-1,
|
||||
wildcard=False, seconds=None, process=None,
|
||||
as_dict=False, values_only=True, excluding=None):
|
||||
in_memory_result = None
|
||||
result = None
|
||||
|
||||
if not item_ids:
|
||||
if oldest_first:
|
||||
query = self._sql['get_many']
|
||||
@@ -562,50 +886,114 @@ class Storage(object):
|
||||
)
|
||||
item_ids = tuple(item_ids) + tuple(excluding)
|
||||
else:
|
||||
query = self._sql['get_by_key'].format(
|
||||
'?,' * (len(item_ids) - 1) + '?'
|
||||
)
|
||||
item_ids = tuple(item_ids)
|
||||
memory_store = self._memory_store
|
||||
if memory_store:
|
||||
in_memory_result = []
|
||||
_item_ids = []
|
||||
for item_id in item_ids:
|
||||
key = to_str(item_id)
|
||||
if key in memory_store:
|
||||
item = memory_store[key]
|
||||
in_memory_result.append((
|
||||
item_id,
|
||||
item[1], # timestamp from memory store item
|
||||
item[2], # object from memory store item
|
||||
None,
|
||||
))
|
||||
else:
|
||||
_item_ids.append(item_id)
|
||||
item_ids = _item_ids
|
||||
|
||||
epoch = since_epoch()
|
||||
cut_off = epoch - seconds if seconds else 0
|
||||
with self._lock, self as (db, cursor), db:
|
||||
result = self._execute(cursor, query, item_ids)
|
||||
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
|
||||
}
|
||||
if item_ids:
|
||||
query = self._sql['get_by_key'].format(
|
||||
'?,' * (len(item_ids) - 1) + '?'
|
||||
)
|
||||
item_ids = tuple(map(to_str, item_ids))
|
||||
else:
|
||||
result = {
|
||||
item[0]: {
|
||||
'age': epoch - item[1],
|
||||
'value': self._decode(item[2], process, item),
|
||||
}
|
||||
for item in result if not cut_off or item[1] >= cut_off
|
||||
}
|
||||
elif values_only:
|
||||
result = [
|
||||
self._decode(item[2], process, item)
|
||||
query = None
|
||||
|
||||
if query:
|
||||
with self as (db, cursor):
|
||||
result = self._execute(cursor, query, item_ids)
|
||||
if result:
|
||||
result = result.fetchall()
|
||||
|
||||
if in_memory_result:
|
||||
if result:
|
||||
in_memory_result.extend(result)
|
||||
result = in_memory_result
|
||||
|
||||
now = since_epoch()
|
||||
cut_off = now - seconds if seconds else 0
|
||||
|
||||
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
|
||||
]
|
||||
}
|
||||
else:
|
||||
result = [
|
||||
(item[0],
|
||||
fromtimestamp(item[1]),
|
||||
self._decode(item[2], process, item))
|
||||
result = {
|
||||
item[0]: {
|
||||
'age': now - item[1],
|
||||
'value': self._decode(item[2], process, item),
|
||||
}
|
||||
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
|
||||
|
||||
def _remove(self, item_id):
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, self._sql['remove'], [item_id])
|
||||
key = to_str(item_id)
|
||||
memory_store = self._memory_store
|
||||
if memory_store and key in memory_store:
|
||||
del memory_store[key]
|
||||
|
||||
with self as (db, cursor):
|
||||
self._execute(
|
||||
cursor,
|
||||
(
|
||||
'BEGIN IMMEDIATE;',
|
||||
(self._sql['remove'], (key,), False),
|
||||
'COMMIT;',
|
||||
),
|
||||
)
|
||||
self._close_actions = True
|
||||
return True
|
||||
|
||||
def _remove_many(self, item_ids):
|
||||
memory_store = self._memory_store
|
||||
if memory_store:
|
||||
_item_ids = []
|
||||
for item_id in item_ids:
|
||||
key = to_str(item_id)
|
||||
if key in memory_store:
|
||||
del memory_store[key]
|
||||
else:
|
||||
_item_ids.append(item_id)
|
||||
item_ids = _item_ids
|
||||
|
||||
num_ids = len(item_ids)
|
||||
query = self._sql['remove_by_key'].format('?,' * (num_ids - 1) + '?')
|
||||
with self._lock, self as (db, cursor), db:
|
||||
self._execute(cursor, query, tuple(item_ids))
|
||||
self._execute(cursor, 'VACUUM')
|
||||
with self as (db, cursor):
|
||||
self._execute(
|
||||
cursor,
|
||||
(
|
||||
'BEGIN IMMEDIATE;',
|
||||
(query, tuple(map(to_str, item_ids)), False),
|
||||
'COMMIT;',
|
||||
),
|
||||
)
|
||||
self._close_actions = True
|
||||
return True
|
||||
|
||||
@@ -26,7 +26,12 @@ class WatchLaterList(Storage):
|
||||
result = self._get_by_ids(process=from_json, as_dict=True)
|
||||
return result
|
||||
|
||||
def add_item(self, video_id, item):
|
||||
def add_item(self, video_id, item=None):
|
||||
if item is None:
|
||||
item = self._get(video_id)
|
||||
if item:
|
||||
self._update(video_id, item)
|
||||
return
|
||||
self._set(video_id, item)
|
||||
|
||||
def del_item(self, video_id):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user