Merge pull request #1365 from MoojMidge/v7.4

v7.4.0+beta.3
This commit is contained in:
MoojMidge 2026-01-12 08:03:18 +09:00 committed by GitHub
commit 5b14da32cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 343 additions and 151 deletions

View file

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

View file

@ -1,3 +1,22 @@
## v7.4.0+beta.3
### Fixed
- Avoid creating PlayerMonitorThread as a subclass of threading.Thread
- Additional workarounds for JSON file operations failing due to addon service start delays #1362
- Avoid unnecessary window navigation when opening More... context menu dialog
- Fix typo in evaluating whether plugin container is loaded or active
- Fix possible exception when using default fallback for failed search
### Changed
- Update and trigger Setup Wizard to set default value for My Subscription sources
- Pass additional headers to all player request and manifest urls by default
- Workaround Kodi not executing Play action on listitems in non-video containers
- Improve context menu runtime checks when used with widget containers
### New
- Add context menu items to open Settings/Setup Wizard from Setup Wizard/Settings main menu entries
- Log summary of stream proxy request and response details #1363
- Log stream proxy request range #1363
## v7.4.0+beta.2
### Fixed
- Fix retrieving items from local history database #1356

View file

@ -350,7 +350,7 @@ msgid "My Subscriptions"
msgstr ""
msgctxt "#30511"
msgid "Queue video"
msgid ""
msgstr ""
msgctxt "#30512"

View file

@ -16,6 +16,7 @@ from re import (
)
from . import logging
from .compatibility import string_type
from .constants import (
CHECK_SETTINGS,
CONTENT,
@ -416,7 +417,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

@ -77,6 +77,7 @@ FOLDER_URI = 'FolderPath'
HAS_FILES = 'HasFiles'
HAS_FOLDERS = 'HasFolders'
HAS_PARENT = 'HasParent'
NUM_ALL_ITEMS = 'NumAllItems'
SCROLLING = 'Scrolling'
UPDATING = 'IsUpdating'
@ -243,6 +244,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

@ -364,7 +364,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:
@ -398,22 +399,7 @@ class AbstractContext(object):
uri = '?'.join((uri, params))
command = 'command://' if command else ''
if run:
return ''.join((command,
'RunAddon('
if run == 'addon' else
'RunScript('
if run == 'script' else
'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 = {}
@ -444,6 +430,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):
@ -694,8 +709,7 @@ class AbstractContext(object):
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
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

@ -377,7 +377,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,
@ -1027,7 +1027,7 @@ class XbmcContext(AbstractContext):
def is_plugin_folder(self, folder_name=None):
if folder_name is None:
folder_name = XbmcContextUI.get_container_info(FOLDER_NAME,
container_id=False)
container_id=None)
return folder_name == self._plugin_name
def refresh_requested(self, force=False, on=False, off=False, params=None):

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

@ -28,6 +28,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 +44,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,6 +95,12 @@ 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)),
@ -103,7 +125,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 +137,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,
@ -145,17 +170,19 @@ class JSONStore(object):
data,
object_pairs_hook=(self._process_data if process else None),
)
loaded = True
except (RuntimeError, IOError, OSError):
self.log.exception(('Access error', 'File: %s'),
filepath,
filepath or self._filename,
stacklevel=stacklevel)
return False
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

@ -29,7 +29,7 @@ from ..constants import (
from ..utils.redact import redact_params
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 +53,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, args=(self,))
self._thread = thread
thread.daemon = True
thread.start()
def abort_now(self):
return (not self._player.isPlaying()
@ -334,6 +336,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__)

View file

@ -230,7 +230,7 @@ class RequestHandler(BaseHTTPRequestHandler, object):
'log_uri': log_uri,
}
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}',
@ -385,13 +385,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']
@ -416,11 +417,13 @@ class RequestHandler(BaseHTTPRequestHandler, object):
'list': priority_list,
}
elif original_path == '/api/timedtext':
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 = []
@ -432,15 +435,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)
*
next(map(int, byte_range[6:].split('-')))
/
int(clen)
)
except (IndexError, StopIteration, 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
@ -490,11 +529,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:
@ -544,11 +588,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:

View file

@ -441,7 +441,7 @@ class XbmcPlugin(AbstractPlugin):
timeout = kwargs.get('timeout', 30)
interval = kwargs.get('interval', 0.1)
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'

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)
@ -580,10 +581,13 @@ class AbstractSettings(object):
return qualities[0]['nom_height']
return self.fixed_video_quality()
def stream_features(self, value=None):
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',
@ -685,17 +689,24 @@ class AbstractSettings(object):
default=('subscriptions',
'saved_playlists',
'bookmark_channels',
'bookmark_playlists')):
'bookmark_playlists'),
match_values=True,
raw_values=False):
if value is not None:
return self.set_string_list(SETTINGS.MY_SUBSCRIPTIONS_SOURCES,
value)
sources = frozenset(
self.get_string_list(SETTINGS.MY_SUBSCRIPTIONS_SOURCES) or default
)
return tuple([
source in sources
for source in default
])
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:

View file

@ -32,6 +32,7 @@ from ...constants import (
HIDE_PROGRESS,
LISTITEM_INFO,
LISTITEM_PROP,
NUM_ALL_ITEMS,
PLUGIN_CONTAINER_INFO,
PROPERTY,
REFRESH_CONTAINER,
@ -298,6 +299,12 @@ class XbmcContextUI(AbstractContextUI):
stacklevel=stacklevel,
)
and (
self.get_container_info(
NUM_ALL_ITEMS,
_container_id,
stacklevel=stacklevel,
)
or
self.get_container_bool(
HAS_FOLDERS,
_container_id,
@ -323,8 +330,8 @@ class XbmcContextUI(AbstractContextUI):
return {
'is_plugin': is_plugin,
'id': container_id,
'is_loaded': is_active,
'is_active': is_loaded,
'is_active': is_active,
'is_loaded': is_loaded,
}
@classmethod

View file

@ -51,12 +51,15 @@ def redact_params(params, _seq_types=(list, tuple)):
log_params = params.copy()
for param, value in params.items():
if param in {'key',
'api_key',
API_KEY,
'api_secret',
API_SECRET,
'client_secret'}:
if isinstance(value, dict):
log_value = redact_params(value)
elif param in {'key',
'api_key',
API_KEY,
'api_secret',
API_SECRET,
'client_secret',
'secret'}:
log_value = (
['...'.join((val[:3], val[-3:]))
if len(val) > 9 else

View file

@ -2976,21 +2976,19 @@ class YouTubeDataClient(YouTubeLoginClient):
return None, None
with response:
headers = response.headers
if kwargs.get('extended_debug'):
self.log.debug(('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}',
'Content: {response.text}'),
response=response,
headers=headers._store if headers else None,
stacklevel=4)
if self.log.verbose_logging:
log_msg = ('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}',
'Content: {response.text}')
else:
self.log.debug(('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}'),
response=response,
headers=headers._store if headers else None,
stacklevel=4)
log_msg = ('Request response',
'Status: {response.status_code!r}',
'Headers: {headers!r}')
self.log.debug(log_msg,
response=response,
headers=headers._store if headers else None,
stacklevel=4)
if response.status_code == 204 and 'no_content' in kwargs:
return None, True
@ -3166,8 +3164,6 @@ class YouTubeDataClient(YouTubeLoginClient):
)
self.log.warning('Aborted', stacklevel=2)
return {}
if context.get_settings().log_level() & 2:
kwargs.setdefault('extended_debug', True)
if cache is None and 'no_content' in kwargs:
cache = False
elif cache is not False and self._context.refresh_requested():

View file

@ -1128,29 +1128,15 @@ class YouTubePlayerClient(YouTubeDataClient):
if itag in stream_list:
break
url = response['mpd_manifest']
headers = response['client']['headers']
url = self._process_url_params(
response['mpd_manifest'],
mpd_manifest=True,
headers=headers,
)
if not url:
continue
headers = response['client']['headers']
url_components = urlsplit(url)
if url_components.query:
params = dict(parse_qs(url_components.query))
params['mpd_version'] = ['7']
url = url_components._replace(
query=urlencode(params, doseq=True),
).geturl()
else:
path = re_sub(
r'/mpd_version/\d+|/?$',
'/mpd_version/7',
url_components.path,
)
url = url_components._replace(
path=path,
).geturl()
stream_list[itag] = self._get_stream_format(
itag=itag,
title='',
@ -1197,12 +1183,14 @@ class YouTubePlayerClient(YouTubeDataClient):
itags = ('9995', '9996') if is_live else ('9993', '9994')
for client_name, response in responses.items():
url = response['hls_manifest']
headers = response['client']['headers']
url = self._process_url_params(
response['hls_manifest'],
headers=headers,
)
if not url:
continue
headers = response['client']['headers']
result = self.request(
url,
headers=headers,
@ -1318,11 +1306,10 @@ class YouTubePlayerClient(YouTubeDataClient):
else:
new_url = url
new_url = self._process_url_params(new_url,
mpd=False,
headers=headers,
referrer=None,
visitor_data=None)
new_url = self._process_url_params(
new_url,
headers=headers,
)
if not new_url:
continue
@ -1416,11 +1403,12 @@ class YouTubePlayerClient(YouTubeDataClient):
def _process_url_params(self,
url,
mpd=True,
stream_proxy=False,
mpd_manifest=False,
headers=None,
cpn=False,
referrer=False,
visitor_data=False,
referrer=None,
visitor_data=None,
method='POST',
digits_re=re_compile(r'\d+')):
if not url:
@ -1473,7 +1461,7 @@ class YouTubePlayerClient(YouTubeDataClient):
or 'https://www.youtube.com/watch?v=%s' % self.video_id,
)
if mpd:
if stream_proxy:
new_params['__id'] = self.video_id
new_params['__method'] = method
new_params['__host'] = [parts.hostname]
@ -1495,15 +1483,23 @@ class YouTubePlayerClient(YouTubeDataClient):
if cpn is not False:
new_params['cpn'] = cpn or self._generate_cpn()
params.update(new_params)
query_str = urlencode(params, doseq=True)
return parts._replace(
parts = parts._replace(
scheme='http',
netloc=get_connect_address(self._context, as_netloc=True),
path=PATHS.STREAM_PROXY,
query=query_str,
).geturl()
)
elif mpd_manifest:
if 'mpd_version' in params:
new_params['mpd_version'] = ['7']
else:
parts = parts._replace(
path=re_sub(
r'/mpd_version/\d+|/?$',
'/mpd_version/7',
parts.path,
),
)
elif 'ratebypass' not in params and 'range' not in params:
content_length = params.get('clen', [''])[0]
@ -1512,7 +1508,7 @@ class YouTubePlayerClient(YouTubeDataClient):
if new_params:
params.update(new_params)
query_str = urlencode(params, doseq=True)
return parts._replace(query=query_str).geturl()
parts = parts._replace(query=query_str)
return parts.geturl()
@ -2406,6 +2402,7 @@ class YouTubePlayerClient(YouTubeDataClient):
urls = self._process_url_params(
unquote(url),
stream_proxy=True,
headers=client['headers'],
cpn=client.get('_cpn'),
)
@ -2851,9 +2848,8 @@ class YouTubePlayerClient(YouTubeDataClient):
url = entity_escape(unquote(self._process_url_params(
subtitle['url'],
stream_proxy=True,
headers=headers,
referrer=None,
visitor_data=None,
)))
if not url:
continue

View file

@ -22,6 +22,7 @@ from ...kodion.constants import (
BUSY_FLAG,
CHANNEL_ID,
CONTENT,
CONTEXT_MENU,
FORCE_PLAY_PARAMS,
INCOGNITO,
ORDER,
@ -535,8 +536,11 @@ def process(provider, context, **_kwargs):
if context.get_handle() == -1:
# This is required to trigger Kodi resume prompt, along with using
# RunPlugin. Prompt will not be used if using PlayMedia
if force_play_params and not params.get(PLAY_STRM):
# RunPlugin. Prompt will not be used if using PlayMedia, however
# Action(Play) does not work in non-video windows
if ((force_play_params or params.get(CONTEXT_MENU))
and not params.get(PLAY_STRM)
and context.is_plugin_folder()):
return UriItem('command://Action(Play)')
return UriItem('command://{0}'.format(

View file

@ -23,6 +23,12 @@ from ...kodion.utils.datetime import since_epoch, strptime
def process_pre_run(context):
context.get_function_cache().clear()
settings = context.get_settings()
if not settings.subscriptions_sources(default=False, raw_values=True):
settings.subscriptions_sources(
settings.subscriptions_sources(raw_values=True)
)
def process_language(context, step, steps, **_kwargs):
localize = context.localize

View file

@ -104,7 +104,7 @@ def _process_rate_video(provider,
)
def _process_more_for_video(context):
def _process_more_for_video(provider, context):
params = context.get_params()
video_id = params.get(VIDEO_ID)
@ -126,8 +126,22 @@ def _process_more_for_video(context):
]
result = context.get_ui().on_select(context.localize('video.more'), items)
if result != -1:
context.execute(result)
if result == -1:
return (
False,
{
provider.FALLBACK: False,
provider.FORCE_RETURN: True,
},
)
return (
True,
{
provider.FALLBACK: result,
provider.FORCE_RETURN: True,
provider.POST_RUN: True,
},
)
def process(provider, context, re_match=None, command=None, **kwargs):
@ -138,6 +152,6 @@ def process(provider, context, re_match=None, command=None, **kwargs):
return _process_rate_video(provider, context, re_match, **kwargs)
if command == 'more':
return _process_more_for_video(context)
return _process_more_for_video(provider, context)
raise KodionException('Unknown video command: %s' % command)

View file

@ -1771,19 +1771,27 @@ class Provider(AbstractProvider):
if settings_bool(settings.SHOW_SETUP_WIZARD, True):
settings_menu_item = DirectoryItem(
localize('setup_wizard'),
create_uri(('config', 'setup_wizard')),
create_uri(PATHS.SETUP_WIZARD),
image='{media}/settings.png',
action=True,
)
context_menu = [
menu_items.open_settings(context)
]
settings_menu_item.add_context_menu(context_menu)
result.append(settings_menu_item)
if settings_bool(settings.SHOW_SETTINGS):
settings_menu_item = DirectoryItem(
localize('settings'),
create_uri(('config', 'youtube')),
create_uri(PATHS.SETTINGS),
image='{media}/settings.png',
action=True,
)
context_menu = [
menu_items.open_setup_wizard(context)
]
settings_menu_item.add_context_menu(context_menu)
result.append(settings_menu_item)
return result, options