Updated kodi settings on Lenovo

This commit is contained in:
2026-03-22 22:28:43 +01:00
parent 725dfa7157
commit 32b5a81da6
10925 changed files with 575678 additions and 5511 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,6 +71,7 @@ class NextPageItem(DirectoryItem):
image=image,
fanart=fanart,
category_label='__inherit__',
special_sort='bottom',
)
self.next_page = page

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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