mirror of
https://github.com/anxdpanic/plugin.video.youtube.git
synced 2025-12-05 18:20:41 -08:00
commit
e86297d3b2
28 changed files with 415 additions and 263 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.youtube" name="YouTube" version="7.3.0+beta.6" provider-name="anxdpanic, bromix, MoojMidge">
|
||||
<addon id="plugin.video.youtube" name="YouTube" version="7.3.0+beta.7" provider-name="anxdpanic, bromix, MoojMidge">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.requests" version="2.27.1"/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
## v7.3.0+beta.7
|
||||
### Fixed
|
||||
- Only add playable items to playlist when adding related items
|
||||
- Fix using invalid default end limit with Playlist.GetItems JSONRPC method
|
||||
- Fix conversion of SRT subtitles to WebVTT #1256
|
||||
- Workaround playback failure of progressive streams
|
||||
- Fix re-sorting live search lists
|
||||
- Disable use of custom thumbnail urls #1245
|
||||
- Workaround addon service not starting prior to plugin invocation #1298
|
||||
- Fix unofficial version using localised sort order #1309
|
||||
- Fix parsing of logged_in query parameter
|
||||
|
||||
### Changed
|
||||
- Ignore player request failures that may incorrectly indicate a need to sign-in #1312
|
||||
- Include playlist_id listitem property for items from virtual playlists
|
||||
|
||||
### New
|
||||
- Add refresh to context menu of playlists
|
||||
- Allow watch urls from music.youtube.com to be directly handled by the addon
|
||||
- Allow urls from www.youtubekids.com to be directly handled by the addon
|
||||
|
||||
## v7.3.0+beta.6
|
||||
### Fixed
|
||||
Fix typo in YouTubePlayerClient error hook
|
||||
- Fix typo in YouTubePlayerClient error hook
|
||||
|
||||
## v7.3.0+beta.5
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -53,9 +53,18 @@ VALUE_TO_STR = {
|
|||
1: 'true',
|
||||
}
|
||||
|
||||
YOUTUBE_HOSTNAMES = frozenset((
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
'm.youtube.com',
|
||||
'www.youtubekids.com',
|
||||
'music.youtube.com',
|
||||
))
|
||||
|
||||
# Flags
|
||||
ABORT_FLAG = 'abort_requested'
|
||||
BUSY_FLAG = 'busy'
|
||||
SERVICE_RUNNING_FLAG = 'service_monitor_running'
|
||||
WAIT_END_FLAG = 'builtin_completed'
|
||||
TRAKT_PAUSE_FLAG = 'script.trakt.paused'
|
||||
|
||||
|
|
@ -216,10 +225,12 @@ __all__ = (
|
|||
# Const values
|
||||
'BOOL_FROM_STR',
|
||||
'VALUE_TO_STR',
|
||||
'YOUTUBE_HOSTNAMES',
|
||||
|
||||
# Flags
|
||||
'ABORT_FLAG',
|
||||
'BUSY_FLAG',
|
||||
'SERVICE_RUNNING_FLAG',
|
||||
'TRAKT_PAUSE_FLAG',
|
||||
'WAIT_END_FLAG',
|
||||
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ class AbstractContext(object):
|
|||
'visitor',
|
||||
))
|
||||
_STRING_BOOL_PARAMS = frozenset((
|
||||
'logged_in',
|
||||
'reload_path',
|
||||
))
|
||||
_STRING_INT_PARAMS = frozenset((
|
||||
|
|
@ -209,7 +210,7 @@ class AbstractContext(object):
|
|||
self.parse_params(params)
|
||||
|
||||
self._uri = None
|
||||
self._path = path
|
||||
self._path = None
|
||||
self._path_parts = []
|
||||
self.set_path(path, force=True)
|
||||
|
||||
|
|
@ -536,7 +537,12 @@ class AbstractContext(object):
|
|||
value = unquote(value)
|
||||
try:
|
||||
if param in self._BOOL_PARAMS:
|
||||
parsed_value = BOOL_FROM_STR.get(str(value), False)
|
||||
parsed_value = BOOL_FROM_STR.get(
|
||||
str(value),
|
||||
bool(value)
|
||||
if param in self._STRING_BOOL_PARAMS else
|
||||
False
|
||||
)
|
||||
elif param in self._INT_PARAMS:
|
||||
parsed_value = int(
|
||||
(BOOL_FROM_STR.get(str(value), value) or 0)
|
||||
|
|
@ -678,7 +684,7 @@ class AbstractContext(object):
|
|||
def tear_down(self):
|
||||
pass
|
||||
|
||||
def ipc_exec(self, target, timeout=None, payload=None):
|
||||
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ from ...compatibility import (
|
|||
from ...constants import (
|
||||
ABORT_FLAG,
|
||||
ADDON_ID,
|
||||
BUSY_FLAG,
|
||||
CHANNEL_ID,
|
||||
CONTENT,
|
||||
FOLDER_NAME,
|
||||
PLAYLIST_ID,
|
||||
PLAY_FORCE_AUDIO,
|
||||
SERVICE_IPC,
|
||||
SERVICE_RUNNING_FLAG,
|
||||
SORT,
|
||||
URI,
|
||||
VIDEO_ID,
|
||||
|
|
@ -962,6 +964,8 @@ class XbmcContext(AbstractContext):
|
|||
attrs = (
|
||||
'_ui',
|
||||
'_playlist',
|
||||
'_api_store',
|
||||
'_access_manager',
|
||||
)
|
||||
for attr in attrs:
|
||||
try:
|
||||
|
|
@ -970,7 +974,15 @@ class XbmcContext(AbstractContext):
|
|||
except AttributeError:
|
||||
pass
|
||||
|
||||
def ipc_exec(self, target, timeout=None, payload=None):
|
||||
def ipc_exec(self, target, timeout=None, payload=None, raise_exc=False):
|
||||
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)
|
||||
return None
|
||||
|
||||
data = {'target': target, 'response_required': bool(timeout)}
|
||||
if payload:
|
||||
data.update(payload)
|
||||
|
|
|
|||
|
|
@ -139,14 +139,26 @@ def media_play_using(context, video_id=VIDEO_ID_INFOLABEL):
|
|||
)
|
||||
|
||||
|
||||
def refresh_listing(context):
|
||||
def refresh_listing(context, path=None, params=None):
|
||||
if path is None:
|
||||
path = (PATHS.ROUTE, context.get_path(),)
|
||||
elif isinstance(path, tuple):
|
||||
path = (PATHS.ROUTE,) + path
|
||||
else:
|
||||
path = (PATHS.ROUTE, path,)
|
||||
if params is None:
|
||||
params = context.get_params()
|
||||
return (
|
||||
context.localize('refresh'),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, context.get_path(),),
|
||||
dict(context.get_params(),
|
||||
refresh=context.refresh_requested(force=True, on=True)),
|
||||
path,
|
||||
dict(params,
|
||||
refresh=context.refresh_requested(
|
||||
force=True,
|
||||
on=True,
|
||||
params=params,
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -829,7 +841,7 @@ def search_sort_by(context, params, order):
|
|||
),
|
||||
context_menu_uri(
|
||||
context,
|
||||
(PATHS.ROUTE, PATHS.SEARCH, 'query',),
|
||||
(PATHS.ROUTE, context.get_path(),),
|
||||
params=dict(params,
|
||||
order=order,
|
||||
page=1,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,12 @@ class AccessManager(JSONStore):
|
|||
}
|
||||
|
||||
def __init__(self, context):
|
||||
self._user = None
|
||||
self._last_origin = None
|
||||
super(AccessManager, self).__init__('access_manager.json', context)
|
||||
|
||||
def init(self):
|
||||
super(AccessManager, self).init()
|
||||
access_manager_data = self._data['access_manager']
|
||||
self._user = access_manager_data.get('current_user', 0)
|
||||
self._last_origin = access_manager_data.get('last_origin', ADDON_ID)
|
||||
|
|
@ -204,7 +209,8 @@ class AccessManager(JSONStore):
|
|||
Returns users
|
||||
:return: users
|
||||
"""
|
||||
return self._data['access_manager'].get('users', {})
|
||||
data = self._data if self._loaded else self.get_data()
|
||||
return data['access_manager'].get('users', {})
|
||||
|
||||
def add_user(self, username='', user=None):
|
||||
"""
|
||||
|
|
@ -546,7 +552,8 @@ class AccessManager(JSONStore):
|
|||
Returns developers
|
||||
:return: dict, developers
|
||||
"""
|
||||
return self._data['access_manager'].get('developers', {})
|
||||
data = self._data if self._loaded else self.get_data()
|
||||
return data['access_manager'].get('developers', {})
|
||||
|
||||
def add_new_developer(self, addon_id):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -38,9 +38,17 @@ class JSONStore(object):
|
|||
self.filepath = None
|
||||
|
||||
self._context = context
|
||||
self._loaded = False
|
||||
self._data = {}
|
||||
self.load(stacklevel=3)
|
||||
self.set_defaults()
|
||||
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
|
||||
|
||||
def set_defaults(self, reset=False):
|
||||
raise NotImplementedError
|
||||
|
|
@ -81,6 +89,7 @@ class JSONStore(object):
|
|||
FILE_WRITE,
|
||||
timeout=5,
|
||||
payload={'filepath': filepath},
|
||||
raise_exc=True,
|
||||
)
|
||||
if response is False:
|
||||
raise IOError
|
||||
|
|
@ -92,7 +101,7 @@ class JSONStore(object):
|
|||
else:
|
||||
with open(filepath, mode='w', encoding='utf-8') as file:
|
||||
file.write(to_unicode(_data))
|
||||
except (IOError, OSError):
|
||||
except (RuntimeError, IOError, OSError):
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
|
|
@ -119,6 +128,7 @@ class JSONStore(object):
|
|||
FILE_READ,
|
||||
timeout=5,
|
||||
payload={'filepath': filepath},
|
||||
raise_exc=True,
|
||||
) is not False:
|
||||
data = self._context.get_ui().get_property(
|
||||
'-'.join((FILE_READ, filepath)),
|
||||
|
|
@ -135,17 +145,23 @@ class JSONStore(object):
|
|||
data,
|
||||
object_pairs_hook=(self._process_data if process else None),
|
||||
)
|
||||
except (IOError, OSError):
|
||||
except (RuntimeError, IOError, OSError):
|
||||
self.log.exception(('Access error', 'File: %s'),
|
||||
filepath,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
self.log.exception(('Invalid data', 'Data: {data!r}'),
|
||||
data=data,
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_data(self, process=True, fallback=True, stacklevel=2):
|
||||
if not self._loaded:
|
||||
self.init()
|
||||
data = self._data
|
||||
|
||||
try:
|
||||
if not data:
|
||||
raise ValueError
|
||||
|
|
@ -160,7 +176,9 @@ class JSONStore(object):
|
|||
if fallback:
|
||||
self.set_defaults(reset=True)
|
||||
return self.get_data(process=process, fallback=False)
|
||||
raise exc
|
||||
if self._loaded:
|
||||
raise exc
|
||||
return data
|
||||
|
||||
def load_data(self, data, process=True, stacklevel=2):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ class KodiLogger(logging.Logger):
|
|||
**kwargs
|
||||
)
|
||||
|
||||
def waring_trace(self, msg, *args, **kwargs):
|
||||
def warning_trace(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(WARNING):
|
||||
self._log(
|
||||
WARNING,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ from ..compatibility import urlsplit, xbmc, xbmcgui
|
|||
from ..constants import (
|
||||
ACTION,
|
||||
ADDON_ID,
|
||||
BOOL_FROM_STR,
|
||||
BUSY_FLAG,
|
||||
CHECK_SETTINGS,
|
||||
CONTAINER_FOCUS,
|
||||
CONTAINER_ID,
|
||||
|
|
@ -112,81 +110,11 @@ class ServiceMonitor(xbmc.Monitor):
|
|||
xbmcgui.Window(10000).setProperty(_property_id, value)
|
||||
return value
|
||||
|
||||
def get_property(self,
|
||||
property_id,
|
||||
stacklevel=2,
|
||||
process=None,
|
||||
log_value=None,
|
||||
log_process=None,
|
||||
raw=False,
|
||||
as_bool=False,
|
||||
default=False):
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
value = xbmcgui.Window(10000).getProperty(_property_id)
|
||||
if log_value is None:
|
||||
log_value = value
|
||||
if log_process:
|
||||
log_value = log_process(log_value)
|
||||
self.log.debug_trace('Get property {property_id!r}: {value!r}',
|
||||
property_id=property_id,
|
||||
value=log_value,
|
||||
stacklevel=stacklevel)
|
||||
if process:
|
||||
value = process(value)
|
||||
return BOOL_FROM_STR.get(value, default) if as_bool else value
|
||||
|
||||
def pop_property(self,
|
||||
property_id,
|
||||
stacklevel=2,
|
||||
process=None,
|
||||
log_value=None,
|
||||
log_process=None,
|
||||
raw=False,
|
||||
as_bool=False,
|
||||
default=False):
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
window = xbmcgui.Window(10000)
|
||||
value = window.getProperty(_property_id)
|
||||
if value:
|
||||
window.clearProperty(_property_id)
|
||||
if process:
|
||||
value = process(value)
|
||||
if log_value is None:
|
||||
log_value = value
|
||||
if log_value and log_process:
|
||||
log_value = log_process(log_value)
|
||||
self.log.debug_trace('Pop property {property_id!r}: {value!r}',
|
||||
property_id=property_id,
|
||||
value=log_value,
|
||||
stacklevel=stacklevel)
|
||||
return BOOL_FROM_STR.get(value, default) if as_bool else value
|
||||
|
||||
def clear_property(self, property_id, stacklevel=2, raw=False):
|
||||
self.log.debug_trace('Clear property {property_id!r}',
|
||||
property_id=property_id,
|
||||
stacklevel=stacklevel)
|
||||
_property_id = property_id if raw else '-'.join((ADDON_ID, property_id))
|
||||
xbmcgui.Window(10000).clearProperty(_property_id)
|
||||
return None
|
||||
|
||||
def refresh_container(self, deferred=False):
|
||||
if deferred:
|
||||
def refresh_container(self, force=False):
|
||||
if force:
|
||||
self.refresh = False
|
||||
if self.get_property(REFRESH_CONTAINER) == BUSY_FLAG:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
return
|
||||
|
||||
container = self._context.get_ui().get_container()
|
||||
if not container['is_plugin'] or not container['is_loaded']:
|
||||
self.log.debug('No plugin container loaded - cancelling refresh')
|
||||
return
|
||||
if container['is_active']:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
else:
|
||||
self.set_property(REFRESH_CONTAINER, BUSY_FLAG)
|
||||
self.log.debug('Plugin container not active - deferring refresh')
|
||||
refreshed = self._context.get_ui().refresh_container(force=force)
|
||||
if refreshed is None:
|
||||
self.refresh = True
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
|
|
@ -317,7 +245,7 @@ class ServiceMonitor(xbmc.Monitor):
|
|||
response = False
|
||||
else:
|
||||
with write_access:
|
||||
content = self.pop_property(
|
||||
content = self._context.get_ui().pop_property(
|
||||
'-'.join((FILE_WRITE, filepath)),
|
||||
log_value='<redacted>',
|
||||
)
|
||||
|
|
@ -347,52 +275,11 @@ class ServiceMonitor(xbmc.Monitor):
|
|||
elif event == CONTAINER_FOCUS:
|
||||
if data:
|
||||
data = json.loads(data)
|
||||
if not data:
|
||||
return
|
||||
|
||||
context = self._context
|
||||
ui = context.get_ui()
|
||||
|
||||
container = ui.get_container()
|
||||
if not all(container.values()):
|
||||
return
|
||||
|
||||
container_id = data.get(CONTAINER_ID)
|
||||
if container_id is None:
|
||||
container_id = container['id']
|
||||
elif not container_id:
|
||||
return
|
||||
if not isinstance(container_id, int):
|
||||
try:
|
||||
container_id = int(container_id)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
position = data.get(CONTAINER_POSITION)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
if ui.get_container_bool(HAS_PARENT, container_id):
|
||||
offset = 0
|
||||
else:
|
||||
offset = -1
|
||||
|
||||
if not isinstance(position, int):
|
||||
if position == 'next':
|
||||
position = ui.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset += 1
|
||||
elif position == 'previous':
|
||||
position = ui.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset -= 1
|
||||
try:
|
||||
position = int(position)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
context.execute('SetFocus({0},{1},absolute)'.format(
|
||||
container_id,
|
||||
position + offset,
|
||||
))
|
||||
if data:
|
||||
self._context.get_ui().focus_container(
|
||||
container_id=data.get(CONTAINER_ID),
|
||||
position=data.get(CONTAINER_POSITION),
|
||||
)
|
||||
|
||||
elif event == RELOAD_ACCESS_MANAGER:
|
||||
self._context.reload_access_manager()
|
||||
|
|
|
|||
|
|
@ -174,14 +174,14 @@ class XbmcPlaylistPlayer(AbstractPlaylistPlayer):
|
|||
def get_items(self, properties=None, start=0, end=-1, dumps=False):
|
||||
if properties is None:
|
||||
properties = ('title', 'file')
|
||||
limits = {'start': start}
|
||||
if end != -1:
|
||||
limits['end'] = end
|
||||
response = jsonrpc(method='Playlist.GetItems',
|
||||
params={
|
||||
'properties': properties,
|
||||
'playlistid': self._playlist.getPlayListId(),
|
||||
'limits': {
|
||||
'start': start,
|
||||
'end': end,
|
||||
},
|
||||
'limits': limits,
|
||||
})
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -372,7 +372,12 @@ class XbmcPlugin(AbstractPlugin):
|
|||
listitem=item,
|
||||
)
|
||||
elif options.get(provider.FORCE_REFRESH):
|
||||
_post_run_action = ui.refresh_container
|
||||
_post_run_action = (
|
||||
context.send_notification,
|
||||
{
|
||||
'method': REFRESH_CONTAINER,
|
||||
},
|
||||
)
|
||||
else:
|
||||
if context.is_plugin_path(
|
||||
ui.get_container_info(FOLDER_URI, container_id=None)
|
||||
|
|
@ -409,10 +414,13 @@ class XbmcPlugin(AbstractPlugin):
|
|||
if any(sync_items):
|
||||
context.send_notification(SYNC_LISTITEM, sync_items)
|
||||
|
||||
if forced and is_same_path and (not played_video_id or route):
|
||||
container = ui.get_property(CONTAINER_ID)
|
||||
position = ui.get_property(CONTAINER_POSITION)
|
||||
if container and position:
|
||||
container = ui.get_property(CONTAINER_ID)
|
||||
position = ui.get_property(CONTAINER_POSITION)
|
||||
|
||||
if is_same_path:
|
||||
if (container and position
|
||||
and (forced or position == 'current')
|
||||
and (not played_video_id or route)):
|
||||
post_run_actions.append((
|
||||
context.send_notification,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -67,11 +67,8 @@ def run(context=_context,
|
|||
log.verbose_logging = False
|
||||
profiler.disable()
|
||||
|
||||
old_path, old_params = context.parse_uri(
|
||||
ui.get_container_info(FOLDER_URI, container_id=None),
|
||||
parse_params=False,
|
||||
)
|
||||
old_path = old_path.rstrip('/')
|
||||
old_path = context.get_path().rstrip('/')
|
||||
old_uri = ui.get_container_info(FOLDER_URI, container_id=None)
|
||||
context.init()
|
||||
current_path = context.get_path().rstrip('/')
|
||||
current_params = context.get_original_params()
|
||||
|
|
@ -79,13 +76,28 @@ def run(context=_context,
|
|||
|
||||
new_params = {}
|
||||
new_kwargs = {}
|
||||
params = context.get_params()
|
||||
|
||||
params = context.get_params()
|
||||
refresh = context.refresh_requested(params=params)
|
||||
is_same_path = refresh != 0 and current_path == old_path
|
||||
forced = (current_handle != -1
|
||||
and (old_path == PATHS.PLAY
|
||||
or (is_same_path and current_params == old_params)))
|
||||
was_playing = old_path == PATHS.PLAY
|
||||
is_same_path = current_path == old_path
|
||||
|
||||
if was_playing or is_same_path or refresh:
|
||||
old_path, old_params = context.parse_uri(
|
||||
old_uri,
|
||||
parse_params=False,
|
||||
)
|
||||
old_path = old_path.rstrip('/')
|
||||
is_same_path = current_path == old_path
|
||||
if was_playing and current_handle != -1:
|
||||
forced = True
|
||||
elif is_same_path and current_params == old_params:
|
||||
forced = True
|
||||
else:
|
||||
forced = False
|
||||
else:
|
||||
forced = False
|
||||
|
||||
if forced:
|
||||
refresh = context.refresh_requested(force=True, off=True, params=params)
|
||||
new_params['refresh'] = refresh if refresh else 0
|
||||
|
|
|
|||
|
|
@ -145,7 +145,10 @@ def _config_actions(context, action, *_args):
|
|||
base_kodi_language = kodi_language.partition('-')[0]
|
||||
|
||||
json_data = client.get_supported_languages(kodi_language)
|
||||
items = json_data.get('items') or DEFAULT_LANGUAGES['items']
|
||||
if json_data:
|
||||
items = json_data.get('items') or DEFAULT_LANGUAGES['items']
|
||||
else:
|
||||
items = DEFAULT_LANGUAGES['items']
|
||||
|
||||
selected_language = [None]
|
||||
|
||||
|
|
@ -184,7 +187,10 @@ def _config_actions(context, action, *_args):
|
|||
return
|
||||
|
||||
json_data = client.get_supported_regions(language=language_id)
|
||||
items = json_data.get('items') or DEFAULT_REGIONS['items']
|
||||
if json_data:
|
||||
items = json_data.get('items') or DEFAULT_REGIONS['items']
|
||||
else:
|
||||
items = DEFAULT_REGIONS['items']
|
||||
|
||||
selected_region = [None]
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from .constants import (
|
|||
ABORT_FLAG,
|
||||
ARTIST,
|
||||
BOOKMARK_ID,
|
||||
BUSY_FLAG,
|
||||
CHANNEL_ID,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_POSITION,
|
||||
|
|
@ -24,6 +25,7 @@ from .constants import (
|
|||
PLAYLIST_ITEM_ID,
|
||||
PLAY_COUNT,
|
||||
PLUGIN_SLEEPING,
|
||||
SERVICE_RUNNING_FLAG,
|
||||
SUBSCRIPTION_ID,
|
||||
TEMP_PATH,
|
||||
TITLE,
|
||||
|
|
@ -67,6 +69,9 @@ def run():
|
|||
localize = context.localize
|
||||
|
||||
clear_property(ABORT_FLAG)
|
||||
if ui.get_property(SERVICE_RUNNING_FLAG) == BUSY_FLAG:
|
||||
monitor.refresh_container()
|
||||
set_property(SERVICE_RUNNING_FLAG)
|
||||
|
||||
# wipe add-on temp folder on updates/restarts (subtitles, and mpd files)
|
||||
rm_dir(TEMP_PATH)
|
||||
|
|
@ -173,7 +178,7 @@ def run():
|
|||
monitor.interrupt = True
|
||||
|
||||
if monitor.refresh and all(container.values()):
|
||||
monitor.refresh_container(deferred=True)
|
||||
monitor.refresh_container(force=True)
|
||||
break
|
||||
|
||||
if (monitor.interrupt
|
||||
|
|
@ -240,6 +245,7 @@ def run():
|
|||
break
|
||||
|
||||
set_property(ABORT_FLAG)
|
||||
clear_property(SERVICE_RUNNING_FLAG)
|
||||
|
||||
# clean up any/all playback monitoring threads
|
||||
player.cleanup_threads(only_ended=False)
|
||||
|
|
|
|||
|
|
@ -59,8 +59,7 @@ class AbstractContextUI(object):
|
|||
def on_busy():
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def refresh_container():
|
||||
def refresh_container(self, force=False, stacklevel=None):
|
||||
"""
|
||||
Needs to be implemented by a mock for testing or the real deal.
|
||||
This will refresh the current container or list.
|
||||
|
|
@ -68,6 +67,9 @@ class AbstractContextUI(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def focus_container(self, container_id=None, position=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def get_infobool(name):
|
||||
raise NotImplementedError()
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ from ...compatibility import string_type, xbmc, xbmcgui
|
|||
from ...constants import (
|
||||
ADDON_ID,
|
||||
BOOL_FROM_STR,
|
||||
BUSY_FLAG,
|
||||
CONTAINER_FOCUS,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_LISTITEM_INFO,
|
||||
CONTAINER_LISTITEM_PROP,
|
||||
CONTAINER_POSITION,
|
||||
CURRENT_CONTAINER_INFO,
|
||||
CURRENT_ITEM,
|
||||
HAS_FILES,
|
||||
HAS_FOLDERS,
|
||||
HAS_PARENT,
|
||||
|
|
@ -192,8 +194,76 @@ class XbmcContextUI(AbstractContextUI):
|
|||
def on_busy():
|
||||
return XbmcBusyDialog()
|
||||
|
||||
def refresh_container(self):
|
||||
self._context.send_notification(REFRESH_CONTAINER)
|
||||
def refresh_container(self, force=False, stacklevel=None):
|
||||
if force:
|
||||
if self.get_property(REFRESH_CONTAINER) == BUSY_FLAG:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
return True
|
||||
|
||||
stacklevel = 2 if stacklevel is None else stacklevel + 1
|
||||
|
||||
container = self.get_container()
|
||||
if not container['is_plugin'] or not container['is_loaded']:
|
||||
self.log.debug('No plugin container loaded - cancelling refresh',
|
||||
stacklevel=stacklevel)
|
||||
return False
|
||||
|
||||
if container['is_active']:
|
||||
self.set_property(REFRESH_CONTAINER)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
return True
|
||||
|
||||
self.set_property(REFRESH_CONTAINER, BUSY_FLAG)
|
||||
self.log.debug('Plugin container not active - deferring refresh',
|
||||
stacklevel=stacklevel)
|
||||
return None
|
||||
|
||||
def focus_container(self, container_id=None, position=None):
|
||||
if position is None:
|
||||
return
|
||||
|
||||
container = self.get_container()
|
||||
if not all(container.values()):
|
||||
return
|
||||
|
||||
if container_id is None:
|
||||
container_id = container['id']
|
||||
elif not container_id:
|
||||
return
|
||||
|
||||
if not isinstance(container_id, int):
|
||||
try:
|
||||
container_id = int(container_id)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
if self.get_container_bool(HAS_PARENT, container_id):
|
||||
offset = 0
|
||||
else:
|
||||
offset = -1
|
||||
|
||||
if not isinstance(position, int):
|
||||
if position == 'next':
|
||||
position = self.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset += 1
|
||||
elif position == 'previous':
|
||||
position = self.get_container_info(CURRENT_ITEM, container_id)
|
||||
offset -= 1
|
||||
elif position == 'current':
|
||||
position = (
|
||||
self.get_property(CONTAINER_POSITION)
|
||||
or self.get_container_info(CURRENT_ITEM, container_id)
|
||||
)
|
||||
try:
|
||||
position = int(position)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
xbmc.executebuiltin('SetFocus({0},{1},absolute)'.format(
|
||||
container_id,
|
||||
position + offset,
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def get_infobool(name, _bool=xbmc.getCondVisibility):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
|
||||
from datetime import timedelta
|
||||
from math import floor, log
|
||||
from re import MULTILINE, compile as re_compile
|
||||
from re import DOTALL, compile as re_compile
|
||||
|
||||
from ..compatibility import byte_string_type
|
||||
|
||||
|
|
@ -103,10 +103,10 @@ def timedelta_to_timestamp(delta, offset=None, multiplier=1.0):
|
|||
def _srt_to_vtt(content,
|
||||
srt_re=re_compile(
|
||||
br'\d+[\r\n]'
|
||||
br'(?P<start>\d+:\d+:\d+,\d+) --> '
|
||||
br'(?P<end>\d+:\d+:\d+,\d+)[\r\n]'
|
||||
br'(?P<text>.+)(?=[\r\n]{2,})',
|
||||
flags=MULTILINE,
|
||||
br'(?P<start>[\d:,]+) --> '
|
||||
br'(?P<end>[\d:,]+)[\r\n]'
|
||||
br'(?P<text>.+?)[\r\n][\r\n]',
|
||||
flags=DOTALL,
|
||||
)):
|
||||
subtitle_iter = srt_re.finditer(content)
|
||||
try:
|
||||
|
|
@ -136,6 +136,7 @@ def _srt_to_vtt(content,
|
|||
except StopIteration:
|
||||
if subtitle == next_subtitle:
|
||||
break
|
||||
subtitle = None
|
||||
next_subtitle = None
|
||||
|
||||
if next_subtitle and end > next_start:
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
'browseEndpoint',
|
||||
'browseId',
|
||||
),
|
||||
'playlist_id': (
|
||||
'tileRenderer',
|
||||
'onSelectCommand',
|
||||
'watchEndpoint',
|
||||
'playlistId',
|
||||
),
|
||||
'continuation': (
|
||||
'contents',
|
||||
'tvBrowseRenderer',
|
||||
|
|
@ -1072,12 +1078,16 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
if playlist_id_upper == 'HL':
|
||||
browse_id = 'FEhistory'
|
||||
json_path = self.JSON_PATHS['tv_grid']
|
||||
response_type = 'videos'
|
||||
else:
|
||||
browse_id = 'VL' + playlist_id_upper
|
||||
json_path = self.JSON_PATHS['tv_playlist']
|
||||
response_type = 'playlistItems'
|
||||
|
||||
return self.get_browse_items(
|
||||
browse_id=browse_id,
|
||||
playlist_id=playlist_id,
|
||||
response_type=response_type,
|
||||
client='tv',
|
||||
do_auth=True,
|
||||
page_token=page_token,
|
||||
|
|
@ -1315,6 +1325,7 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
def get_browse_items(self,
|
||||
browse_id=None,
|
||||
channel_id=None,
|
||||
playlist_id=None,
|
||||
skip_ids=None,
|
||||
params=None,
|
||||
route=None,
|
||||
|
|
@ -1340,7 +1351,12 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
'youtube#playlistListResponse',
|
||||
'youtube#playlist',
|
||||
'contentId',
|
||||
)
|
||||
),
|
||||
'playlistItems': (
|
||||
'youtube#playlistItemListResponse',
|
||||
'youtube#playlistItem',
|
||||
'contentId',
|
||||
),
|
||||
},
|
||||
data=None,
|
||||
client=None,
|
||||
|
|
@ -1448,6 +1464,13 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
)
|
||||
if skip_ids and _channel_id in skip_ids:
|
||||
continue
|
||||
if playlist_id:
|
||||
_playlist_id = playlist_id
|
||||
else:
|
||||
_playlist_id = self.json_traverse(
|
||||
item,
|
||||
json_path.get('playlist_id'),
|
||||
)
|
||||
items.append({
|
||||
'kind': item_kind,
|
||||
'id': item_id,
|
||||
|
|
@ -1470,6 +1493,7 @@ class YouTubeDataClient(YouTubeLoginClient):
|
|||
),
|
||||
),
|
||||
'channelId': _channel_id,
|
||||
'playlistId': _playlist_id,
|
||||
}
|
||||
})
|
||||
if not items:
|
||||
|
|
|
|||
|
|
@ -18,20 +18,9 @@ from ...kodion import logging
|
|||
class YouTubeLoginClient(YouTubeRequestClient):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ANDROID_CLIENT_AUTH_URL = 'https://android.clients.google.com/auth'
|
||||
DOMAIN_SUFFIX = '.apps.googleusercontent.com'
|
||||
DEVICE_CODE_URL = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
REVOKE_URL = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
SERVICE_URLS = 'oauth2:' + 'https://www.googleapis.com/auth/'.join((
|
||||
'youtube '
|
||||
'youtube.force-ssl '
|
||||
'plus.me '
|
||||
'emeraldsea.mobileapps.doritos.cookie '
|
||||
'plus.stream.read '
|
||||
'plus.stream.write '
|
||||
'plus.pages.manage '
|
||||
'identity.plus.page.impersonation',
|
||||
))
|
||||
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
TOKEN_TYPES = {
|
||||
0: 'tv',
|
||||
|
|
|
|||
|
|
@ -790,6 +790,31 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
'-1': ('original', 'main', -6),
|
||||
}
|
||||
|
||||
FAILURE_REASONS = {
|
||||
'abort': frozenset((
|
||||
'country',
|
||||
'not available',
|
||||
)),
|
||||
'auth': frozenset((
|
||||
'not a bot',
|
||||
'please sign in',
|
||||
)),
|
||||
'reauth': frozenset((
|
||||
'confirm your age',
|
||||
'inappropriate',
|
||||
'member',
|
||||
)),
|
||||
'retry': frozenset((
|
||||
'try again later',
|
||||
'unavailable',
|
||||
'unknown',
|
||||
)),
|
||||
'skip': frozenset((
|
||||
'error code: 6',
|
||||
'latest version',
|
||||
)),
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
context,
|
||||
clients=None,
|
||||
|
|
@ -826,18 +851,18 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
self._auth_client = {}
|
||||
self._client_groups = (
|
||||
('custom', clients if clients else ()),
|
||||
('auth_enabled_initial_request', (
|
||||
('auth_enabled|initial_request|no_playable_streams', (
|
||||
'tv_embed',
|
||||
'tv_unplugged',
|
||||
'tv',
|
||||
)),
|
||||
('auth_disabled_kids_vp9_avc1', (
|
||||
('auth_disabled|kids|av1|vp9|vp9.2|avc1|stereo_sound|multi_audio', (
|
||||
'ios_testsuite_params',
|
||||
)),
|
||||
('auth_disabled_kids_av1_avc1', (
|
||||
('auth_disabled|kids|av1|vp9.2|avc1|surround_sound|multi_audio', (
|
||||
'android_testsuite_params',
|
||||
)),
|
||||
('auth_enabled_no_kids', (
|
||||
('auth_enabled|no_kids|av1|vp9.2|avc1|surround_sound', (
|
||||
'android_vr',
|
||||
)),
|
||||
('mpd', (
|
||||
|
|
@ -1285,7 +1310,11 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
else:
|
||||
new_url = url
|
||||
|
||||
new_url = self._process_url_params(new_url, mpd=False)
|
||||
new_url = self._process_url_params(new_url,
|
||||
mpd=False,
|
||||
headers=headers,
|
||||
referrer=None,
|
||||
visitor_data=None)
|
||||
if not new_url:
|
||||
continue
|
||||
|
||||
|
|
@ -1622,26 +1651,7 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
responses = {}
|
||||
stream_list = {}
|
||||
|
||||
abort_reasons = {
|
||||
'country',
|
||||
'not available',
|
||||
}
|
||||
reauth_reasons = {
|
||||
'confirm your age',
|
||||
'inappropriate',
|
||||
'please sign in',
|
||||
'not a bot',
|
||||
'member',
|
||||
}
|
||||
skip_reasons = {
|
||||
'latest version',
|
||||
'error code: 6',
|
||||
}
|
||||
retry_reasons = {
|
||||
'try again later',
|
||||
'unavailable',
|
||||
'unknown',
|
||||
}
|
||||
fail = self.FAILURE_REASONS
|
||||
abort = False
|
||||
|
||||
logged_in = self.logged_in
|
||||
|
|
@ -1670,17 +1680,17 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
for name, clients in self._client_groups:
|
||||
if not clients:
|
||||
continue
|
||||
if name == 'auth_enabled_initial_request':
|
||||
if name == 'mpd' and not use_mpd:
|
||||
continue
|
||||
if name == 'ask' and use_mpd and not ask_for_quality:
|
||||
continue
|
||||
if name.startswith('auth_enabled|initial_request'):
|
||||
if visitor_data and not logged_in:
|
||||
continue
|
||||
allow_skip = False
|
||||
client_data['_auth_requested'] = True
|
||||
else:
|
||||
allow_skip = True
|
||||
if name == 'mpd' and not use_mpd:
|
||||
continue
|
||||
if name == 'ask' and use_mpd and not ask_for_quality:
|
||||
continue
|
||||
|
||||
exclude_retry = set()
|
||||
restart = None
|
||||
|
|
@ -1792,8 +1802,17 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
video_id=video_id,
|
||||
client=_client_name,
|
||||
has_auth=_has_auth)
|
||||
compare_reason = _reason.lower()
|
||||
if any(why in compare_reason for why in reauth_reasons):
|
||||
fail_reason = _reason.lower()
|
||||
if any(why in fail_reason for why in fail['auth']):
|
||||
if _has_auth:
|
||||
restart = False
|
||||
elif restart is None and logged_in:
|
||||
client_data['_auth_requested'] = True
|
||||
restart = True
|
||||
else:
|
||||
continue
|
||||
break
|
||||
elif any(why in fail_reason for why in fail['reauth']):
|
||||
if _client.get('_auth_required') == 'ignore_fail':
|
||||
continue
|
||||
elif client_data.get('_auth_required'):
|
||||
|
|
@ -1803,13 +1822,13 @@ class YouTubePlayerClient(YouTubeDataClient):
|
|||
client_data['_auth_required'] = True
|
||||
restart = True
|
||||
break
|
||||
if any(why in compare_reason for why in abort_reasons):
|
||||
elif any(why in fail_reason for why in fail['abort']):
|
||||
abort = True
|
||||
break
|
||||
if any(why in compare_reason for why in skip_reasons):
|
||||
elif any(why in fail_reason for why in fail['skip']):
|
||||
if allow_skip:
|
||||
break
|
||||
if any(why in compare_reason for why in retry_reasons):
|
||||
elif any(why in fail_reason for why in fail['retry']):
|
||||
continue
|
||||
else:
|
||||
self.log.warning('Unknown playabilityStatus: {status!r}',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from re import compile as re_compile
|
|||
|
||||
from ...kodion import logging
|
||||
from ...kodion.compatibility import parse_qsl, unescape, urlencode, urlsplit
|
||||
from ...kodion.constants import YOUTUBE_HOSTNAMES
|
||||
from ...kodion.network import BaseRequestsClass
|
||||
|
||||
|
||||
|
|
@ -63,16 +64,16 @@ class YouTubeResolver(AbstractResolver):
|
|||
r'|(?P<is_clip>"clipConfig":\{)'
|
||||
r'|("startTimeMs":"(?P<start_time>\d+)")'
|
||||
r'|("endTimeMs":"(?P<end_time>\d+)")')
|
||||
_RE_MUSIC_VIDEO_ID = re_compile(r'"INITIAL_ENDPOINT":.+?videoId\\":\\"'
|
||||
r'(?P<video_id>[^\\"]+)'
|
||||
r'\\"')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(YouTubeResolver, self).__init__(*args, **kwargs)
|
||||
|
||||
def supports_url(self, url, url_components):
|
||||
if url_components.hostname not in {
|
||||
'www.youtube.com',
|
||||
'youtube.com',
|
||||
'm.youtube.com',
|
||||
}:
|
||||
hostname = url_components.hostname
|
||||
if hostname not in YOUTUBE_HOSTNAMES:
|
||||
return False
|
||||
|
||||
path = url_components.path.lower()
|
||||
|
|
@ -91,10 +92,14 @@ class YouTubeResolver(AbstractResolver):
|
|||
'/redirect',
|
||||
'/shorts',
|
||||
'/supported_browsers',
|
||||
'/watch',
|
||||
)):
|
||||
return 'HEAD'
|
||||
|
||||
if path.startswith('/watch'):
|
||||
if hostname.startswith('music.'):
|
||||
return 'GET'
|
||||
return 'HEAD'
|
||||
|
||||
# user channel in the form of youtube.com/username
|
||||
path = path.strip('/').split('/', 1)
|
||||
return 'GET' if len(path) == 1 and path[0] else False
|
||||
|
|
@ -192,7 +197,17 @@ class YouTubeResolver(AbstractResolver):
|
|||
query=urlencode(new_params)
|
||||
).geturl()
|
||||
|
||||
# we try to extract the channel id from the html content
|
||||
# try to extract the real videoId from the html content
|
||||
elif method == 'GET' and url_components.hostname.startswith('music.'):
|
||||
match = self._RE_MUSIC_VIDEO_ID.search(response_text)
|
||||
if match:
|
||||
params = dict(parse_qsl(url_components.query))
|
||||
params['v'] = match.group('video_id')
|
||||
return url_components._replace(
|
||||
query=urlencode(params)
|
||||
).geturl()
|
||||
|
||||
# try to extract the channel id from the html content
|
||||
# With the channel id we can construct a URL we already work with
|
||||
# https://www.youtube.com/channel/<CHANNEL_ID>
|
||||
elif method == 'GET':
|
||||
|
|
@ -217,11 +232,7 @@ class CommonResolver(AbstractResolver):
|
|||
super(CommonResolver, self).__init__(*args, **kwargs)
|
||||
|
||||
def supports_url(self, url, url_components):
|
||||
if url_components.hostname in {
|
||||
'www.youtube.com',
|
||||
'youtube.com',
|
||||
'm.youtube.com',
|
||||
}:
|
||||
if url_components.hostname in YOUTUBE_HOSTNAMES:
|
||||
return False
|
||||
return 'HEAD'
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from ...kodion.constants import (
|
|||
START,
|
||||
VIDEO_ID,
|
||||
VIDEO_IDS,
|
||||
YOUTUBE_HOSTNAMES,
|
||||
)
|
||||
from ...kodion.items import DirectoryItem, UriItem, VideoItem
|
||||
from ...kodion.utils.convert_format import duration_to_seconds
|
||||
|
|
@ -41,11 +42,6 @@ class UrlToItemConverter(object):
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
RE_PATH_ID = re_compile(r'/[^/]*?[/@](?P<id>[^/?#]+)', IGNORECASE)
|
||||
VALID_HOSTNAMES = {
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
'm.youtube.com',
|
||||
}
|
||||
|
||||
def __init__(self, flatten=True):
|
||||
self._flatten = flatten
|
||||
|
|
@ -67,7 +63,7 @@ class UrlToItemConverter(object):
|
|||
def add_url(self, url):
|
||||
parsed_url = urlsplit(url)
|
||||
if (not parsed_url.hostname
|
||||
or parsed_url.hostname.lower() not in self.VALID_HOSTNAMES):
|
||||
or parsed_url.hostname.lower() not in YOUTUBE_HOSTNAMES):
|
||||
self.log.debug('Unknown hostname "{hostname}" in url "{url}"',
|
||||
hostname=parsed_url.hostname,
|
||||
url=url)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,13 @@ from ...kodion.constants import (
|
|||
PATHS,
|
||||
PLAYLIST_ID,
|
||||
)
|
||||
from ...kodion.items import AudioItem, CommandItem, DirectoryItem, menu_items
|
||||
from ...kodion.items import (
|
||||
AudioItem,
|
||||
CommandItem,
|
||||
DirectoryItem,
|
||||
MediaItem,
|
||||
menu_items,
|
||||
)
|
||||
from ...kodion.utils.convert_format import friendly_number, strip_html_from_text
|
||||
from ...kodion.utils.datetime import (
|
||||
get_scheduled_start,
|
||||
|
|
@ -401,7 +407,8 @@ def update_playlist_items(provider, context, playlist_id_dict,
|
|||
show_details = settings.show_detailed_description()
|
||||
item_count_color = settings.get_label_color('itemCount')
|
||||
|
||||
fanart_type = context.get_param(FANART_TYPE)
|
||||
params = context.get_params()
|
||||
fanart_type = params.get(FANART_TYPE)
|
||||
if fanart_type is None:
|
||||
fanart_type = settings.fanart_selection()
|
||||
thumb_size = settings.get_thumbnail_size()
|
||||
|
|
@ -443,6 +450,7 @@ def update_playlist_items(provider, context, playlist_id_dict,
|
|||
cxm_play_recently_added = menu_items.playlist_play_recently_added(context)
|
||||
cxm_view_playlist = menu_items.playlist_view(context)
|
||||
cxm_play_shuffled_playlist = menu_items.playlist_shuffle(context)
|
||||
cxm_refresh_listing = menu_items.refresh_listing(context, path, params)
|
||||
cxm_remove_saved_playlist = menu_items.playlist_remove_from_library(context)
|
||||
cxm_save_playlist = (
|
||||
menu_items.playlist_save_to_library(context)
|
||||
|
|
@ -585,6 +593,7 @@ def update_playlist_items(provider, context, playlist_id_dict,
|
|||
cxm_play_recently_added,
|
||||
cxm_view_playlist,
|
||||
cxm_play_shuffled_playlist,
|
||||
cxm_refresh_listing,
|
||||
cxm_separator,
|
||||
cxm_save_playlist,
|
||||
menu_items.bookmark_add(
|
||||
|
|
@ -1238,6 +1247,7 @@ if PREFER_WEBP_THUMBS:
|
|||
THUMB_URL = 'https://i.ytimg.com/vi_webp/{0}/{1}{2}.webp'
|
||||
else:
|
||||
THUMB_URL = 'https://i.ytimg.com/vi/{0}/{1}{2}.jpg'
|
||||
RE_CUSTOM_THUMB = re_compile(r'_custom_[0-9]')
|
||||
THUMB_TYPES = {
|
||||
'default': {
|
||||
'name': 'default',
|
||||
|
|
@ -1326,10 +1336,17 @@ def get_thumbnail(thumb_size, thumbnails, default_thumb=None):
|
|||
url = (thumbnail[1] if is_dict else thumbnail).get('url')
|
||||
if not url:
|
||||
return default_thumb
|
||||
if PREFER_WEBP_THUMBS and '/vi_webp/' not in url and '?' not in url:
|
||||
url = url.replace('/vi/', '/vi_webp/', 1).replace('.jpg', '.webp', 1)
|
||||
if url.startswith('//'):
|
||||
url = 'https:' + url
|
||||
if '?' in url:
|
||||
url = urlsplit(url)
|
||||
url = url._replace(
|
||||
netloc='i.ytimg.com',
|
||||
path=RE_CUSTOM_THUMB.sub('', url.path),
|
||||
query=None,
|
||||
).geturl()
|
||||
elif PREFER_WEBP_THUMBS and '/vi_webp/' not in url:
|
||||
url = url.replace('/vi/', '/vi_webp/', 1).replace('.jpg', '.webp', 1)
|
||||
return url
|
||||
|
||||
|
||||
|
|
@ -1360,6 +1377,7 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id):
|
|||
next_item = next((
|
||||
item for item in result_items
|
||||
if (item
|
||||
and isinstance(item, MediaItem)
|
||||
and not any((
|
||||
item.get_uri() == playlist_item.get('file')
|
||||
or item.get_name() == playlist_item.get('title')
|
||||
|
|
|
|||
|
|
@ -364,8 +364,12 @@ def _process_list_response(provider,
|
|||
item.available = yt_item.get('_available', False)
|
||||
|
||||
elif kind_type == 'playlistitem':
|
||||
playlist_item_id = item_id
|
||||
video_id = snippet['resourceId']['videoId']
|
||||
video_id = snippet.get('resourceId', {}).get('videoId')
|
||||
if video_id:
|
||||
playlist_item_id = item_id
|
||||
else:
|
||||
video_id = item_id
|
||||
playlist_item_id = None
|
||||
channel_id = (snippet.get('videoOwnerChannelId')
|
||||
or snippet.get('channelId'))
|
||||
playlist_id = snippet.get('playlistId')
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ def _play_stream(provider, context):
|
|||
ask_for_quality = settings.ask_for_video_quality()
|
||||
if ui.pop_property(PLAY_PROMPT_QUALITY) and not screensaver:
|
||||
ask_for_quality = True
|
||||
elif ui.pop_property(PLAY_FORCE_AUDIO):
|
||||
if ui.pop_property(PLAY_FORCE_AUDIO):
|
||||
audio_only = True
|
||||
else:
|
||||
audio_only = settings.audio_only()
|
||||
|
|
@ -97,6 +97,7 @@ def _play_stream(provider, context):
|
|||
|
||||
if not streams:
|
||||
ui.show_notification(context.localize('error.no_streams_found'))
|
||||
logging.debug('No streams found')
|
||||
return False
|
||||
|
||||
stream = _select_stream(
|
||||
|
|
@ -106,7 +107,6 @@ def _play_stream(provider, context):
|
|||
audio_only=audio_only,
|
||||
use_mpd=use_mpd,
|
||||
)
|
||||
|
||||
if stream is None:
|
||||
return False
|
||||
|
||||
|
|
@ -345,7 +345,6 @@ def _select_stream(context,
|
|||
|
||||
stream_list.sort(key=_stream_sort, reverse=True)
|
||||
num_streams = len(stream_list)
|
||||
ask_for_quality = ask_for_quality and num_streams >= 1
|
||||
|
||||
if logging.debugging:
|
||||
def _default_NA():
|
||||
|
|
@ -362,7 +361,7 @@ def _select_stream(context,
|
|||
idx=idx,
|
||||
stream=defaultdict(_default_NA, stream))
|
||||
|
||||
if ask_for_quality:
|
||||
if ask_for_quality and num_streams > 1:
|
||||
selected_stream = context.get_ui().on_select(
|
||||
context.localize('select_video_quality'),
|
||||
[stream['title'] for stream in stream_list],
|
||||
|
|
|
|||
|
|
@ -242,11 +242,13 @@ def _process_disliked_videos(provider, context, client):
|
|||
|
||||
def _process_live_events(provider, context, client, event_type='live'):
|
||||
# TODO: cache result
|
||||
params = context.get_params()
|
||||
json_data = client.get_live_events(
|
||||
event_type=event_type,
|
||||
order='date' if event_type == 'upcoming' else 'viewCount',
|
||||
page_token=context.get_param('page_token', ''),
|
||||
location=context.get_param('location', False),
|
||||
order=params.get('order',
|
||||
'date' if event_type == 'upcoming' else 'viewCount'),
|
||||
page_token=params.get('page_token', ''),
|
||||
location=params.get('location', False),
|
||||
after={'days': 3} if event_type == 'completed' else None,
|
||||
)
|
||||
if not json_data:
|
||||
|
|
|
|||
|
|
@ -1488,13 +1488,14 @@ class Provider(AbstractProvider):
|
|||
# watch later
|
||||
if settings_bool(settings.SHOW_WATCH_LATER, True):
|
||||
if watch_later_id:
|
||||
path = (
|
||||
(PATHS.VIRTUAL_PLAYLIST, watch_later_id)
|
||||
if watch_later_id.lower() == 'wl' else
|
||||
(PATHS.MY_PLAYLIST, watch_later_id)
|
||||
)
|
||||
watch_later_item = DirectoryItem(
|
||||
localize('watch_later'),
|
||||
create_uri(
|
||||
(PATHS.VIRTUAL_PLAYLIST, watch_later_id)
|
||||
if watch_later_id.lower() == 'wl' else
|
||||
(PATHS.MY_PLAYLIST, watch_later_id)
|
||||
),
|
||||
create_uri(path),
|
||||
image='{media}/watch_later.png',
|
||||
)
|
||||
context_menu = [
|
||||
|
|
@ -1510,6 +1511,9 @@ class Provider(AbstractProvider):
|
|||
menu_items.playlist_shuffle(
|
||||
context, watch_later_id
|
||||
),
|
||||
menu_items.refresh_listing(
|
||||
context, path, {}
|
||||
),
|
||||
]
|
||||
watch_later_item.add_context_menu(context_menu)
|
||||
result.append(watch_later_item)
|
||||
|
|
@ -1541,9 +1545,10 @@ class Provider(AbstractProvider):
|
|||
playlists = resource_manager.get_related_playlists('mine')
|
||||
if playlists and 'likes' in playlists:
|
||||
liked_list_id = playlists['likes'] or 'LL'
|
||||
path = (PATHS.VIRTUAL_PLAYLIST, liked_list_id)
|
||||
liked_videos_item = DirectoryItem(
|
||||
localize('video.liked'),
|
||||
create_uri((PATHS.VIRTUAL_PLAYLIST, liked_list_id)),
|
||||
create_uri(path),
|
||||
image='{media}/likes.png',
|
||||
)
|
||||
context_menu = [
|
||||
|
|
@ -1559,6 +1564,9 @@ class Provider(AbstractProvider):
|
|||
menu_items.playlist_shuffle(
|
||||
context, liked_list_id
|
||||
),
|
||||
menu_items.refresh_listing(
|
||||
context, path, {}
|
||||
),
|
||||
]
|
||||
liked_videos_item.add_context_menu(context_menu)
|
||||
result.append(liked_videos_item)
|
||||
|
|
@ -1575,13 +1583,14 @@ class Provider(AbstractProvider):
|
|||
# history
|
||||
if settings_bool(settings.SHOW_HISTORY, True):
|
||||
if history_id:
|
||||
path = (
|
||||
(PATHS.VIRTUAL_PLAYLIST, history_id)
|
||||
if history_id.lower() == 'hl' else
|
||||
(PATHS.MY_PLAYLIST, history_id)
|
||||
)
|
||||
watch_history_item = DirectoryItem(
|
||||
localize('history'),
|
||||
create_uri(
|
||||
(PATHS.VIRTUAL_PLAYLIST, history_id)
|
||||
if history_id.lower() == 'hl' else
|
||||
(PATHS.MY_PLAYLIST, history_id)
|
||||
),
|
||||
create_uri(path),
|
||||
image='{media}/history.png',
|
||||
)
|
||||
context_menu = [
|
||||
|
|
@ -1597,6 +1606,9 @@ class Provider(AbstractProvider):
|
|||
menu_items.playlist_shuffle(
|
||||
context, history_id
|
||||
),
|
||||
menu_items.refresh_listing(
|
||||
context, path, {}
|
||||
),
|
||||
]
|
||||
watch_history_item.add_context_menu(context_menu)
|
||||
result.append(watch_history_item)
|
||||
|
|
@ -2210,7 +2222,6 @@ class Provider(AbstractProvider):
|
|||
attrs = (
|
||||
'_resource_manager',
|
||||
'_client',
|
||||
'_api_check',
|
||||
)
|
||||
for attr in attrs:
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue