diff --git a/addon.xml b/addon.xml index 8f0ef4643..2c8ffd672 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index f9c853aa1..aa93b75e7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,22 @@ +## v7.1.1+beta.6 +### Fixed +- Ensure http server is started prior to creating MPD for playback #961 +- Fix shuffle play of playlists not working +- Fix plugin category label not being applied if content type not set +- Fix Kodi navigating to root path of Video window if plugin listing fails + +### Changed +- Use redirect in multiple busy dialog crash workaround #938 +- Disable unusable alternate players #966 +- Standardise plugin URIs for routing + - path parameters used for folders and sub-folders + - query parameters used for changing display modes, filtering, sorting and inputs + +### New +- Add Quick Search and search management context menu items to Search folders +- Add context menu items to Clear and Play All/Shuffle in Bookmarks/Watch Later/Watch History folders +- Add progress dialog to My Subscription loading + ## v7.1.1+beta.5 ### Fixed - Fix Python2/Android incompatibilty when checking CPU count #958 diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 02486db08..7ceaaa048 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -34,6 +34,7 @@ class AbstractProvider(object): RESULT_CACHE_TO_DISC = 'cache_to_disc' # (bool) RESULT_FORCE_RESOLVE = 'force_resolve' # (bool) + RESULT_TRY_FALLBACK = 'try_fallback' # (bool) RESULT_UPDATE_LISTING = 'update_listing' # (bool) # map for regular expression (path) to method (names) @@ -68,13 +69,13 @@ def __init__(self): self.register_path(r''.join(( '^', PATHS.WATCH_LATER, - '/(?Padd|clear|list|remove)/?$' + '/(?Padd|clear|list|play|remove)/?$' )), self.on_watch_later) self.register_path(r''.join(( '^', PATHS.BOOKMARKS, - '/(?Padd|clear|list|remove)/?$' + '/(?Padd|clear|list|play|remove)/?$' )), self.on_bookmarks) self.register_path(r''.join(( @@ -86,34 +87,34 @@ def __init__(self): self.register_path(r''.join(( '^', PATHS.HISTORY, - '/?$' + '/(?Pclear|list|mark_unwatched|mark_watched|play|remove|reset_resume)/?$' )), self.on_playback_history) self.register_path(r'(?P.*\/)extrafanart\/([\?#].+)?$', self.on_extra_fanart) @classmethod - def register_path(cls, re_path, method=None): + def register_path(cls, re_path, command=None): """ Registers a new method for the given regular expression :param re_path: regular expression of the path - :param method: method or function to be registered + :param command: command or function to be registered :return: """ - def wrapper(method): - if callable(method): - func = method + def wrapper(command): + if callable(command): + func = command else: - func = getattr(method, '__func__', None) + func = getattr(command, '__func__', None) if not callable(func): return None cls._dict_path[re.compile(re_path, re.UNICODE)] = func - return method + return command - if method: - return wrapper(method) + if command: + return wrapper(command) return wrapper def run_wizard(self, context): @@ -257,13 +258,30 @@ def reroute(self, context, path=None, params=None, uri=None): path, params = uri if not path: + context.log_error('Rerouting - No route path') return False + window_fallback = params.pop('window_fallback', False) + window_replace = params.pop('window_replace', False) + window_return = params.pop('window_return', True) + + if window_fallback: + container_uri = context.get_infolabel('Container.FolderPath') + if context.is_plugin_path(container_uri): + context.log_debug('Rerouting - Fallback route not required') + return ( + False, + { + self.RESULT_TRY_FALLBACK: False, + }, + ) + if 'refresh' in params: container = context.get_infolabel('System.CurrentControlId') position = context.get_infolabel('Container.CurrentItem') params['refresh'] += 1 elif path == current_path and params == current_params: + context.log_error('Rerouting - Unable to reroute to current path') return False else: container = None @@ -271,8 +289,6 @@ def reroute(self, context, path=None, params=None, uri=None): result = None function_cache = context.get_function_cache() - window_replace = params.pop('window_replace', False) - window_return = params.pop('window_return', True) try: result, options = function_cache.run( self.navigate, @@ -287,10 +303,12 @@ def reroute(self, context, path=None, params=None, uri=None): uri = context.create_uri(path, params) if result: context.log_debug('Rerouting - Success' - '\n\tURI: {uri}' - '\n\tReplace: |{window_replace}|' - '\n\tReturn: |{window_return}|' + '\n\tURI: {uri}' + '\n\tFallback: |{window_fallback}|' + '\n\tReplace: |{window_replace}|' + '\n\tReturn: |{window_return}|' .format(uri=uri, + window_fallback=window_fallback, window_replace=window_replace, window_return=window_return)) else: diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index ba3ac74bd..dc0ddd6d8 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -13,7 +13,14 @@ import os from ..logger import Logger -from ..compatibility import parse_qsl, quote, to_str, urlencode, urlsplit +from ..compatibility import ( + parse_qsl, + quote, + to_str, + unquote, + urlencode, + urlsplit, +) from ..constants import ( PATHS, PLAY_FORCE_AUDIO, @@ -62,6 +69,7 @@ class AbstractContext(Logger): 'logged_in', 'resume', 'screensaver', + 'window_fallback', 'window_replace', 'window_return', } @@ -139,9 +147,13 @@ def __init__(self, path='/', params=None, plugin_id=''): self._plugin_icon = None self._version = 'UNKNOWN' - self._path = self.create_path(path) + self._path = path + self._path_parts = [] + self.set_path(path, force=True) + self._params = params or {} self.parse_params(self._params) + self._uri = self.create_uri(self._path, self._params) @staticmethod @@ -265,7 +277,9 @@ def create_uri(self, path=None, params=None, run=False): uri = self._plugin_id.join(('plugin://', uri)) if params: - uri = '?'.join((uri, urlencode(params))) + if isinstance(params, (dict, list, tuple)): + params = urlencode(params) + uri = '?'.join((uri, params)) return ''.join(( 'RunPlugin(', @@ -273,32 +287,42 @@ def create_uri(self, path=None, params=None, run=False): ')' )) if run else uri + def get_parent_uri(self, **kwargs): + return self.create_uri(self._path_parts[:-1], **kwargs) + @staticmethod def create_path(*args, **kwargs): - path = '/'.join([ - part - for part in [ + include_parts = kwargs.get('parts') + parts = [ + part for part in [ str(arg).strip('/').replace('\\', '/').replace('//', '/') for arg in args ] if part - ]) - if path: - path = path.join(('/', '/')) + ] + if parts: + path = '/'.join(parts).join(('/', '/')) + if path.startswith(PATHS.ROUTE): + parts = parts[2:] + elif path.startswith(PATHS.COMMAND): + parts = [] + elif path.startswith(PATHS.GOTO_PAGE): + parts = parts[2:] + if parts and parts[0].isnumeric(): + parts = parts[1:] else: - return '/' + return ('/', parts) if include_parts else '/' if kwargs.get('is_uri'): - return quote(path) - return path + path = quote(path) + return (path, parts) if include_parts else path def get_path(self): return self._path def set_path(self, *path, **kwargs): if kwargs.get('force'): - self._path = path[0] - else: - self._path = self.create_path(*path) + path = unquote(path[0]).split('/') + self._path, self._path_parts = self.create_path(*path, parts=True) def get_params(self): return self._params @@ -308,7 +332,7 @@ def get_param(self, name, default=None): def parse_uri(self, uri): uri = urlsplit(uri) - path = uri.path + path = uri.path.rstrip('/') params = self.parse_params( dict(parse_qsl(uri.query, keep_blank_values=True)), update=False, diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index d167057f7..4368ccbb3 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -18,7 +18,6 @@ from ..abstract_context import AbstractContext from ...compatibility import ( parse_qsl, - unquote, urlsplit, xbmc, xbmcaddon, @@ -142,6 +141,7 @@ class XbmcContext(AbstractContext): 'my_channel': 30507, 'my_location': 30654, 'my_subscriptions': 30510, + 'my_subscriptions.loading': 575, 'my_subscriptions.filter.add': 30587, 'my_subscriptions.filter.added': 30589, 'my_subscriptions.filter.remove': 30588, @@ -392,8 +392,7 @@ def init(self): return # first the path of the uri - parsed_url = urlsplit(uri) - self._path = unquote(parsed_url.path) + self.set_path(urlsplit(uri).path, force=True) # after that try to get the params self._params = {} @@ -547,20 +546,26 @@ def set_content(self, content_type, sub_type=None, category_label=None): )) def apply_content(self): + # ui local variable used for ui.get_view_manager() in unofficial version ui = self.get_ui() + content_type = ui.pop_property(CONTENT_TYPE) - if not content_type: - return + if content_type: + content_type, sub_type, category_label = json.loads(content_type) + self.log_debug('Applying content-type: |{type}| for |{path}|'.format( + type=(sub_type or content_type), path=self.get_path() + )) + xbmcplugin.setContent(self._plugin_handle, content_type) + else: + content_type = None + sub_type = None + category_label = None - content_type, sub_type, category_label = json.loads(content_type) - self.log_debug('Applying content-type: |{type}| for |{path}|'.format( - type=(sub_type or content_type), path=self.get_path() - )) - xbmcplugin.setContent(self._plugin_handle, content_type) if category_label is None: category_label = self.get_param('category_label') if category_label: xbmcplugin.setPluginCategory(self._plugin_handle, category_label) + detailed_labels = self.get_settings().show_detailed_labels() if sub_type == 'history': self.add_sort_method( @@ -582,6 +587,7 @@ def apply_content(self): (SORT.UNSORTED,), (SORT.LABEL,), ) + if content_type == CONTENT.VIDEO_CONTENT: self.add_sort_method( (SORT.CHANNEL, '[%A - ]%T \u2022 %P', '%D | %J'), @@ -825,7 +831,7 @@ def wakeup(self, target, timeout=None, payload=None): data.update(payload) self.send_notification(WAKEUP, data) if not timeout: - return + return None pop_property = self.get_ui().pop_property no_timeout = timeout < 0 @@ -834,17 +840,34 @@ def wakeup(self, target, timeout=None, payload=None): wait_period = wait_period_ms / 1000 while no_timeout or remaining > 0: - awake = pop_property(WAKEUP) - if awake: - if awake == target: - self.log_debug('Wakeup |{0}| in {1}ms' - .format(awake, timeout - remaining)) - else: - self.log_error('Wakeup |{0}| in {1}ms - expected |{2}|' - .format(awake, timeout - remaining, target)) + data = pop_property(WAKEUP) + if data: + data = json.loads(data) + + if data: + response = data.get('response') + response_target = data.get('target') or 'Unknown' + + if target == response_target: + if response: + self.log_debug('Wakeup |{0}| in {1}ms' + .format(response_target, + timeout - remaining)) + else: + self.log_error('Wakeup |{0}| in {1}ms - failed' + .format(response_target, + timeout - remaining)) + return response + + self.log_error('Wakeup |{0}| in {1}ms - expected |{2}|' + .format(response_target, + timeout - remaining, + target)) break + wait(wait_period) remaining -= wait_period_ms else: self.log_error('Wakeup |{0}| timed out in {1}ms' .format(target, timeout)) + return False diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index 48bb1ac62..16e888f58 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -16,10 +16,8 @@ from .directory_item import DirectoryItem from .image_item import ImageItem from .media_item import AudioItem, MediaItem, VideoItem -from .new_search_item import NewSearchItem from .next_page_item import NextPageItem -from .search_history_item import SearchHistoryItem -from .search_item import SearchItem +from .search_items import NewSearchItem, SearchHistoryItem, SearchItem from .uri_item import UriItem from .utils import from_json from .watch_later_item import WatchLaterItem diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 32b43952b..fe44b0b28 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -90,7 +90,7 @@ def parse_item_ids_from_uri(self): item_ids = {} uri = urlsplit(self._uri) - path = uri.path + path = uri.path.rstrip('/') params = dict(parse_qsl(uri.query)) video_id = params.get('video_id') diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 886588b77..0f524a0d9 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -102,6 +102,21 @@ def refresh(context): ) +def play_all_from(context, route, order='normal'): + return ( + context.localize('playlist.play.shuffle') + if order == 'shuffle' else + context.localize('playlist.play.all'), + context.create_uri( + (route, 'play',), + { + 'order': order, + }, + run=True, + ), + ) + + def queue_video(context): return ( context.localize('video.queue'), @@ -159,7 +174,7 @@ def shuffle_playlist(context, playlist_id): (PATHS.ROUTE, PATHS.PLAY,), { 'playlist_id': playlist_id, - 'order': 'random', + 'order': 'shuffle', 'action': 'list', }, run=True, @@ -285,10 +300,9 @@ def remove_my_subscriptions_filter(context, channel_name): return ( context.localize('my_subscriptions.filter.remove'), context.create_uri( - ('my_subscriptions', 'filter',), + ('my_subscriptions', 'filter', 'remove'), { 'item_name': channel_name, - 'action': 'remove' }, run=True, ), @@ -299,10 +313,9 @@ def add_my_subscriptions_filter(context, channel_name): return ( context.localize('my_subscriptions.filter.add'), context.create_uri( - ('my_subscriptions', 'filter',), + ('my_subscriptions', 'filter', 'add',), { 'item_name': channel_name, - 'action': 'add', }, run=True, ), @@ -482,9 +495,8 @@ def history_remove(context, video_id, video_name=''): return ( context.localize('history.remove'), context.create_uri( - (PATHS.HISTORY,), + (PATHS.HISTORY, 'remove',), { - 'action': 'remove', 'video_id': video_id, 'item_name': video_name, }, @@ -497,10 +509,7 @@ def history_clear(context): return ( context.localize('history.clear'), context.create_uri( - (PATHS.HISTORY,), - { - 'action': 'clear' - }, + (PATHS.HISTORY, 'clear',), run=True, ), ) @@ -510,10 +519,9 @@ def history_mark_watched(context, video_id): return ( context.localize('history.mark.watched'), context.create_uri( - (PATHS.HISTORY,), + (PATHS.HISTORY, 'mark_watched',), { 'video_id': video_id, - 'action': 'mark_watched', }, run=True, ), @@ -524,10 +532,9 @@ def history_mark_unwatched(context, video_id): return ( context.localize('history.mark.unwatched'), context.create_uri( - (PATHS.HISTORY,), + (PATHS.HISTORY, 'mark_unwatched',), { 'video_id': video_id, - 'action': 'mark_unwatched', }, run=True, ), @@ -538,10 +545,9 @@ def history_reset_resume(context, video_id): return ( context.localize('history.reset.resume_point'), context.create_uri( - (PATHS.HISTORY,), + (PATHS.HISTORY, 'reset_resume',), { 'video_id': video_id, - 'action': 'reset_resume', }, run=True, ), @@ -680,11 +686,20 @@ def goto_home(context): ) -def goto_quick_search(context): +def goto_quick_search(context, params=None, incognito=None): + if params is None: + params = {} + if incognito is None: + incognito = params.get('incognito') + else: + params['incognito'] = incognito return ( - context.localize('search.quick'), + context.localize('search.quick.incognito' + if incognito else + 'search.quick'), context.create_uri( (PATHS.ROUTE, PATHS.SEARCH, 'input',), + params, run=True, ), ) diff --git a/resources/lib/youtube_plugin/kodion/items/new_search_item.py b/resources/lib/youtube_plugin/kodion/items/new_search_item.py deleted file mode 100644 index 3eeab206a..000000000 --- a/resources/lib/youtube_plugin/kodion/items/new_search_item.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from .directory_item import DirectoryItem -from ..constants import PATHS - - -class NewSearchItem(DirectoryItem): - def __init__(self, - context, - name=None, - image=None, - fanart=None, - incognito=False, - channel_id='', - addon_id='', - location=False): - if not name: - name = context.get_ui().bold(context.localize('search.new')) - - if image is None: - image = '{media}/new_search.png' - - params = {} - if addon_id: - params['addon_id'] = addon_id - if incognito: - params['incognito'] = incognito - if channel_id: - params['channel_id'] = channel_id - if location: - params['location'] = location - - super(NewSearchItem, self).__init__(name, - context.create_uri( - (PATHS.SEARCH, 'input',), - params=params, - ), - image=image, - fanart=fanart) diff --git a/resources/lib/youtube_plugin/kodion/items/search_history_item.py b/resources/lib/youtube_plugin/kodion/items/search_history_item.py deleted file mode 100644 index ba406f877..000000000 --- a/resources/lib/youtube_plugin/kodion/items/search_history_item.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from . import menu_items -from .directory_item import DirectoryItem -from ..constants import PATHS - - -class SearchHistoryItem(DirectoryItem): - def __init__(self, context, query, image=None, fanart=None, location=False): - if image is None: - image = '{media}/search.png' - - if isinstance(query, dict): - params = query - query = params['q'] - else: - params = {'q': query} - if location: - params['location'] = location - - super(SearchHistoryItem, self).__init__(query, - context.create_uri( - (PATHS.SEARCH, 'query',), - params=params, - ), - image=image, - fanart=fanart) - - context_menu = [ - menu_items.search_remove(context, query), - menu_items.search_rename(context, query), - menu_items.search_clear(context), - menu_items.separator(), - menu_items.search_sort_by(context, params, 'relevance'), - menu_items.search_sort_by(context, params, 'date'), - menu_items.search_sort_by(context, params, 'viewCount'), - menu_items.search_sort_by(context, params, 'rating'), - menu_items.search_sort_by(context, params, 'title'), - ] - self.add_context_menu(context_menu) diff --git a/resources/lib/youtube_plugin/kodion/items/search_item.py b/resources/lib/youtube_plugin/kodion/items/search_item.py deleted file mode 100644 index 4329c450b..000000000 --- a/resources/lib/youtube_plugin/kodion/items/search_item.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2018 plugin.video.youtube - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from __future__ import absolute_import, division, unicode_literals - -from .directory_item import DirectoryItem -from ..constants import PATHS - - -class SearchItem(DirectoryItem): - def __init__(self, - context, - name=None, - image=None, - fanart=None, - location=False): - if not name: - name = context.localize('search') - - if image is None: - image = '{media}/search.png' - - params = {} - if location: - params['location'] = location - - super(SearchItem, self).__init__(name, - context.create_uri( - (PATHS.SEARCH, 'list',), - params=params, - ), - image=image, - fanart=fanart) diff --git a/resources/lib/youtube_plugin/kodion/items/search_items.py b/resources/lib/youtube_plugin/kodion/items/search_items.py new file mode 100644 index 000000000..e45ad011e --- /dev/null +++ b/resources/lib/youtube_plugin/kodion/items/search_items.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2014-2016 bromix (plugin.video.youtube) + Copyright (C) 2016-2018 plugin.video.youtube + + SPDX-License-Identifier: GPL-2.0-only + See LICENSES/GPL-2.0-only for more information. +""" + +from __future__ import absolute_import, division, unicode_literals + +from . import menu_items +from .directory_item import DirectoryItem +from ..constants import PATHS + + +class SearchItem(DirectoryItem): + def __init__(self, + context, + name=None, + image=None, + fanart=None, + location=False): + if not name: + name = context.localize('search') + + if image is None: + image = '{media}/search.png' + + params = {} + if location: + params['location'] = location + + super(SearchItem, self).__init__(name, + context.create_uri( + (PATHS.SEARCH, 'list',), + params=params, + ), + image=image, + fanart=fanart) + + context_menu = [ + menu_items.search_clear(context), + menu_items.separator(), + menu_items.goto_quick_search(context, params), + menu_items.goto_quick_search(context, params, incognito=True) + ] + self.add_context_menu(context_menu) + + +class SearchHistoryItem(DirectoryItem): + def __init__(self, context, query, image=None, fanart=None, location=False): + if image is None: + image = '{media}/search.png' + + if isinstance(query, dict): + params = query + query = params['q'] + else: + params = {'q': query} + if location: + params['location'] = location + + super(SearchHistoryItem, self).__init__(query, + context.create_uri( + (PATHS.SEARCH, 'query',), + params=params, + ), + image=image, + fanart=fanart) + + context_menu = [ + menu_items.search_remove(context, query), + menu_items.search_rename(context, query), + menu_items.search_clear(context), + menu_items.separator(), + menu_items.search_sort_by(context, params, 'relevance'), + menu_items.search_sort_by(context, params, 'date'), + menu_items.search_sort_by(context, params, 'viewCount'), + menu_items.search_sort_by(context, params, 'rating'), + menu_items.search_sort_by(context, params, 'title'), + ] + self.add_context_menu(context_menu) + + +class NewSearchItem(DirectoryItem): + def __init__(self, + context, + name=None, + image=None, + fanart=None, + incognito=False, + channel_id='', + addon_id='', + location=False): + if not name: + name = context.get_ui().bold(context.localize('search.new')) + + if image is None: + image = '{media}/new_search.png' + + params = {} + if addon_id: + params['addon_id'] = addon_id + if incognito: + params['incognito'] = incognito + if channel_id: + params['channel_id'] = channel_id + if location: + params['location'] = location + + super(NewSearchItem, self).__init__(name, + context.create_uri( + (PATHS.SEARCH, 'input',), + params=params, + ), + image=image, + fanart=fanart) + + if context.is_plugin_path(context.get_uri(), (PATHS.SEARCH, 'list',)): + context_menu = [ + menu_items.search_clear(context), + menu_items.separator(), + menu_items.goto_quick_search(context, params, not incognito) + ] + else: + context_menu = [ + menu_items.goto_quick_search(context, params, not incognito) + ] + self.add_context_menu(context_menu) diff --git a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py index 774c1bdeb..89c208cc4 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py @@ -240,7 +240,7 @@ def run(self): self._provider.on_playlist_x( self._provider, self._context, - method='remove', + command='remove', category='video', playlist_id=watch_later_id, video_id=playlist_item_id, @@ -272,7 +272,7 @@ def run(self): self._provider, self._context, rating_match, - method='rate', + command='rate', ) if settings.get_bool('youtube.post.play.refresh', False): diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index fe553017b..535c764d5 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -107,7 +107,9 @@ def onNotification(self, sender, method, data): player = xbmc.Player() try: playing_file = urlsplit(player.getPlayingFile()) - if playing_file.path in {PATHS.MPD, PATHS.REDIRECT}: + if playing_file.path in {PATHS.MPD, + PATHS.PLAY, + PATHS.REDIRECT}: self.onWake() except RuntimeError: pass @@ -129,10 +131,13 @@ def onNotification(self, sender, method, data): if target == PLUGIN_WAKEUP: self.interrupt = True + response = True elif target == SERVER_WAKEUP: if not self.httpd and self.httpd_required(): - self.start_httpd() + response = self.start_httpd() + else: + response = True if self.httpd_sleep_allowed: self.httpd_sleep_allowed = None @@ -142,9 +147,14 @@ def onNotification(self, sender, method, data): self._settings_collect = True elif state == 'process': self.onSettingsChanged(force=True) + response = True + + else: + return if data.get('response_required'): - self.set_property(WAKEUP, target) + data['response'] = response + self.set_property(WAKEUP, json.dumps(data, ensure_ascii=False)) elif event == REFRESH_CONTAINER: self.refresh_container() @@ -245,7 +255,7 @@ def start_httpd(self): port=self._httpd_port, context=context) if not self.httpd: - return + return False self.httpd_thread = threading.Thread(target=self.httpd.serve_forever) self.httpd_thread.start() @@ -254,6 +264,7 @@ def start_httpd(self): context.log_debug('HTTPServer: Listening on |{ip}:{port}|' .format(ip=address[0], port=address[1])) + return True def shutdown_httpd(self): if self.httpd: diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index ecf663906..2a4edf891 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -13,7 +13,7 @@ from traceback import format_stack from ..abstract_plugin import AbstractPlugin -from ...compatibility import xbmcplugin +from ...compatibility import urlsplit, xbmcplugin from ...constants import ( BUSY_FLAG, CONTAINER_FOCUS, @@ -261,6 +261,17 @@ def run(self, provider, context, focused=None): succeeded = bool(result) if not succeeded: ui.clear_property(CONTENT_TYPE) + + try_fallback = options.get(provider.RESULT_TRY_FALLBACK, True) + if try_fallback: + result, post_run_action = self.uri_action( + context, + context.get_parent_uri(params={ + 'window_fallback': True, + 'window_replace': True, + 'window_return': False, + }), + ) cache_to_disc = False update_listing = True @@ -312,7 +323,12 @@ def uri_action(context, uri): elif context.is_plugin_path(uri): context.log_debug('Redirecting to: |{0}|'.format(uri)) - action = 'RunPlugin({0})'.format(uri) + uri = urlsplit(uri) + action = context.create_uri( + (PATHS.ROUTE, uri.path.rstrip('/') or PATHS.HOME), + uri.query, + run=True, + ) result = False else: diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index 86f97b248..21ab91556 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -467,7 +467,7 @@ def run(argv): if args: args = urlsplit(args[0]) - path = args.path + path = args.path.rstrip('/') if path: path = path.split('/') category = path[0] diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py index 2597d5377..71f68dca0 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py @@ -15,7 +15,11 @@ class AbstractContextUI(object): def __init__(self): pass - def create_progress_dialog(self, heading, text=None, background=False): + def create_progress_dialog(self, + heading, + message='', + background=False, + message_template=None): raise NotImplementedError() def on_keyboard_input(self, title, default='', hidden=False): diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py index 83ab3ac57..16f9526f1 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py @@ -14,14 +14,24 @@ class AbstractProgressDialog(object): - def __init__(self, dialog, heading, text, total=100): + def __init__(self, + dialog, + heading, + message='', + total=None, + message_template=None): self._dialog = dialog() - self._dialog.create(heading, text) + self._dialog.create(heading, message) + + self._position = None + self._total = int(total) if total else 100 + + self._message = message + self._message_template = message_template + self._template_params = {} # simple reset because KODI won't do it :( - self._total = int(total) - self._position = 1 - self.update(steps=-1) + self.update(position=0) def __enter__(self): return self @@ -43,20 +53,44 @@ def close(self): def set_total(self, total): self._total = int(total) - def update(self, steps=1, text=None): - self._position += steps + def reset_total(self, new_total, **kwargs): + self._total = int(new_total) + self.update(position=0, **kwargs) - if not self._total: - position = 0 - elif self._position >= self._total: - position = 100 - else: - position = int(100 * self._position / self._total) + def update_total(self, new_total, **kwargs): + self._total = int(new_total) + self.update(steps=0, **kwargs) + + def grow_total(self, new_total, **kwargs): + total = int(new_total) + if total > self._total: + self._total = total + return self._total + + def update(self, steps=1, position=None, message=None, **template_params): + if position is None: + self._position += steps - if isinstance(text, string_type): - self._dialog.update(percent=position, message=text) + if not self._total: + position = 0 + elif self._position >= self._total: + position = 100 + else: + position = int(100 * self._position / self._total) else: - self._dialog.update(percent=position) + self._position = position + + if isinstance(message, string_type): + self._message = message + elif template_params and self._message_template: + self._template_params.update(template_params) + message = self._message_template.format(**self._template_params) + self._message = message + + self._dialog.update( + percent=position, + message=message, + ) def is_aborted(self): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index d3cd82bb5..381b4333c 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -22,11 +22,15 @@ def __init__(self, context): super(XbmcContextUI, self).__init__() self._context = context - def create_progress_dialog(self, heading, text=None, background=False): + def create_progress_dialog(self, + heading, + message='', + background=False, + message_template=None): if background: - return XbmcProgressDialogBG(heading, text) + return XbmcProgressDialogBG(heading, message, message_template) - return XbmcProgressDialog(heading, text) + return XbmcProgressDialog(heading, message, message_template) def on_keyboard_input(self, title, default='', hidden=False): # Starting with Gotham (13.X > ...) diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py index 596ada365..e9e793e67 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_progress_dialog.py @@ -15,22 +15,28 @@ class XbmcProgressDialog(AbstractProgressDialog): - def __init__(self, heading, text): - super(XbmcProgressDialog, self).__init__(xbmcgui.DialogProgress, - heading, - text, - 100) + def __init__(self, heading, message='', message_template=None): + super(XbmcProgressDialog, self).__init__( + dialog=xbmcgui.DialogProgress, + heading=heading, + message=message, + total=100, + message_template=message_template, + ) def is_aborted(self): return self._dialog.iscanceled() class XbmcProgressDialogBG(AbstractProgressDialog): - def __init__(self, heading, text): - super(XbmcProgressDialogBG, self).__init__(xbmcgui.DialogProgressBG, - heading, - text, - 100) + def __init__(self, heading, message='', message_template=None): + super(XbmcProgressDialogBG, self).__init__( + dialog=xbmcgui.DialogProgressBG, + heading=heading, + message=message, + total=100, + message_template=message_template, + ) def is_aborted(self): return False diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 38a66b238..1f535f47e 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -88,6 +88,7 @@ class YouTubeRequestClient(BaseRequestsClass): # Limited subtitle availability 'android_testsuite': { '_id': 30, + '_disabled': True, '_query_subtitles': True, 'json': { 'params': _PLAYER_PARAMS['android_testsuite'], @@ -116,6 +117,7 @@ class YouTubeRequestClient(BaseRequestsClass): # Limited subtitle availability 'android_youtube_tv': { '_id': 29, + '_disabled': True, '_query_subtitles': True, 'json': { 'params': _PLAYER_PARAMS['android'], diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index c2d64151c..6406004c8 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1516,6 +1516,7 @@ def get_my_subscriptions(self, logged_in=False, do_filter=False, refresh=False, + progress_dialog=None, **kwargs): """ modified by PureHemp, using YouTube RSS for fetching latest videos @@ -1622,7 +1623,10 @@ def _get_channels(output, _params=params): 'Accept-Language': 'en-US,en;q=0.7,de;q=0.3' } - def _get_feed_cache(output, channel_id, _cache=cache, _refresh=refresh): + def _get_feed_cache(output, + channel_id, + _cache=cache, + _refresh=refresh): cached = _cache.get_item(channel_id) if cached: feed_details = cached['value'] @@ -1643,6 +1647,7 @@ def _get_feed_cache(output, channel_id, _cache=cache, _refresh=refresh): feeds[channel_id].update(feed_details) else: feeds[channel_id] = feed_details + return True, False def _get_feed(output, channel_id, _headers=headers): @@ -1670,10 +1675,19 @@ def _get_feed(output, channel_id, _headers=headers): } def _parse_feeds(feeds, + sort_method, + sort_limits, + progress_dialog=None, utf8=self._context.get_system_version().compatible(19), filters=subscription_filters, _ns=namespaces, _cache=cache): + if progress_dialog: + total = len(feeds) + progress_dialog.reset_total(new_total=total, + current=0, + total=total) + all_items = {} new_cache = {} for channel_id, feed in feeds.items(): @@ -1713,7 +1727,7 @@ def _parse_feeds(feeds, 'video_ids': set(), } feed_items.sort(reverse=True, - key=partial(_sort_by_date_time, + key=partial(sort_method, limits=feed_limits)) feed_items = feed_items[:min(1000, feed_limits['num'])] new_cache[channel_id] = { @@ -1734,9 +1748,19 @@ def _parse_feeds(feeds, else: all_items[channel_id] = feed_items + if progress_dialog: + progress_dialog.update(current=len(all_items)) + if new_cache: _cache.set_items(new_cache) - return list(chain.from_iterable(all_items.values())) + # filter, sorting by publish date and trim + if all_items: + return sorted( + chain.from_iterable(all_items.values()), + reverse=True, + key=partial(sort_method, limits=sort_limits), + ) + return None def _threaded_fetch(kwargs, output, @@ -1849,6 +1873,15 @@ def _threaded_fetch(kwargs, del payloads[pool_id] completed = [] iterator = iter(payloads) + if progress_dialog: + total = progress_dialog.grow_total( + new_total=len(threaded_output['channel_ids']), + ) + progress_dialog.update( + steps=0, + current=len(threaded_output['feeds']), + total=total, + ) continue payload = payloads[pool_id] @@ -1889,14 +1922,13 @@ def _threaded_fetch(kwargs, counter.acquire(True) new_thread.start() - items = _parse_feeds(threaded_output['feeds']) - - # filter, sorting by publish date and trim - if items: - items.sort(reverse=True, - key=partial(_sort_by_date_time, - limits=totals)) - else: + items = _parse_feeds( + threaded_output['feeds'], + sort_method=_sort_by_date_time, + sort_limits=totals, + progress_dialog=progress_dialog, + ) + if not items: return None if totals['num'] > totals['end']: diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 6bb4be82d..728749bf4 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -716,8 +716,6 @@ def __init__(self, # Limited audio stream availability with some clients 'mpd': ( 'android_vr', - 'android_youtube_tv', - 'android_testsuite', ), # Progressive streams # Limited video and audio stream availability diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index b8c9ba892..1c5e9f628 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -34,7 +34,11 @@ from ...kodion.utils import strip_html_from_text -def _process_list_response(provider, context, json_data, item_filter): +def _process_list_response(provider, + context, + json_data, + item_filter=None, + progress_dialog=None): yt_items = json_data.get('items', []) if not yt_items: context.log_warning('v3 response: Items list is empty') @@ -68,6 +72,12 @@ def _process_list_response(provider, context, json_data, item_filter): fanart_type = False untitled = context.localize('untitled') + if progress_dialog: + total = len(yt_items) + progress_dialog.reset_total(new_total=total, + current=0, + total=total) + for yt_item in yt_items: kind, is_youtube, is_plugin, kind_type = _parse_kind(yt_item) if not (is_youtube or is_plugin) or not kind_type: @@ -298,6 +308,8 @@ def _process_list_response(provider, context, json_data, item_filter): do_callbacks = True items.append(item) + if progress_dialog: + progress_dialog.update(current=len(items)) # this will also update the channel_id_dict with the correct channel_id # for each video. @@ -415,6 +427,12 @@ def _fetch(resource): completed = [] iterator = iter(resources) threads['loop'].set() + + if progress_dialog: + progress_dialog.reset_total(new_total=remaining, + current=0, + total=remaining) + while threads['loop'].wait(): try: resource_id = next(iterator) @@ -433,6 +451,8 @@ def _fetch(resource): if resource['complete']: remaining -= 1 completed.append(resource_id) + if progress_dialog: + progress_dialog.update(current=len(completed)) continue defer = resource['defer'] @@ -480,7 +500,8 @@ def response_to_items(provider, sort=None, reverse=False, process_next_page=True, - item_filter=None): + item_filter=None, + progress_dialog=None): kind, is_youtube, is_plugin, kind_type = _parse_kind(json_data) if not is_youtube and not is_plugin: context.log_debug('v3 response discarded: |%s|' % kind) @@ -494,7 +515,11 @@ def response_to_items(provider, override=params.get('item_filter'), ) result = _process_list_response( - provider, context, json_data, item_filter + provider, + context, + json_data, + item_filter=item_filter, + progress_dialog=progress_dialog, ) if not result: return [] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index 7eaaff141..ef6405208 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -56,18 +56,21 @@ def _do_login(token_type): else: verification_url = 'youtube.com/activate' - text = [localize('sign.go_to') % ui.bold(verification_url), - '[CR]%s %s' % (localize('sign.enter_code'), - ui.bold(user_code))] - text = ''.join(text) + message = ''.join(( + localize('sign.go_to') % ui.bold(verification_url), + '[CR]', + localize('sign.enter_code'), + ' ', + ui.bold(user_code), + )) with ui.create_progress_dialog( - heading=localize('sign.in'), text=text, background=False - ) as dialog: + heading=localize('sign.in'), message=message, background=False + ) as progress_dialog: steps = ((10 * 60) // interval) # 10 Minutes - dialog.set_total(steps) + progress_dialog.set_total(steps) for _ in range(steps): - dialog.update() + progress_dialog.update() try: json_data = _client.request_access_token(token_type, device_code) @@ -102,7 +105,7 @@ def _do_login(token_type): context.log_error('Error requesting access token: |error|' .format(error=message)) - if dialog.is_aborted(): + if progress_dialog.is_aborted(): break context.sleep(interval) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 180f1e00e..bc5e7cd41 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -30,6 +30,7 @@ PLAY_PROMPT_SUBTITLES, PLAY_TIMESHIFT, PLAY_WITH, + SERVER_WAKEUP, ) from ...kodion.items import AudioItem, UriItem, VideoItem from ...kodion.network import get_connect_address @@ -68,6 +69,9 @@ def _play_stream(provider, context): audio_only = settings.audio_only() use_adaptive_formats = (not is_external or settings.alternative_player_adaptive()) + use_mpd = (use_adaptive_formats + and settings.use_mpd_videos() + and context.wakeup(SERVER_WAKEUP, timeout=5)) try: streams = client.get_streams( @@ -75,7 +79,7 @@ def _play_stream(provider, context): video_id=video_id, ask_for_quality=ask_for_quality, audio_only=audio_only, - use_mpd=use_adaptive_formats and settings.use_mpd_videos(), + use_mpd=use_mpd, ) except YouTubeException as exc: msg = ('yt_play.play_video - Error' @@ -176,13 +180,13 @@ def _play_stream(provider, context): def _play_playlist(provider, context): - videos = [] + video_items = [] params = context.get_params() - playlist_player = context.get_playlist_player() - playlist_player.stop() - action = params.get('action') + if not action and context.get_handle() == -1: + action = 'play' + playlist_ids = params.get('playlist_ids') if not playlist_ids: playlist_ids = [params.get('playlist_id')] @@ -192,9 +196,14 @@ def _play_playlist(provider, context): ui = context.get_ui() with ui.create_progress_dialog( - context.localize('playlist.progress.updating'), - context.localize('please_wait'), - background=True + heading=context.localize('playlist.progress.updating'), + message=context.localize('please_wait'), + background=True, + message_template=( + '{wait} {{current}}/{{total}}'.format( + wait=context.localize('please_wait'), + ) + ), ) as progress_dialog: json_data = resource_manager.get_playlist_items(playlist_ids) @@ -202,11 +211,8 @@ def _play_playlist(provider, context): progress_dialog.set_total(total) progress_dialog.update( steps=0, - text='{wait} {current}/{total}'.format( - wait=context.localize('please_wait'), - current=0, - total=total, - ) + current=0, + total=total, ) # start the loop and fill the list with video items @@ -215,76 +221,29 @@ def _play_playlist(provider, context): context, chunk, process_next_page=False) - videos.extend(result) + video_items.extend(result) progress_dialog.update( steps=len(result), - text='{wait} {current}/{total}'.format( - wait=context.localize('please_wait'), - current=len(videos), - total=total, - ) + current=len(video_items), + total=total, ) - if not videos: + if not video_items: return False - # select order - order = params.get('order') - if not order and not video_id: - order = 'ask' - if order == 'ask': - order_list = ('default', 'reverse', 'shuffle') - items = [(context.localize('playlist.play.%s' % order), order) - for order in order_list] - order = ui.on_select(context.localize('playlist.play.select'), - items) - if order not in order_list: - order = 'default' - - # reverse the list - if order == 'reverse': - videos = videos[::-1] - elif order == 'shuffle': - # we have to shuffle the playlist by our self. - # The implementation of XBMC/KODI is quite weak :( - random.shuffle(videos) - - if action == 'list': - context.set_content(CONTENT.VIDEO_CONTENT) - return videos - - # clear the playlist - playlist_player.clear() - playlist_player.unshuffle() - - # check if we have a video as starting point for the playlist - playlist_position = None if video_id else 0 - # add videos to playlist - for idx, video in enumerate(videos): - playlist_player.add(video) - if playlist_position is None and video.video_id == video_id: - playlist_position = idx - - options = { - provider.RESULT_CACHE_TO_DISC: False, - provider.RESULT_FORCE_RESOLVE: True, - provider.RESULT_UPDATE_LISTING: True, - } - - if action == 'queue': - return videos, options - if context.get_handle() == -1 or action == 'play': - playlist_player.play_playlist_item(playlist_position + 1) - return False - return videos[playlist_position], options + return ( + process_items_for_playlist(context, video_items, action, video_id), + { + provider.RESULT_CACHE_TO_DISC: False, + provider.RESULT_FORCE_RESOLVE: True, + provider.RESULT_UPDATE_LISTING: True, + }, + ) def _play_channel_live(provider, context): channel_id = context.get_param('channel_id') - index = context.get_param('live', 1) - 1 - if index < 0: - index = 0 _, json_data = provider.get_client(context).search_with_params(params={ 'type': 'video', 'eventType': 'live', @@ -294,25 +253,26 @@ def _play_channel_live(provider, context): if not json_data: return False - video_items = v3.response_to_items(provider, - context, - json_data, - process_next_page=False) - - try: - video_item = video_items[index] - except IndexError: + channel_streams = v3.response_to_items(provider, + context, + json_data, + process_next_page=False) + if not channel_streams: return False - playlist_player = context.get_playlist_player() - playlist_player.stop() - playlist_player.clear() - playlist_player.add(video_item) - - if context.get_handle() == -1: - playlist_player.play_playlist_item(1) - return False - return video_item + return ( + process_items_for_playlist( + context, + channel_streams, + action='play' if context.get_handle() == -1 else None, + play_from=context.get_param('live', 1), + ), + { + provider.RESULT_CACHE_TO_DISC: False, + provider.RESULT_FORCE_RESOLVE: True, + provider.RESULT_UPDATE_LISTING: True, + }, + ) def process(provider, context, **_kwargs): @@ -401,3 +361,78 @@ def process(provider, context, **_kwargs): if 'channel_id' in params: return _play_channel_live(provider, context) return False + + +def process_items_for_playlist(context, items, action=None, play_from=None): + # select order + order = context.get_param('order') + if not order and play_from is None: + order = 'ask' + if order == 'ask': + order_list = ('default', 'reverse', 'shuffle') + selection_list = [ + (context.localize('playlist.play.%s' % order), order) + for order in order_list + ] + order = context.get_ui().on_select( + context.localize('playlist.play.select'), + selection_list, + ) + if order not in order_list: + order = 'default' + + # reverse the list + if order == 'reverse': + items = items[::-1] + elif order == 'shuffle': + # we have to shuffle the playlist by our self. + # The implementation of XBMC/KODI is quite weak :( + random.shuffle(items) + + if action == 'list': + context.set_content(CONTENT.VIDEO_CONTENT) + return items + + # stop and clear the playlist + playlist_player = context.get_playlist_player() + playlist_player.clear() + playlist_player.unshuffle() + + # check if we have a video as starting point for the playlist + if play_from == 'start': + play_from = 0 + elif play_from == 'end': + play_from = -1 + if isinstance(play_from, int): + playlist_position = play_from + else: + playlist_position = None + + # add videos to playlist + for idx, item in enumerate(items): + if not item.playable: + continue + playlist_player.add(item) + if playlist_position is None and item.video_id == play_from: + playlist_position = idx + + num_items = playlist_player.size() + if not num_items: + return False + + if isinstance(play_from, int): + if num_items >= play_from > 0: + playlist_position = play_from - 1 + elif play_from < 0: + playlist_position = num_items + play_from + else: + playlist_position = 0 + elif playlist_position is None: + playlist_position = 0 + + if action == 'queue': + return items + if action == 'play': + playlist_player.play_playlist_item(playlist_position + 1) + return False + return items[playlist_position] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 3b50fba78..31aebd9ca 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -318,23 +318,23 @@ def _process_rename_playlist(provider, context): return False -def _playlist_id_change(context, playlist, method): +def _playlist_id_change(context, playlist, command): playlist_id = context.get_param('playlist_id', '') if not playlist_id: - raise KodionException('{type}/{method}: missing playlist_id' - .format(type=playlist, method=method)) + raise KodionException('{type}/{command}: missing playlist_id' + .format(type=playlist, command=command)) playlist_name = context.get_param('item_name', '') if not playlist_name: - raise KodionException('{type}/{method}: missing playlist_name' - .format(type=playlist, method=method)) + raise KodionException('{type}/{command}: missing playlist_name' + .format(type=playlist, command=command)) if context.get_ui().on_yes_no_input( context.get_name(), - context.localize('{type}.list.{method}.check'.format( - type=playlist, method=method + context.localize('{type}.list.{command}.check'.format( + type=playlist, command=command )) % playlist_name ): - if method == 'remove': + if command == 'remove': playlist_id = None if playlist == 'watch_later': context.get_access_manager().set_watch_later_id(playlist_id) @@ -347,35 +347,35 @@ def _playlist_id_change(context, playlist, method): def process(provider, context, re_match=None, - method=None, + command=None, category=None, **kwargs): if re_match: - if method is None: - method = re_match.group('method') + if command is None: + command = re_match.group('command') if category is None: category = re_match.group('category') - if method == 'add' and category == 'video': + if command == 'add' and category == 'video': return _process_add_video(provider, context) - if method == 'remove' and category == 'video': + if command == 'remove' and category == 'video': return _process_remove_video(provider, context, **kwargs) - if method == 'remove' and category == 'playlist': + if command == 'remove' and category == 'playlist': return _process_remove_playlist(provider, context) - if method == 'select' and category == 'playlist': + if command == 'select' and category == 'playlist': return _process_select_playlist(provider, context) - if method == 'rename' and category == 'playlist': + if command == 'rename' and category == 'playlist': return _process_rename_playlist(provider, context) - if method in {'set', 'remove'} and category == 'watch_later': - return _playlist_id_change(context, category, method) + if command in {'set', 'remove'} and category == 'watch_later': + return _playlist_id_change(context, category, command) - if method in {'set', 'remove'} and category == 'history': - return _playlist_id_change(context, category, method) + if command in {'set', 'remove'} and category == 'history': + return _playlist_id_change(context, category, command) - raise KodionException('Unknown category |{0}| or method |{1}|' - .format(category, method)) + raise KodionException('Unknown playlist category |{0}| or command |{1}|' + .format(category, command)) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index feabbc150..9c2cad36d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -298,17 +298,33 @@ def _process_saved_playlists_tv(provider, context, client): def _process_my_subscriptions(provider, context, client, filtered=False): context.set_content(CONTENT.VIDEO_CONTENT) - params = context.get_params() - json_data = client.get_my_subscriptions( - page_token=params.get('page', 1), - logged_in=provider.is_logged_in(), - do_filter=filtered, - refresh=params.get('refresh'), - ) + with context.get_ui().create_progress_dialog( + heading=context.localize('my_subscriptions.loading'), + message=context.localize('please_wait'), + background=True, + message_template=( + '{wait} {{current}}/{{total}}'.format( + wait=context.localize('please_wait'), + ) + ), + ) as progress_dialog: + params = context.get_params() + json_data = client.get_my_subscriptions( + page_token=params.get('page', 1), + logged_in=provider.is_logged_in(), + do_filter=filtered, + refresh=params.get('refresh'), + progress_dialog=progress_dialog, + ) - if not json_data: - return False - return v3.response_to_items(provider, context, json_data) + if not json_data: + return False + return v3.response_to_items( + provider, + context, + json_data, + progress_dialog=progress_dialog, + ) def process(provider, context, re_match): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index 3087f03e0..9bcd25f14 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -82,20 +82,20 @@ def _process_remove(_provider, context, client): def process(provider, context, re_match): - method = re_match.group('method') + command = re_match.group('command') # we need a login client = provider.get_client(context) if not provider.is_logged_in(): return UriItem(context.create_uri(('sign', 'in'))) - if method == 'list': + if command == 'list': return _process_list(provider, context, client) - if method == 'add': + if command == 'add': return _process_add(provider, context, client) - if method == 'remove': + if command == 'remove': return _process_remove(provider, context, client) - raise KodionException('Unknown subscriptions method: %s' % method) + raise KodionException('Unknown subscriptions command: %s' % command) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index 88e86cbd5..294a51a9a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -116,14 +116,14 @@ def _process_more_for_video(context): context.execute(result) -def process(provider, context, re_match=None, method=None): - if re_match and method is None: - method = re_match.group('method') +def process(provider, context, re_match=None, command=None): + if re_match and command is None: + command = re_match.group('command') - if method == 'rate': + if command == 'rate': return _process_rate_video(provider, context, re_match) - if method == 'more': + if command == 'more': return _process_more_for_video(context) - raise KodionException('Unknown method: %s' % method) + raise KodionException('Unknown video command: %s' % command) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 599c61f71..3654e034a 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -60,12 +60,12 @@ def __init__(self): self._logged_in = False self.on_video_x = self.register_path( - '^/video/(?P[^/]+)/?$', + '^/video/(?P[^/]+)/?$', yt_video.process, ) self.on_playlist_x = self.register_path( - '^/playlist/(?P[^/]+)/(?P[^/]+)/?$', + '^/playlist/(?P[^/]+)/(?P[^/]+)/?$', yt_playlist.process, ) @@ -80,7 +80,7 @@ def __init__(self): ) self.register_path( - '^/subscriptions/(?P[^/]+)/?$', + '^/subscriptions/(?P[^/]+)/?$', yt_subscriptions.process, ) @@ -517,7 +517,7 @@ def on_channel_live(provider, context, re_match): return result @AbstractProvider.register_path( - r'^/(?P(channel|handle|user))' + r'^/(?P(channel|handle|user))' r'/(?P[^/]+)/?$') @staticmethod def on_channel(provider, context, re_match): @@ -538,10 +538,10 @@ def on_channel(provider, context, re_match): params = context.get_params() ui = context.get_ui() - method = re_match.group('method') + command = re_match.group('command') identifier = re_match.group('identifier') - if (method == 'channel' + if (command == 'channel' and identifier and identifier.lower() == 'property' and listitem_channel_id @@ -550,7 +550,7 @@ def on_channel(provider, context, re_match): channel=create_uri(('channel', listitem_channel_id)) )) - if method == 'channel' and not identifier: + if command == 'channel' and not identifier: return False context.set_content(CONTENT.VIDEO_CONTENT) @@ -564,14 +564,14 @@ def on_channel(provider, context, re_match): only have the handle or username of a channel. """ if identifier == 'mine': - method = 'mine' + command = 'mine' elif identifier.startswith('@'): - method = 'handle' - if method == 'channel': + command = 'handle' + if command == 'channel': channel_id = identifier else: channel_id = None - identifier = {method: True, 'identifier': identifier} + identifier = {command: True, 'identifier': identifier} if not channel_id: context.log_debug('Trying to get channel ID for |{0}|'.format( @@ -942,16 +942,18 @@ def on_configure_addon(provider, context, re_match): addon=ADDON_ID, action=action )) - @AbstractProvider.register_path('^/my_subscriptions/filter/?$') + @AbstractProvider.register_path( + '^/my_subscriptions/filter' + '/(?Padd|remove)/?$' + ) @staticmethod - def on_manage_my_subscription_filter(context, **_kwargs): + def on_manage_my_subscription_filter(context, re_match): settings = context.get_settings() ui = context.get_ui() - params = context.get_params() - action = params.get('action') - channel = params.get('item_name') - if not channel or not action: + channel = context.get_param('item_name') + command = re_match.group('command') + if not channel or not command: return filter_enabled = settings.get_bool('youtube.folder.my_subscriptions_filtered.show', False) @@ -966,19 +968,19 @@ def on_manage_my_subscription_filter(context, **_kwargs): filter_list = filter_string.split(',') filter_list = [x.lower() for x in filter_list] - if action == 'add': + if command == 'add': if channel_name not in filter_list: filter_list.append(channel_name) - elif action == 'remove' and channel_name in filter_list: + elif command == 'remove' and channel_name in filter_list: filter_list = [chan_name for chan_name in filter_list if chan_name != channel_name] modified_string = ','.join(filter_list).lstrip(',') if filter_string != modified_string: settings.set_string('youtube.filter.my_subscriptions_filtered.list', modified_string) message = '' - if action == 'add': + if command == 'add': message = context.localize('my_subscriptions.filter.added') - elif action == 'remove': + elif command == 'remove': message = context.localize('my_subscriptions.filter.removed') if message: ui.show_notification(message=message) @@ -1083,15 +1085,15 @@ def on_api_key_update(context, **_kwargs): @staticmethod def on_playback_history(provider, context, re_match): params = context.get_params() - action = params.get('action') - if not action: + command = re_match.group('command') + if not command: return False localize = context.localize playback_history = context.get_playback_history() ui = context.get_ui() - if action == 'list': + if command in {'list', 'play'}: context.set_content(CONTENT.VIDEO_CONTENT, sub_type='history') items = playback_history.get_items() if not items: @@ -1120,9 +1122,16 @@ def on_playback_history(provider, context, re_match): ] } video_items = v3.response_to_items(provider, context, v3_response) + if command == 'play': + return yt_play.process_items_for_playlist( + context, + video_items, + action='play', + play_from='start', + ) return video_items - if action == 'clear': + if command == 'clear': if not ui.on_yes_no_input( localize('history.clear'), localize('history.clear.check') @@ -1143,7 +1152,7 @@ def on_playback_history(provider, context, re_match): if not video_id: return False - if action == 'remove': + if command == 'remove': video_name = params.get('item_name') or video_id video_name = to_unicode(video_name) if not ui.on_yes_no_input( @@ -1174,17 +1183,17 @@ def on_playback_history(provider, context, re_match): 'played_percent': 0 } - if action == 'mark_unwatched': + if command == 'mark_unwatched': if play_data.get('play_count', 0) > 0: play_data['play_count'] = 0 play_data['played_time'] = 0 play_data['played_percent'] = 0 - elif action == 'mark_watched': + elif command == 'mark_watched': if not play_data.get('play_count', 0): play_data['play_count'] = 1 - elif action == 'reset_resume': + elif command == 'reset_resume': play_data['played_time'] = 0 play_data['played_percent'] = 0 @@ -1205,6 +1214,7 @@ def on_root(provider, context, re_match): # _.get_my_playlists() # context.set_content(CONTENT.LIST_CONTENT) + context.set_param('category_label', localize('youtube')) result = [] @@ -1337,12 +1347,26 @@ def on_root(provider, context, re_match): watch_later_item.add_context_menu(context_menu) result.append(watch_later_item) else: - watch_history_item = DirectoryItem( + watch_later_item = DirectoryItem( localize('watch_later'), create_uri((PATHS.WATCH_LATER, 'list')), image='{media}/watch_later.png', ) - result.append(watch_history_item) + context_menu = [ + menu_items.watch_later_local_clear(context), + menu_items.separator(), + menu_items.play_all_from( + context, + route=PATHS.WATCH_LATER, + ), + menu_items.play_all_from( + context, + route=PATHS.WATCH_LATER, + order='shuffle', + ), + ] + watch_later_item.add_context_menu(context_menu) + result.append(watch_later_item) # liked videos if logged_in and settings_bool('youtube.folder.liked_videos.show', True): @@ -1402,9 +1426,25 @@ def on_root(provider, context, re_match): elif local_history: watch_history_item = DirectoryItem( localize('history'), - create_uri((PATHS.HISTORY,), params={'action': 'list'}), + create_uri((PATHS.HISTORY, 'list')), image='{media}/history.png', ) + context_menu = [ + menu_items.history_clear( + context + ), + menu_items.separator(), + menu_items.play_all_from( + context, + route=PATHS.HISTORY, + ), + menu_items.play_all_from( + context, + route=PATHS.HISTORY, + order='shuffle', + ), + ] + watch_history_item.add_context_menu(context_menu) result.append(watch_history_item) # (my) playlists @@ -1442,6 +1482,22 @@ def on_root(provider, context, re_match): create_uri((PATHS.BOOKMARKS, 'list')), image='{media}/bookmarks.png', ) + context_menu = [ + menu_items.bookmarks_clear( + context + ), + menu_items.separator(), + menu_items.play_all_from( + context, + route=PATHS.BOOKMARKS, + ), + menu_items.play_all_from( + context, + route=PATHS.BOOKMARKS, + order='shuffle', + ), + ] + bookmarks_item.add_context_menu(context_menu) result.append(bookmarks_item) # browse channels @@ -1527,7 +1583,7 @@ def on_bookmarks(provider, context, re_match): if not command: return False - if command == 'list': + if command in {'list', 'play'}: context.set_content(CONTENT.VIDEO_CONTENT) bookmarks_list = context.get_bookmarks_list() items = bookmarks_list.get_items() @@ -1635,6 +1691,13 @@ def _update(new_item): v3_response['items'].append(item) bookmarks = v3.response_to_items(provider, context, v3_response) + if command == 'play': + return yt_play.process_items_for_playlist( + context, + bookmarks, + action='play', + play_from='start', + ) return bookmarks ui = context.get_ui() @@ -1703,7 +1766,7 @@ def on_watch_later(provider, context, re_match): localize = context.localize ui = context.get_ui() - if command == 'list': + if command in {'list', 'play'}: context.set_content(CONTENT.VIDEO_CONTENT, sub_type='watch_later') items = context.get_watch_later_list().get_items() if not items: @@ -1732,6 +1795,13 @@ def on_watch_later(provider, context, re_match): ] } video_items = v3.response_to_items(provider, context, v3_response) + if command == 'play': + return yt_play.process_items_for_playlist( + context, + video_items, + action='play', + play_from='start', + ) return video_items if command == 'clear':