mirror of
https://github.com/anxdpanic/plugin.video.youtube.git
synced 2026-01-23 04:52:07 -08:00
commit
5b14da32cc
22 changed files with 343 additions and 151 deletions
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ msgid "My Subscriptions"
|
|||
msgstr ""
|
||||
|
||||
msgctxt "#30511"
|
||||
msgid "Queue video"
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30512"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue