diff --git a/addon.xml b/addon.xml index e5a77d0e4..0fdfbc8c3 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 33e03c8e8..5cf89fc29 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,33 @@ +## v7.1.0+beta.2 +### Fixed +- Fix possible regression causing 6s delay on first play +- Fix regression in building client details causing wrong referer to be used +- Only reroute to new window if container was not filled by the plugin #896 +- Only reroute to new window if modal dialog is not open #896 + +### Changed +- Use a CommandItem that opens the Info dialog for comments to avoid log spam +- Revert old workaround for Kodi treating a non-folder listitem as playable +- Improve parsing of plugin url query parameters to allow empty values + +### New +- Add items_per_page query parameter to allow number of items in widgets to be customised #896 +- Add item_filter query parameter to override "Hide videos from listings" setting #896 + - Comma seperated string of item types to be filtered out of listing + - "?item_filter=shorts" will remove shorts from listing + - "?item_filter=shorts,live" will remove shorts and live streams from listing + - "?item_filter" will show all item types + - Allowable item types: + - shorts + - upcoming + - upcoming_live + - live + - premieres + - completed + - vod + ## v7.1.0+beta.1 -### +### Fixed - Fix logging/retry of sqlite3.OperationalError - Fix trying to use ISA for progressive live streams - Retain list position when refreshing listings diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index e9789d074..5998d686d 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -218,7 +218,14 @@ def on_goto_page(provider, context, re_match): else: page_token = '' params = dict(params, page=page, page_token=page_token) - return provider.reroute(context=context, path=path, params=params) + + if (not context.get_infobool('System.HasActiveModalDialog') + and context.is_plugin_path( + context.get_infolabel('Container.FolderPath'), + partial=True, + )): + return provider.reroute(context=context, path=path, params=params) + return provider.navigate(context.clone(path, params)) @staticmethod def on_reroute(provider, context, re_match): diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 755501e8e..3473e2468 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -65,6 +65,7 @@ class AbstractContext(object): } _INT_PARAMS = { 'fanart_type', + 'items_per_page', 'live', 'next_page_token', 'offset', @@ -81,6 +82,7 @@ class AbstractContext(object): } _LIST_PARAMS = { 'channel_ids', + 'item_filter', 'playlist_ids', } _STRING_PARAMS = { @@ -305,7 +307,10 @@ def get_param(self, name, default=None): def parse_uri(self, uri): uri = urlsplit(uri) path = uri.path - params = self.parse_params(dict(parse_qsl(uri.query)), update=False) + params = self.parse_params( + dict(parse_qsl(uri.query, keep_blank_values=True)), + update=False, + ) return path, params def parse_params(self, params, update=True): @@ -327,9 +332,11 @@ def parse_params(self, params, update=True): elif param in self._FLOAT_PARAMS: parsed_value = float(value) elif param in self._LIST_PARAMS: - parsed_value = [ - val for val in value.split(',') if val - ] + parsed_value = ( + list(value) + if isinstance(value, (list, tuple)) else + [val for val in value.split(',') if val] + ) elif param in self._STRING_PARAMS: parsed_value = to_str(value) if param in self._STRING_BOOL_PARAMS: 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 b05a3ed1b..ece6ea1b6 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -346,7 +346,9 @@ def init(self): if num_args > 2: params = sys.argv[2][1:] if params: - self.parse_params(dict(parse_qsl(params))) + self.parse_params( + dict(parse_qsl(params, keep_blank_values=True)) + ) # then Kodi resume status if num_args > 3 and sys.argv[3].lower() == 'resume:true': @@ -425,9 +427,7 @@ def get_subtitle_language(self): def get_playlist_player(self, playlist_type=None): if not self._playlist or playlist_type: - self._playlist = XbmcPlaylistPlayer(playlist_type, - proxy(self), - retry=3) + self._playlist = XbmcPlaylistPlayer(proxy(self), playlist_type) return self._playlist def get_ui(self): diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index f7396f929..2c9c6aa26 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -568,15 +568,17 @@ def directory_listitem(context, directory_item, show_fanart=None, **_kwargs): set_info(list_item, directory_item, props) """ - # ListItems that do not open a lower level list should have the isFolder - # parameter of the xbmcplugin.addDirectoryItem set to False, however this - # now appears to mark the ListItem as playable, even if the IsPlayable - # property is not set or set to "false". - # Set isFolder to True as a workaround, regardless of whether the ListItem - # is actually a folder. - is_folder = not directory_item.is_action() + ListItems that do not open a lower level list should have the isFolder + parameter of the xbmcplugin.addDirectoryItem set to False, however this + now appears to mark the ListItem as playable, even if the IsPlayable + property is not set or set to "false". + Set isFolder to True as a workaround, regardless of whether the ListItem + is actually a folder. """ - is_folder = True + # Workaround: + # is_folder = True + # Test correctly setting isFolder: + is_folder = not directory_item.is_action() context_menu = directory_item.get_context_menu() if context_menu is not None: diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py index 16f54ba6e..05383c1b8 100644 --- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist_player.py @@ -31,18 +31,26 @@ class XbmcPlaylistPlayer(AbstractPlaylistPlayer): 'audio': xbmc.PLAYLIST_MUSIC, # 0 } - def __init__(self, playlist_type, context, retry=0): + def __init__(self, context, playlist_type=None, retry=None): super(XbmcPlaylistPlayer, self).__init__() self._context = context - playlist_id = self._PLAYER_PLAYLIST.get(playlist_type) - if not playlist_type: + player = xbmc.Player() + if retry is None: + retry = 3 if player.isPlaying() else 0 + + if playlist_type is None: playlist_id = self.get_playlist_id(retry=retry) + else: + playlist_id = ( + self._PLAYER_PLAYLIST.get(playlist_type) + or self._PLAYER_PLAYLIST['video'] + ) self.set_playlist_id(playlist_id) self._playlist = xbmc.PlayList(playlist_id) - self._player = xbmc.Player() + self._player = player def clear(self): self._playlist.clear() diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 928f09e0e..56c94b8fb 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -412,8 +412,13 @@ def stream_select(self, value=None): 'vod': True, } - def item_filter(self, update=None): - types = dict.fromkeys(self.get_string_list(SETTINGS.HIDE_VIDEOS), False) + def item_filter(self, update=None, override=None): + types = dict.fromkeys( + self.get_string_list(SETTINGS.HIDE_VIDEOS) + if override is None else + override, + False + ) types = dict(self._DEFAULT_FILTER, **types) if update: if 'live_folder' in update: diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 506241b8b..133d141d4 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -352,6 +352,10 @@ def build_client(cls, client_name=None, data=None): client = merge_dicts(cls.CLIENTS['_common'], client, templates) client['_name'] = client_name + for values, template_id, template in templates.values(): + if template_id in values: + values[template_id] = template.format(**client) + try: params = client['params'] if client.get('_access_token'): @@ -373,8 +377,4 @@ def build_client(cls, client_name=None, data=None): except KeyError: pass - for values, template_id, template in templates.values(): - if template_id in values: - values[template_id] = template.format(**client) - return client diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index a004e15b4..98a4a9154 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -127,8 +127,8 @@ def __init__(self, context, **kwargs): super(YouTube, self).__init__(context=context, **kwargs) - def get_max_results(self): - return self._max_results + def max_results(self): + return self._context.get_param('items_per_page') or self._max_results def get_language(self): return self._language @@ -348,7 +348,7 @@ def get_subscription(self, :return: """ params = {'part': 'snippet', - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'order': order} if channel_id == 'mine': params['mine'] = 'true' @@ -364,7 +364,7 @@ def get_subscription(self, def get_guide_category(self, guide_category_id, page_token='', **kwargs): params = {'part': 'snippet,contentDetails,brandingSettings', - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'categoryId': guide_category_id, 'regionCode': self._region, 'hl': self._language} @@ -377,7 +377,7 @@ def get_guide_category(self, guide_category_id, page_token='', **kwargs): def get_guide_categories(self, page_token='', **kwargs): params = {'part': 'snippet', - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'regionCode': self._region, 'hl': self._language} if page_token: @@ -390,7 +390,7 @@ def get_guide_categories(self, page_token='', **kwargs): def get_trending_videos(self, page_token='', **kwargs): params = {'part': 'snippet,status', - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'regionCode': self._region, 'hl': self._language, 'chart': 'mostPopular'} @@ -403,7 +403,7 @@ def get_trending_videos(self, page_token='', **kwargs): def get_video_category(self, video_category_id, page_token='', **kwargs): params = {'part': 'snippet,contentDetails,status', - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'videoCategoryId': video_category_id, 'chart': 'mostPopular', 'regionCode': self._region, @@ -417,7 +417,7 @@ def get_video_category(self, video_category_id, page_token='', **kwargs): def get_video_categories(self, page_token='', **kwargs): params = {'part': 'snippet', - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'regionCode': self._region, 'hl': self._language} if page_token: @@ -604,7 +604,7 @@ def get_related_for_home(self, page_token='', refresh=False): # Increase value to recursively retrieve recommendations for the first # recommended video, up to the set maximum recursion depth max_depth = 2 - items_per_page = self._max_results + items_per_page = self.max_results() diversity_limits = items_per_page // (num_items * max_depth) items = [[] for _ in range(max_depth * len(video_ids))] counts = { @@ -811,7 +811,7 @@ def rank_and_sort(item): def get_activities(self, channel_id, page_token='', **kwargs): params = {'part': 'snippet,contentDetails', - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'regionCode': self._region, 'hl': self._language} @@ -844,7 +844,7 @@ def get_channel_sections(self, channel_id, **kwargs): def get_playlists_of_channel(self, channel_id, page_token='', **kwargs): params = {'part': 'snippet', - 'maxResults': str(self._max_results)} + 'maxResults': str(self.max_results())} if channel_id != 'mine': params['channelId'] = channel_id else: @@ -864,7 +864,7 @@ def get_playlist_item_id_of_video_id(self, json_data = self.get_playlist_items( playlist_id=playlist_id, page_token=page_token, - max_results=50, + max_results=self.max_results(), ) if not json_data: return None @@ -890,7 +890,7 @@ def get_playlist_items(self, **kwargs): # prepare params if max_results is None: - max_results = self._max_results + max_results = self.max_results() params = {'part': 'snippet', 'maxResults': str(max_results), 'playlistId': playlist_id} @@ -954,7 +954,7 @@ def get_disliked_videos(self, page_token='', **kwargs): # prepare params params = {'part': 'snippet,status', 'myRating': 'dislike', - 'maxResults': str(self._max_results)} + 'maxResults': str(self.max_results())} if page_token: params['pageToken'] = page_token @@ -1022,7 +1022,7 @@ def get_live_events(self, 'regionCode': self._region, 'hl': self._language, 'relevanceLanguage': self._language, - 'maxResults': str(self._max_results)} + 'maxResults': str(self.max_results())} if location: settings = self._context.get_settings() @@ -1049,7 +1049,7 @@ def get_related_videos(self, offset=0, retry=0, **kwargs): - max_results = self._max_results if max_results <= 0 else max_results + max_results = self.max_results() if max_results <= 0 else max_results post_data = {'videoId': video_id} if page_token: @@ -1248,7 +1248,7 @@ def get_parent_comments(self, page_token='', max_results=0, **kwargs): - max_results = self._max_results if max_results <= 0 else max_results + max_results = self.max_results() if max_results <= 0 else max_results # prepare params params = {'part': 'snippet', @@ -1270,7 +1270,7 @@ def get_child_comments(self, page_token='', max_results=0, **kwargs): - max_results = self._max_results if max_results <= 0 else max_results + max_results = self.max_results() if max_results <= 0 else max_results # prepare params params = {'part': 'snippet', @@ -1293,7 +1293,7 @@ def get_channel_videos(self, channel_id, page_token='', **kwargs): params = {'part': 'snippet', 'hl': self._language, - 'maxResults': str(self._max_results), + 'maxResults': str(self.max_results()), 'type': 'video', 'safeSearch': 'none', 'order': 'date'} @@ -1355,7 +1355,7 @@ def search(self, 'regionCode': self._region, 'hl': self._language, 'relevanceLanguage': self._language, - 'maxResults': str(self._max_results)} + 'maxResults': str(self.max_results())} if event_type and event_type in {'live', 'upcoming', 'completed'}: params['eventType'] = event_type @@ -1405,7 +1405,7 @@ def get_my_subscriptions(self, 'items': [], 'pageInfo': { 'totalResults': 0, - 'resultsPerPage': self._max_results, + 'resultsPerPage': self.max_results(), }, } @@ -1430,8 +1430,8 @@ def get_my_subscriptions(self, page = page_token or 1 totals = { 'num': 0, - 'start': -self._max_results, - 'end': page * self._max_results, + 'start': -self.max_results(), + 'end': page * self.max_results(), 'video_ids': set(), } totals['start'] += totals['end'] @@ -1837,7 +1837,7 @@ def _perform(_playlist_idx, _page_token, _offset, _result): if not _result: _result = {'items': []} - _new_offset = self._max_results - len(_result['items']) + _offset + _new_offset = self.max_results() - len(_result['items']) + _offset if _offset > 0: _items = _items[_offset:] _result['offset'] = _new_offset @@ -1867,23 +1867,23 @@ def _perform(_playlist_idx, _page_token, _offset, _result): _continuations = (_data.get('continuations', [{}])[0] .get('nextContinuationData', {}) .get('continuation', '')) - if _continuations and len(_result['items']) <= self._max_results: + if _continuations and len(_result['items']) <= self.max_results(): _result['next_page_token'] = _continuations - if len(_result['items']) < self._max_results: + if len(_result['items']) < self.max_results(): _result = _perform(_playlist_idx=playlist_index, _page_token=_continuations, _offset=0, _result=_result) # trim result - if len(_result['items']) > self._max_results: + if len(_result['items']) > self.max_results(): _items = _result['items'] - _items = _items[:self._max_results] + _items = _items[:self.max_results()] _result['items'] = _items _result['continue'] = True - if len(_result['items']) < self._max_results: + if len(_result['items']) < self.max_results(): if 'continue' in _result: del _result['continue'] diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 96878d12e..f8b9fdb20 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -15,7 +15,7 @@ from math import log10 from ...kodion.constants import CONTENT, LICENSE_TOKEN, LICENSE_URL, PATHS -from ...kodion.items import AudioItem, DirectoryItem, menu_items +from ...kodion.items import AudioItem, DirectoryItem, CommandItem, menu_items from ...kodion.utils import ( datetime_parser, friendly_number, @@ -118,7 +118,10 @@ def make_comment_item(context, snippet, uri, total_replies=0): ui.new_line(body, cr_before=2), )) - comment_item = DirectoryItem(label, uri, plot=plot, action=(not uri)) + if uri: + comment_item = DirectoryItem(label, uri, plot=plot) + else: + comment_item = CommandItem(label, 'Action(Info)', context, plot=plot) datetime = datetime_parser.parse(published_at) comment_item.set_added_utc(datetime) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 9313de34f..d80a1365d 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -451,8 +451,13 @@ def response_to_items(provider, context.log_debug('v3 response discarded: |%s|' % kind) return [] + params = context.get_params() + if kind_type in _KNOWN_RESPONSE_KINDS: - item_filter = context.get_settings().item_filter(item_filter) + item_filter = context.get_settings().item_filter( + update=item_filter, + override=params.get('item_filter'), + ) result = _process_list_response( provider, context, json_data, item_filter ) @@ -477,7 +482,6 @@ def response_to_items(provider, We implemented our own calculation for the token into the YouTube client This should work for up to ~2000 entries. """ - params = context.get_params() current_page = params.get('page') next_page = current_page + 1 if current_page else 2 new_params = dict(params, page=next_page)