diff --git a/addon.xml b/addon.xml index 29f3ec62d..6eb7a9b81 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 19626a5da..ae9fa26df 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,19 @@ +## v7.0.3+beta.3 +### Fixed +Fix invalid error when removing subscriptions #568 +Fix removing item from playlist #570 +Fix related videos and respect pagination limits #572 +Fix not correctly including visitor data in continuation requests +Fix for possible database locks during setup +Fix incorrect timezone details for premiered time #574 + +### Changed +Don't show subscribe context menu item in My Subscriptions #568 + +### New +Add ability to unsubscribe from My Subscriptions #240, #568 +Make MPEG-DASH frame rate details configurable #336 + ## v7.0.3+beta.2 ### Changed - Function and data cache are now created per user diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index ecf6f327a..e6835655f 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -1384,3 +1384,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Disable fractional framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Disable all framerate hinting" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 9c994c905..9a3788533 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1384,3 +1384,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Disable fractional framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Disable all framerate hinting" +msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index 872d3f7fe..9bbf340c8 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -1380,3 +1380,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Disable fractional framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Disable all framerate hinting" +msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index e60a50d0a..77d46b799 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -1385,3 +1385,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Disable fractional framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Disable all framerate hinting" +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index c7f85f70b..1998c20b6 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -38,25 +38,25 @@ def __init__(self): self.register_path(r'^/$', '_internal_root') self.register_path(r''.join(( - '^/', + '^', paths.WATCH_LATER, '/(?Padd|clear|list|remove)/?$' )), '_internal_watch_later') self.register_path(r''.join(( - '^/', + '^', paths.FAVORITES, '/(?Padd|clear|list|remove)/?$' )), '_internal_favorite') self.register_path(r''.join(( - '^/', + '^', paths.SEARCH, '/(?Pinput|query|list|remove|clear|rename)/?$' )), '_internal_search') self.register_path(r''.join(( - '^/', + '^', paths.HISTORY, '/$' )), 'on_playback_history') @@ -320,7 +320,7 @@ def _internal_search(self, context, re_match): search_history.update(query) except: pass - context.set_path('/kodion/search/query/') + context.set_path(paths.SEARCH, 'query') if isinstance(query, bytes): query = query.decode('utf-8') return self.on_search(query, context, re_match) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index cd14cf4bb..2fd760fb8 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -11,10 +11,16 @@ from __future__ import absolute_import, division, unicode_literals -SEARCH = 'kodion/search' -FAVORITES = 'kodion/favorites' -WATCH_LATER = 'kodion/watch_later' -HISTORY = 'kodion/playback_history' +SEARCH = '/kodion/search' +FAVORITES = '/kodion/favorites' +WATCH_LATER = '/kodion/watch_later' +HISTORY = '/kodion/playback_history' + +DISLIKED_VIDEOS = '/special/disliked_videos' +LIKED_VIDEOS = '/channel/mine/playlist/LL' +MY_PLAYLISTS = '/channel/mine/playlists' +MY_SUBSCRIPTIONS = '/special/new_uploaded_videos' +SUBSCRIPTIONS = '/subscriptions/list' API = '/youtube/api' API_SUBMIT = '/youtube/api/submit' diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 540a09839..26f8187a5 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -239,8 +239,8 @@ def create_uri(self, path='/', params=None): def get_path(self): return self._path - def set_path(self, value): - self._path = value + def set_path(self, *path): + self._path = create_path(*path) def get_params(self): return self._params @@ -370,3 +370,7 @@ def sleep(milli_seconds): @staticmethod def get_infolabel(name): raise NotImplementedError() + + @staticmethod + def get_listitem_detail(detail_name, attr=False): + raise NotImplementedError() 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 271709431..0e51d6a02 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -327,24 +327,10 @@ def format_time(time_obj, str_format=None): return time_obj.strftime(str_format) def get_language(self): - """ - The xbmc.getLanguage() method is fucked up!!! We always return 'en-US' for now - """ - - """ - if self.get_system_version().get_release_name() == 'Frodo': - return 'en-US' - - try: - language = xbmc.getLanguage(0, region=True) - language = language.split('-') - language = '%s-%s' % (language[0].lower(), language[1].upper()) - return language - except Exception as exc: - self.log_error('Failed to get system language (%s)', exc.__str__()) - return 'en-US' - """ - + kodi_language = xbmc.getLanguage(format=xbmc.ISO_639_1, region=True) + lang_code, seperator, region = kodi_language.partition('-') + if region: + return seperator.join((lang_code.lower(), region.upper())) return 'en-US' def get_language_name(self, lang_id=None): @@ -606,3 +592,11 @@ def abort_requested(self): @staticmethod def get_infolabel(name): return xbmc.getInfoLabel(name) + + @staticmethod + def get_listitem_detail(detail_name, attr=False): + return xbmc.getInfoLabel( + 'Container.ListItem(0).{0}'.format(detail_name) + if attr else + 'Container.ListItem(0).Property({0})'.format(detail_name) + ) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index fd707a9b3..a6238b1fe 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -312,18 +312,10 @@ def go_to_channel(context, channel_id, channel_name): def subscribe_to_channel(context, channel_id, channel_name=''): - if not channel_name: - return ( - context.localize('subscribe'), - 'RunPlugin({0})'.format(context.create_uri( - ('subscriptions', 'add',), - { - 'subscription_id': channel_id, - }, - )) - ) return ( - context.localize('subscribe_to') % context.get_ui().bold(channel_name), + context.localize('subscribe_to') % context.get_ui().bold(channel_name) + if channel_name else + context.localize('subscribe'), 'RunPlugin({0})'.format(context.create_uri( ('subscriptions', 'add',), { @@ -333,13 +325,19 @@ def subscribe_to_channel(context, channel_id, channel_name=''): ) -def unsubscribe_from_channel(context, channel_id): +def unsubscribe_from_channel(context, channel_id=None, subscription_id=None): return ( context.localize('unsubscribe'), 'RunPlugin({0})'.format(context.create_uri( ('subscriptions', 'remove',), { - 'subscription_id': channel_id, + 'subscription_id': subscription_id, + }, + )) if subscription_id else + 'RunPlugin({0})'.format(context.create_uri( + ('subscriptions', 'remove',), + { + 'channel_id': channel_id, }, )) ) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 2582a9bb3..25082c10a 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -292,7 +292,7 @@ def get_mpd_video_qualities(self): if selected >= key] def stream_features(self): - return self.get_string_list(settings.MPD_STREAM_FEATURES) + return frozenset(self.get_string_list(settings.MPD_STREAM_FEATURES)) _STREAM_SELECT = { 1: 'auto', diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index ce76b3bcb..d990b5753 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -173,15 +173,21 @@ def _open(self): self.__class__._table_created = False self.__class__._table_updated = True - try: - db = sqlite3.connect(self._filepath, - check_same_thread=False, - timeout=1, - isolation_level=None) - except sqlite3.OperationalError as exc: - log_error('SQLStorage._execute - {exc}:\n{details}'.format( - exc=exc, details=''.join(format_stack()) - )) + for _ in range(3): + try: + db = sqlite3.connect(self._filepath, + check_same_thread=False, + timeout=1, + isolation_level=None) + break + except (sqlite3.Error, sqlite3.OperationalError) as exc: + log_error('SQLStorage._open - {exc}:\n{details}'.format( + exc=exc, details=''.join(format_stack()) + )) + if isinstance(exc, sqlite3.Error): + return False + time.sleep(0.1) + else: return False cursor = db.cursor() @@ -207,7 +213,7 @@ def _open(self): ) if not self._table_updated: - for result in cursor.execute(self._sql['has_old_table']): + for result in self._execute(cursor, self._sql['has_old_table']): if result[0] == 1: statements.extend(( 'PRAGMA writable_schema = 1;', @@ -220,7 +226,7 @@ def _open(self): transaction_begin = len(sql_script) + 1 sql_script.extend(('BEGIN;', 'COMMIT;', 'VACUUM;')) sql_script[transaction_begin:transaction_begin] = statements - cursor.executescript('\n'.join(sql_script)) + self._execute(cursor, '\n'.join(sql_script), script=True) self.__class__._table_created = True self.__class__._table_updated = True @@ -239,7 +245,7 @@ def _close(self): self._db = None @staticmethod - def _execute(cursor, query, values=None, many=False): + def _execute(cursor, query, values=None, many=False, script=False): if values is None: values = () """ @@ -251,17 +257,16 @@ def _execute(cursor, query, values=None, many=False): try: if many: return cursor.executemany(query, values) + if script: + return cursor.executescript(query) return cursor.execute(query, values) - except sqlite3.OperationalError as exc: + except (sqlite3.Error, sqlite3.OperationalError) as exc: log_error('SQLStorage._execute - {exc}:\n{details}'.format( exc=exc, details=''.join(format_stack()) )) + if isinstance(exc, sqlite3.Error): + return [] time.sleep(0.1) - except sqlite3.Error as exc: - log_error('SQLStorage._execute - {exc}:\n{details}'.format( - exc=exc, details=''.join(format_stack()) - )) - return [] return [] def _optimize_file_size(self, defer=False): 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 73e8da332..e9a2fa780 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py @@ -33,7 +33,7 @@ def on_ok(self, title, text): def on_remove_content(self, content_name): raise NotImplementedError() - def on_select(self, title, items=None): + def on_select(self, title, items=None, preselect=-1, use_details=False): raise NotImplementedError() def open_settings(self): 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 b6fc6a7e2..0180834f8 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 @@ -66,38 +66,40 @@ def on_delete_content(self, content_name): text = self._context.localize('content.delete') % to_unicode(content_name) return self.on_yes_no_input(self._context.localize('content.delete.confirm'), text) - def on_select(self, title, items=None): + def on_select(self, title, items=None, preselect=-1, use_details=False): if items is None: items = [] - use_details = (isinstance(items[0], tuple) and len(items[0]) == 4) - - _dict = {} - _items = [] - i = 0 - for item in items: + result_map = {} + dialog_items = [] + for idx, item in enumerate(items): if isinstance(item, tuple): - if use_details: - new_item = xbmcgui.ListItem(label=item[0], label2=item[1]) - new_item.setArt({'icon': item[3], 'thumb': item[3]}) - _items.append(new_item) - _dict[i] = item[2] + num_details = len(item) + if num_details > 2: + list_item = xbmcgui.ListItem(label=item[0], + label2=item[1], + offscreen=True) + if num_details > 3: + use_details = True + icon = item[3] + list_item.setArt({'icon': icon, 'thumb': icon}) + if num_details > 4 and item[4]: + preselect = idx + result_map[idx] = item[2] + dialog_items.append(list_item) else: - _dict[i] = item[1] - _items.append(item[0]) + result_map[idx] = item[1] + dialog_items.append(item[0]) else: - _dict[i] = i - _items.append(item) - - i += 1 + result_map[idx] = idx + dialog_items.append(item) dialog = xbmcgui.Dialog() - if use_details: - result = dialog.select(title, _items, useDetails=use_details) - else: - result = dialog.select(title, _items) - - return _dict.get(result, -1) + result = dialog.select(title, + dialog_items, + preselect=preselect, + useDetails=use_details) + return result_map.get(result, -1) def show_notification(self, message, diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 7676698e0..999c8ebd1 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -82,6 +82,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime.combine( date=date.today(), time=dt_time(**match) @@ -95,6 +97,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime(**match) # full date time @@ -105,6 +109,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime(**match) # period - at the moment we support only hours, minutes and seconds @@ -129,6 +135,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime(**match) raise KodionException('Could not parse |{datetime}| as ISO 8601' diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 41c040515..a96d827b7 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -184,7 +184,7 @@ def create_path(*args): if isinstance(arg, (list, tuple)): return create_path(*arg) - comps.append(str(arg.strip('/').replace('\\', '/').replace('//', '/'))) + comps.append(str(arg).strip('/').replace('\\', '/').replace('//', '/')) uri_path = '/'.join(comps) if uri_path: @@ -199,7 +199,7 @@ def create_uri_path(*args): if isinstance(arg, (list, tuple)): return create_uri_path(*arg) - comps.append(str(arg.strip('/').replace('\\', '/').replace('//', '/'))) + comps.append(str(arg).strip('/').replace('\\', '/').replace('//', '/')) uri_path = '/'.join(comps) if uri_path: diff --git a/resources/lib/youtube_plugin/script_actions.py b/resources/lib/youtube_plugin/script_actions.py index 2bd4560b5..8e0401a33 100644 --- a/resources/lib/youtube_plugin/script_actions.py +++ b/resources/lib/youtube_plugin/script_actions.py @@ -58,7 +58,9 @@ def _config_actions(action, *_args): ] sub_opts[sub_setting] = ui.bold(sub_opts[sub_setting]) - result = ui.on_select(localize('subtitles.language'), sub_opts) + result = ui.on_select(localize('subtitles.language'), + sub_opts, + preselect=sub_setting) if result > -1: settings.set_subtitle_languages(result) @@ -182,7 +184,10 @@ def select_user(reason, new_user=False): usernames.append(username) if new_user: usernames.append(ui.italic(localize('user.new'))) - return ui.on_select(reason, usernames), sorted(current_users.keys()) + return ( + ui.on_select(reason, usernames, preselect=current_user), + sorted(current_users.keys()), + ) def add_user(): results = ui.on_keyboard_input(localize('user.enter_name')) diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 13c2d5f53..e21972ce5 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -174,7 +174,6 @@ class YouTubeRequestClient(BaseRequestsClass): 'smarttv_embedded': { '_id': 85, 'json': { - 'params': '2AMBCgIQBg', 'context': { 'client': { 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', @@ -275,18 +274,18 @@ def __init__(self, language=None, region=None, exc_type=None, **_kwargs): super(YouTubeRequestClient, self).__init__(exc_type=exc_type) @classmethod - def json_traverse(cls, json_data, path): + def json_traverse(cls, json_data, path, default=None): if not json_data or not path: - return None + return default result = json_data for idx, keys in enumerate(path): if not isinstance(result, (dict, list, tuple)): - return None + return default if isinstance(keys, slice): return [ - cls.json_traverse(part, path[idx + 1:]) + cls.json_traverse(part, path[idx + 1:], default=default) for part in result[keys] if part ] @@ -296,7 +295,7 @@ def json_traverse(cls, json_data, path): for key in keys: if isinstance(key, (list, tuple)): - new_result = cls.json_traverse(result, key) + new_result = cls.json_traverse(result, key, default=default) if new_result: result = new_result break @@ -308,10 +307,10 @@ def json_traverse(cls, json_data, path): continue break else: - return None + return default if result == json_data: - return None + return default return result @classmethod diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 21958d354..0ea54d265 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -14,6 +14,7 @@ import xml.etree.ElementTree as ET from copy import deepcopy from itertools import chain, islice +from operator import itemgetter from random import randint from .login_client import LoginClient @@ -50,6 +51,42 @@ class YouTube(LoginClient): 'Host': 'www.googleapis.com', }, }, + 'tv': { + 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + 'method': None, + 'json': { + 'context': { + 'client': { + 'clientName': 'TVHTML5', + 'clientVersion': '5.20150304', + }, + }, + }, + 'headers': { + 'Host': 'www.youtube.com', + }, + 'params': { + 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', + }, + }, + 'tv_embed': { + 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + 'method': None, + 'json': { + 'context': { + 'client': { + 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', + 'clientVersion': '2.0', + }, + }, + }, + 'headers': { + 'Host': 'www.youtube.com', + }, + 'params': { + 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', + }, + }, '_common': { '_access_token': None, 'json': { @@ -210,6 +247,7 @@ def remove_playlist(self, playlist_id, **kwargs): return self.api_request(method='DELETE', path='playlists', params=params, + no_content=True, **kwargs) def get_supported_languages(self, language=None, **kwargs): @@ -285,6 +323,7 @@ def rate_video(self, video_id, rating='like', **kwargs): return self.api_request(method='POST', path='videos/rate', params=params, + no_content=True, **kwargs) def add_video_to_playlist(self, playlist_id, video_id, **kwargs): @@ -309,6 +348,7 @@ def remove_video_from_playlist(self, return self.api_request(method='DELETE', path='playlistItems', params=params, + no_content=True, **kwargs) def unsubscribe(self, subscription_id, **kwargs): @@ -316,6 +356,15 @@ def unsubscribe(self, subscription_id, **kwargs): return self.api_request(method='DELETE', path='subscriptions', params=params, + no_content=True, + **kwargs) + + def unsubscribe_channel(self, channel_id, **kwargs): + post_data = {'channelIds': [channel_id]} + return self.api_request(version=1, + method='POST', + path='subscription/unsubscribe', + post_data=post_data, **kwargs) def subscribe(self, channel_id, **kwargs): @@ -693,9 +742,10 @@ def index_items(items, index, # Fetch related videos. Use threads for faster execution. def threaded_get_related(video_id, func, *args, **kwargs): - related_videos = self.get_related_videos(video_id).get('items') - if related_videos: - func(related_videos[:items_per_page], *args, **kwargs) + related = self.get_related_videos(video_id, + max_results=items_per_page) + if related and 'items' in related: + func(related['items'][:items_per_page], *args, **kwargs) running = 0 threads = [] @@ -1023,113 +1073,200 @@ def get_related_videos(self, video_id, page_token='', max_results=0, + offset=0, + retry=0, **kwargs): - # TODO: Improve handling of InnerTube requests, including automatic - # continuation processing to retrieve max_results number of results - # See Youtube.get_saved_playlists for existing implementation max_results = self._max_results if max_results <= 0 else max_results post_data = {'videoId': video_id} if page_token: post_data['continuation'] = page_token - result = self.api_request(version=1, + result = self.api_request(version=('tv' if retry == 1 else + 'tv_embed' if retry == 2 else 1), method='POST', path='next', post_data=post_data, no_login=True) if not result: - return [] + return {} - related_videos = self.json_traverse( - result, - path=( - ( - 'onResponseReceivedEndpoints', - 0, - 'appendContinuationItemsAction', - 'continuationItems', - ) if page_token else ( - 'contents', - 'twoColumnWatchNextResults', - 'secondaryResults', - 'secondaryResults', - 'results', - ) - ) + ( - slice(None), - ( - ( - 'compactVideoRenderer', - # 'videoId', - ), - ( - 'continuationItemRenderer', - 'continuationEndpoint', - 'continuationCommand', - # 'token', - ), - ), - ) - ) - if not related_videos: - return [] - - channel_id = self.json_traverse( - result, - path=( + related_videos = self.json_traverse(result, path=( + ( + 'onResponseReceivedEndpoints', + 0, + 'appendContinuationItemsAction', + 'continuationItems', + ) if page_token else ( + 'contents', + 'singleColumnWatchNextResults', + 'pivot', + 'pivot', + 'contents', + slice(0, None, None), + 'pivotShelfRenderer', + 'content', + 'pivotHorizontalListRenderer', + 'items', + ) if retry == 1 else ( + 'contents', + 'singleColumnWatchNextResults', + 'results', + 'results', + 'contents', + 2, + 'shelfRenderer', + 'content', + 'horizontalListRenderer', + 'items', + ) if retry == 2 else ( 'contents', 'twoColumnWatchNextResults', + 'secondaryResults', + 'secondaryResults', 'results', - 'results', - 'contents', - 1, - 'videoSecondaryInfoRenderer', - 'owner', - 'videoOwnerRenderer', - 'title', - 'runs', - 0, - 'navigationEndpoint', - 'browseEndpoint', - 'browseId' ) - ) + ) + ( + slice(offset, None, None), + ( + 'pivotVideoRenderer', + # 'videoId', + ) if retry == 1 else ( + 'compactVideoRenderer', + # 'videoId', + ) if retry == 2 else ( + ( + 'compactVideoRenderer', + # 'videoId', + ), + ( + 'continuationItemRenderer', + 'continuationEndpoint', + 'continuationCommand', + # 'token', + ), + ), + ), default=[]) + if not related_videos or not any(related_videos): + return {} if retry > 1 else self.get_related_videos( + video_id, + page_token=page_token, + max_results=max_results, + retry=(retry + 1), + **kwargs + ) - v3_response = { - 'kind': 'youtube#videoListResponse', - 'items': [ - { - 'kind': "youtube#video", - 'id': video['videoId'], - 'related_video_id': video_id, - 'related_channel_id': channel_id, - 'partial': True, - 'snippet': { - 'title': video['title']['simpleText'], - 'thumbnails': dict(zip( - ('default', 'high'), - video['thumbnail']['thumbnails'], - )), - 'channelId': self.json_traverse(video, ( - ('longBylineText', 'shortBylineText'), + channel_id = self.json_traverse(result, path=( + 'contents', + 'singleColumnWatchNextResults', + 'results', + 'results', + 'contents', + 1, + 'itemSectionRenderer', + 'contents', + 0, + 'videoOwnerRenderer', + 'navigationEndpoint', + 'browseEndpoint', + 'browseId' + ) if retry else ( + 'contents', + 'twoColumnWatchNextResults', + 'results', + 'results', + 'contents', + 1, + 'videoSecondaryInfoRenderer', + 'owner', + 'videoOwnerRenderer', + 'title', + 'runs', + 0, + 'navigationEndpoint', + 'browseEndpoint', + 'browseId' + )) + + thumb_getter = itemgetter(0, -1) + if retry == 1: + related_videos = chain.from_iterable(related_videos) + + items = [{ + 'kind': "youtube#video", + 'id': video['videoId'], + 'related_video_id': video_id, + 'related_channel_id': channel_id, + 'partial': True, + 'snippet': { + 'title': self.json_traverse(video, path=( + 'title', + ( + ( + 'simpleText', + ), + ( 'runs', 0, - 'navigationEndpoint', - 'browseEndpoint', - 'browseId', - )), - } - } - for video in related_videos - if video and 'videoId' in video - ] + 'text' + ), + ) + )), + 'thumbnails': dict(zip( + ('default', 'high'), + thumb_getter(video['thumbnail']['thumbnails']), + )), + 'channelId': self.json_traverse(video, path=( + ('longBylineText', 'shortBylineText'), + 'runs', + 0, + 'navigationEndpoint', + 'browseEndpoint', + 'browseId', + )), + } + } for video in related_videos if video and 'videoId' in video] + + v3_response = { + 'kind': 'youtube#videoListResponse', + 'items': [], } - last_item = related_videos[-1] - if last_item and 'token' in last_item: - v3_response['nextPageToken'] = last_item['token'] + if not retry: + last_item = related_videos[-1] + if last_item and 'token' in last_item: + page_token = last_item['token'] + + while 1: + remaining = max_results - len(items) + if remaining < 0: + items = items[:max_results] + if page_token: + v3_response['nextPageToken'] = page_token + v3_response['offset'] = remaining + break + if not page_token: + break + + if not remaining: + v3_response['nextPageToken'] = page_token + break + + continuation = self.get_related_videos( + video_id, + page_token=page_token, + max_results=remaining, + **kwargs + ) + if 'nextPageToken' in continuation: + page_token = continuation['nextPageToken'] + else: + page_token = '' + if 'items' in continuation: + items.extend(continuation['items']) + + v3_response['items'] = items return v3_response def get_parent_comments(self, @@ -1581,6 +1718,8 @@ def _response_hook(self, **kwargs): response = kwargs['response'] self._context.log_debug('API response: |{0.status_code}|\n' 'headers: |{0.headers}|'.format(response)) + if response.status_code == 204 and 'no_content' in kwargs: + return True try: json_data = response.json() if 'error' in json_data: @@ -1707,9 +1846,8 @@ def api_request(self, params=log_params, data=client.get('json'), headers=log_headers)) - - json_data = self.request(response_hook=self._response_hook, - response_hook_kwargs=kwargs, - error_hook=self._error_hook, - **client) - return json_data + response = self.request(response_hook=self._response_hook, + response_hook_kwargs=kwargs, + error_hook=self._error_hook, + **client) + return response diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index c956915c6..8f7b52b0b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -14,7 +14,7 @@ import time from math import log10 -from ...kodion.constants import content +from ...kodion.constants import content, paths from ...kodion.items import DirectoryItem, menu_items from ...kodion.utils import ( create_path, @@ -167,7 +167,7 @@ def update_channel_infos(provider, context, channel_id_dict, path = context.get_path() filter_list = None - if path == '/subscriptions/list/': + if path.startswith(paths.SUBSCRIPTIONS): in_subscription_list = True if settings.get_bool('youtube.folder.my_subscriptions_filtered.show', False): @@ -209,7 +209,7 @@ def update_channel_infos(provider, context, channel_id_dict, channel_item.set_channel_subscription_id(subscription_id) context_menu.append( menu_items.unsubscribe_from_channel( - context, subscription_id + context, subscription_id=subscription_id ) ) @@ -282,7 +282,7 @@ def update_playlist_infos(provider, context, playlist_id_dict, channel_id = snippet['channelId'] # if the path directs to a playlist of our own, set channel id to 'mine' - if path == '/channel/mine/playlists/': + if path.startswith(paths.MY_PLAYLISTS): channel_id = 'mine' channel_name = snippet.get('channelTitle', '') @@ -377,6 +377,13 @@ def update_video_infos(provider, context, video_id_dict, path = context.get_path() ui = context.get_ui() + if path.startswith(paths.MY_SUBSCRIPTIONS): + in_my_subscriptions_list = True + playlist_match = False + else: + in_my_subscriptions_list = False + playlist_match = __RE_PLAYLIST_MATCH.match(path) + for video_id, yt_item in data.items(): video_item = video_id_dict[video_id] @@ -572,7 +579,6 @@ def update_video_infos(provider, context, video_id_dict, /channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/ /playlist/[PLAYLIST_ID]/ """ - playlist_match = __RE_PLAYLIST_MATCH.match(path) playlist_id = playlist_channel_id = '' if playlist_match: replace_context_menu = True @@ -617,7 +623,10 @@ def update_video_infos(provider, context, video_id_dict, video_item.set_playlist_item_id(playlist_item_id) context_menu.append( menu_items.remove_video_from_playlist( - context, playlist_id, video_id, video_item.get_name() + context, + playlist_id=playlist_id, + video_id=playlist_item_id, + video_name=video_item.get_name(), ) ) @@ -632,8 +641,12 @@ def update_video_infos(provider, context, video_id_dict, ) if logged_in: - # subscribe to the channel of the video context_menu.append( + # unsubscribe from the channel of the video + menu_items.unsubscribe_from_channel( + context, channel_id=channel_id + ) if in_my_subscriptions_list else + # subscribe to the channel of the video menu_items.subscribe_to_channel( context, channel_id, channel_name ) @@ -658,8 +671,8 @@ def update_video_infos(provider, context, video_id_dict, ) # more... - refresh_container = (path.startswith('/channel/mine/playlist/LL') - or path == '/special/disliked_videos/') + refresh_container = path.startswith((paths.LIKED_VIDEOS, + paths.DISLIKED_VIDEOS)) context_menu.extend(( menu_items.more_for_video( context, @@ -804,8 +817,13 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id): result_items = [] try: - json_data = client.get_related_videos(video_id, page_token=page_token, max_results=17) - result_items = v3.response_to_items(provider, context, json_data, process_next_page=False) + json_data = client.get_related_videos(video_id, + page_token=page_token, + max_results=5) + result_items = v3.response_to_items(provider, + context, + json_data, + process_next_page=False) page_token = json_data.get('nextPageToken', '') except: context.get_ui().show_notification('Failed to add a suggested video.', time_ms=5000) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 80b849c37..43d0afae8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -21,6 +21,7 @@ update_playlist_infos, update_video_infos, ) +from ...kodion.constants import paths from ...kodion import KodionException from ...kodion.items import DirectoryItem, NextPageItem, VideoItem, menu_items @@ -118,7 +119,7 @@ def _process_list_response(provider, context, json_data): elif kind == 'playlist': # set channel id to 'mine' if the path is for a playlist of our own - if context.get_path() == '/channel/mine/playlists/': + if context.get_path().startswith(paths.MY_PLAYLISTS): channel_id = 'mine' else: channel_id = snippet['channelId'] @@ -382,6 +383,7 @@ def response_to_items(provider, yt_total_results = int(page_info.get('totalResults', 0)) yt_results_per_page = int(page_info.get('resultsPerPage', 0)) page = int(context.get_param('page', 1)) + offset = int(json_data.get('offset', 0)) yt_visitor_data = json_data.get('visitorData', '') yt_next_page_token = json_data.get('nextPageToken', '') yt_click_tracking = json_data.get('clickTracking', '') @@ -394,10 +396,12 @@ def response_to_items(provider, new_params = dict(context.get_params(), page_token=yt_next_page_token) - if yt_click_tracking: + if yt_visitor_data: new_params['visitor'] = yt_visitor_data if yt_click_tracking: new_params['click_tracking'] = yt_click_tracking + if offset: + new_params['offset'] = offset new_context = context.clone(new_params=new_params) current_page = new_context.get_param('page', 1) next_page_item = NextPageItem(new_context, current_page) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index d031dfc0d..7647b2823 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1344,13 +1344,25 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): allow_hfr = 'hfr' in stream_features disable_hfr_max = 'no_hfr_max' in stream_features allow_ssa = 'ssa' in stream_features + integer_frame_rate_hint = 'no_frac_fr_hint' in stream_features stream_select = _settings.stream_select() fps_scale_map = { 0: '{0}000/1000', # --.00 fps - 24: '24000/1001', # 23.97 fps + 24: '24000/1000', # 24.00 fps + 25: '25000/1000', # 25.00 fps + 30: '30000/1000', # 30.00 fps + 48: '48000/1000', # 48.00 fps + 50: '50000/1000', # 50.00 fps + 60: '60000/1000', # 60.00 fps + } if integer_frame_rate_hint else { + 0: '{0}000/1000', # --.00 fps + 24: '24000/1001', # 23.976 fps + 25: '25000/1000', # 25.00 fps 30: '30000/1001', # 29.97 fps - 60: '60000/1001', # 59.97 fps + 48: '48000/1000', # 48.00 fps + 50: '50000/1000', # 50.00 fps + 60: '60000/1001', # 59.94 fps } quality_factor_map = { @@ -1672,6 +1684,7 @@ def _filter_group(previous_group, previous_stream, item): _settings = self._context.get_settings() stream_features = _settings.stream_features() do_filter = 'filter' in stream_features + frame_rate_hint = 'no_fr_hint' not in stream_features stream_select = _settings.stream_select() main_stream = { @@ -1822,8 +1835,8 @@ def _filter_group(previous_group, previous_stream, item): ' mimeType="{mimeType}"' ' bandwidth="{bitrate}"' ' width="{width}"' - ' height="{height}"' - ' frameRate="{frameRate}"' + ' height="{height}"' + + (' frameRate="{frameRate}"' if frame_rate_hint else '') + # quality and priority attributes are not used by ISA ' qualityRanking="{quality}"' ' selectionPriority="{priority}"' diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index cbd880c40..14a6c7bd2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -15,7 +15,7 @@ def _process_add_video(provider, context, keymap_action=False): - path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + path = context.get_listitem_detail('FileNameAndPath', attr=True) client = provider.get_client(context) logged_in = provider.is_logged_in() @@ -64,9 +64,9 @@ def _process_add_video(provider, context, keymap_action=False): def _process_remove_video(provider, context): - listitem_playlist_id = context.get_infolabel('Container.ListItem(0).Property(playlist_id)') - listitem_playlist_item_id = context.get_infolabel('Container.ListItem(0).Property(playlist_item_id)') - listitem_title = context.get_infolabel('Container.ListItem(0).Title') + listitem_playlist_id = context.get_listitem_detail('playlist_id') + listitem_playlist_item_id = context.get_listitem_detail('playlist_item_id') + listitem_title = context.get_listitem_detail('Title', attr=True) keymap_action = False playlist_id = context.get_param('playlist_id', '') @@ -96,9 +96,11 @@ def _process_remove_video(provider, context): if playlist_id.strip().lower() not in ('wl', 'hl'): if context.get_ui().on_remove_content(video_name): - json_data = provider.get_client(context).remove_video_from_playlist(playlist_id=playlist_id, - playlist_item_id=video_id) - if not json_data: + success = provider.get_client(context).remove_video_from_playlist( + playlist_id=playlist_id, + playlist_item_id=video_id, + ) + if not success: return False context.get_ui().refresh_container() @@ -139,7 +141,7 @@ def _process_remove_playlist(provider, context): def _process_select_playlist(provider, context): # Get listitem path asap, relies on listitems focus - path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + path = context.get_listitem_detail('FileNameAndPath', attr=True) params = context.get_params() ui = context.get_ui() diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 39f0695f9..3e1c34cb7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -13,91 +13,213 @@ from ...kodion.network import Locator -DEFAULT_LANGUAGES = {'items': [{'snippet': {'name': 'Afrikaans', 'hl': 'af'}, 'id': 'af'}, {'snippet': {'name': 'Azerbaijani', 'hl': 'az'}, 'id': 'az'}, {'snippet': {'name': 'Indonesian', 'hl': 'id'}, 'id': 'id'}, {'snippet': {'name': 'Malay', 'hl': 'ms'}, 'id': 'ms'}, - {'snippet': {'name': 'Catalan', 'hl': 'ca'}, 'id': 'ca'}, {'snippet': {'name': 'Czech', 'hl': 'cs'}, 'id': 'cs'}, {'snippet': {'name': 'Danish', 'hl': 'da'}, 'id': 'da'}, {'snippet': {'name': 'German', 'hl': 'de'}, 'id': 'de'}, - {'snippet': {'name': 'Estonian', 'hl': 'et'}, 'id': 'et'}, {'snippet': {'name': 'English (United Kingdom)', 'hl': 'en-GB'}, 'id': 'en-GB'}, {'snippet': {'name': 'English', 'hl': 'en'}, 'id': 'en'}, - {'snippet': {'name': 'Spanish (Spain)', 'hl': 'es'}, 'id': 'es'}, {'snippet': {'name': 'Spanish (Latin America)', 'hl': 'es-419'}, 'id': 'es-419'}, {'snippet': {'name': 'Basque', 'hl': 'eu'}, 'id': 'eu'}, - {'snippet': {'name': 'Filipino', 'hl': 'fil'}, 'id': 'fil'}, {'snippet': {'name': 'French', 'hl': 'fr'}, 'id': 'fr'}, {'snippet': {'name': 'French (Canada)', 'hl': 'fr-CA'}, 'id': 'fr-CA'}, {'snippet': {'name': 'Galician', 'hl': 'gl'}, 'id': 'gl'}, - {'snippet': {'name': 'Croatian', 'hl': 'hr'}, 'id': 'hr'}, {'snippet': {'name': 'Zulu', 'hl': 'zu'}, 'id': 'zu'}, {'snippet': {'name': 'Icelandic', 'hl': 'is'}, 'id': 'is'}, {'snippet': {'name': 'Italian', 'hl': 'it'}, 'id': 'it'}, - {'snippet': {'name': 'Swahili', 'hl': 'sw'}, 'id': 'sw'}, {'snippet': {'name': 'Latvian', 'hl': 'lv'}, 'id': 'lv'}, {'snippet': {'name': 'Lithuanian', 'hl': 'lt'}, 'id': 'lt'}, {'snippet': {'name': 'Hungarian', 'hl': 'hu'}, 'id': 'hu'}, - {'snippet': {'name': 'Dutch', 'hl': 'nl'}, 'id': 'nl'}, {'snippet': {'name': 'Norwegian', 'hl': 'no'}, 'id': 'no'}, {'snippet': {'name': 'Uzbek', 'hl': 'uz'}, 'id': 'uz'}, {'snippet': {'name': 'Polish', 'hl': 'pl'}, 'id': 'pl'}, - {'snippet': {'name': 'Portuguese (Portugal)', 'hl': 'pt-PT'}, 'id': 'pt-PT'}, {'snippet': {'name': 'Portuguese (Brazil)', 'hl': 'pt'}, 'id': 'pt'}, {'snippet': {'name': 'Romanian', 'hl': 'ro'}, 'id': 'ro'}, - {'snippet': {'name': 'Albanian', 'hl': 'sq'}, 'id': 'sq'}, {'snippet': {'name': 'Slovak', 'hl': 'sk'}, 'id': 'sk'}, {'snippet': {'name': 'Slovenian', 'hl': 'sl'}, 'id': 'sl'}, {'snippet': {'name': 'Finnish', 'hl': 'fi'}, 'id': 'fi'}, - {'snippet': {'name': 'Swedish', 'hl': 'sv'}, 'id': 'sv'}, {'snippet': {'name': 'Vietnamese', 'hl': 'vi'}, 'id': 'vi'}, {'snippet': {'name': 'Turkish', 'hl': 'tr'}, 'id': 'tr'}, {'snippet': {'name': 'Bulgarian', 'hl': 'bg'}, 'id': 'bg'}, - {'snippet': {'name': 'Kyrgyz', 'hl': 'ky'}, 'id': 'ky'}, {'snippet': {'name': 'Kazakh', 'hl': 'kk'}, 'id': 'kk'}, {'snippet': {'name': 'Macedonian', 'hl': 'mk'}, 'id': 'mk'}, {'snippet': {'name': 'Mongolian', 'hl': 'mn'}, 'id': 'mn'}, - {'snippet': {'name': 'Russian', 'hl': 'ru'}, 'id': 'ru'}, {'snippet': {'name': 'Serbian', 'hl': 'sr'}, 'id': 'sr'}, {'snippet': {'name': 'Ukrainian', 'hl': 'uk'}, 'id': 'uk'}, {'snippet': {'name': 'Greek', 'hl': 'el'}, 'id': 'el'}, - {'snippet': {'name': 'Armenian', 'hl': 'hy'}, 'id': 'hy'}, {'snippet': {'name': 'Hebrew', 'hl': 'iw'}, 'id': 'iw'}, {'snippet': {'name': 'Urdu', 'hl': 'ur'}, 'id': 'ur'}, {'snippet': {'name': 'Arabic', 'hl': 'ar'}, 'id': 'ar'}, - {'snippet': {'name': 'Persian', 'hl': 'fa'}, 'id': 'fa'}, {'snippet': {'name': 'Nepali', 'hl': 'ne'}, 'id': 'ne'}, {'snippet': {'name': 'Marathi', 'hl': 'mr'}, 'id': 'mr'}, {'snippet': {'name': 'Hindi', 'hl': 'hi'}, 'id': 'hi'}, - {'snippet': {'name': 'Bengali', 'hl': 'bn'}, 'id': 'bn'}, {'snippet': {'name': 'Punjabi', 'hl': 'pa'}, 'id': 'pa'}, {'snippet': {'name': 'Gujarati', 'hl': 'gu'}, 'id': 'gu'}, {'snippet': {'name': 'Tamil', 'hl': 'ta'}, 'id': 'ta'}, - {'snippet': {'name': 'Telugu', 'hl': 'te'}, 'id': 'te'}, {'snippet': {'name': 'Kannada', 'hl': 'kn'}, 'id': 'kn'}, {'snippet': {'name': 'Malayalam', 'hl': 'ml'}, 'id': 'ml'}, {'snippet': {'name': 'Sinhala', 'hl': 'si'}, 'id': 'si'}, - {'snippet': {'name': 'Thai', 'hl': 'th'}, 'id': 'th'}, {'snippet': {'name': 'Lao', 'hl': 'lo'}, 'id': 'lo'}, {'snippet': {'name': 'Myanmar (Burmese)', 'hl': 'my'}, 'id': 'my'}, {'snippet': {'name': 'Georgian', 'hl': 'ka'}, 'id': 'ka'}, - {'snippet': {'name': 'Amharic', 'hl': 'am'}, 'id': 'am'}, {'snippet': {'name': 'Khmer', 'hl': 'km'}, 'id': 'km'}, {'snippet': {'name': 'Chinese', 'hl': 'zh-CN'}, 'id': 'zh-CN'}, {'snippet': {'name': 'Chinese (Taiwan)', 'hl': 'zh-TW'}, 'id': 'zh-TW'}, - {'snippet': {'name': 'Chinese (Hong Kong)', 'hl': 'zh-HK'}, 'id': 'zh-HK'}, {'snippet': {'name': 'Japanese', 'hl': 'ja'}, 'id': 'ja'}, {'snippet': {'name': 'Korean', 'hl': 'ko'}, 'id': 'ko'}]} -DEFAULT_REGIONS = {'items': [{'snippet': {'gl': 'DZ', 'name': 'Algeria'}, 'id': 'DZ'}, {'snippet': {'gl': 'AR', 'name': 'Argentina'}, 'id': 'AR'}, {'snippet': {'gl': 'AU', 'name': 'Australia'}, 'id': 'AU'}, {'snippet': {'gl': 'AT', 'name': 'Austria'}, 'id': 'AT'}, - {'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}, 'id': 'AZ'}, {'snippet': {'gl': 'BH', 'name': 'Bahrain'}, 'id': 'BH'}, {'snippet': {'gl': 'BY', 'name': 'Belarus'}, 'id': 'BY'}, {'snippet': {'gl': 'BE', 'name': 'Belgium'}, 'id': 'BE'}, - {'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}, 'id': 'BA'}, {'snippet': {'gl': 'BR', 'name': 'Brazil'}, 'id': 'BR'}, {'snippet': {'gl': 'BG', 'name': 'Bulgaria'}, 'id': 'BG'}, {'snippet': {'gl': 'CA', 'name': 'Canada'}, 'id': 'CA'}, - {'snippet': {'gl': 'CL', 'name': 'Chile'}, 'id': 'CL'}, {'snippet': {'gl': 'CO', 'name': 'Colombia'}, 'id': 'CO'}, {'snippet': {'gl': 'HR', 'name': 'Croatia'}, 'id': 'HR'}, {'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}, 'id': 'CZ'}, - {'snippet': {'gl': 'DK', 'name': 'Denmark'}, 'id': 'DK'}, {'snippet': {'gl': 'EG', 'name': 'Egypt'}, 'id': 'EG'}, {'snippet': {'gl': 'EE', 'name': 'Estonia'}, 'id': 'EE'}, {'snippet': {'gl': 'FI', 'name': 'Finland'}, 'id': 'FI'}, - {'snippet': {'gl': 'FR', 'name': 'France'}, 'id': 'FR'}, {'snippet': {'gl': 'GE', 'name': 'Georgia'}, 'id': 'GE'}, {'snippet': {'gl': 'DE', 'name': 'Germany'}, 'id': 'DE'}, {'snippet': {'gl': 'GH', 'name': 'Ghana'}, 'id': 'GH'}, - {'snippet': {'gl': 'GR', 'name': 'Greece'}, 'id': 'GR'}, {'snippet': {'gl': 'HK', 'name': 'Hong Kong'}, 'id': 'HK'}, {'snippet': {'gl': 'HU', 'name': 'Hungary'}, 'id': 'HU'}, {'snippet': {'gl': 'IS', 'name': 'Iceland'}, 'id': 'IS'}, - {'snippet': {'gl': 'IN', 'name': 'India'}, 'id': 'IN'}, {'snippet': {'gl': 'ID', 'name': 'Indonesia'}, 'id': 'ID'}, {'snippet': {'gl': 'IQ', 'name': 'Iraq'}, 'id': 'IQ'}, {'snippet': {'gl': 'IE', 'name': 'Ireland'}, 'id': 'IE'}, - {'snippet': {'gl': 'IL', 'name': 'Israel'}, 'id': 'IL'}, {'snippet': {'gl': 'IT', 'name': 'Italy'}, 'id': 'IT'}, {'snippet': {'gl': 'JM', 'name': 'Jamaica'}, 'id': 'JM'}, {'snippet': {'gl': 'JP', 'name': 'Japan'}, 'id': 'JP'}, - {'snippet': {'gl': 'JO', 'name': 'Jordan'}, 'id': 'JO'}, {'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}, 'id': 'KZ'}, {'snippet': {'gl': 'KE', 'name': 'Kenya'}, 'id': 'KE'}, {'snippet': {'gl': 'KW', 'name': 'Kuwait'}, 'id': 'KW'}, - {'snippet': {'gl': 'LV', 'name': 'Latvia'}, 'id': 'LV'}, {'snippet': {'gl': 'LB', 'name': 'Lebanon'}, 'id': 'LB'}, {'snippet': {'gl': 'LY', 'name': 'Libya'}, 'id': 'LY'}, {'snippet': {'gl': 'LT', 'name': 'Lithuania'}, 'id': 'LT'}, - {'snippet': {'gl': 'LU', 'name': 'Luxembourg'}, 'id': 'LU'}, {'snippet': {'gl': 'MK', 'name': 'Macedonia'}, 'id': 'MK'}, {'snippet': {'gl': 'MY', 'name': 'Malaysia'}, 'id': 'MY'}, {'snippet': {'gl': 'MX', 'name': 'Mexico'}, 'id': 'MX'}, - {'snippet': {'gl': 'ME', 'name': 'Montenegro'}, 'id': 'ME'}, {'snippet': {'gl': 'MA', 'name': 'Morocco'}, 'id': 'MA'}, {'snippet': {'gl': 'NP', 'name': 'Nepal'}, 'id': 'NP'}, {'snippet': {'gl': 'NL', 'name': 'Netherlands'}, 'id': 'NL'}, - {'snippet': {'gl': 'NZ', 'name': 'New Zealand'}, 'id': 'NZ'}, {'snippet': {'gl': 'NG', 'name': 'Nigeria'}, 'id': 'NG'}, {'snippet': {'gl': 'NO', 'name': 'Norway'}, 'id': 'NO'}, {'snippet': {'gl': 'OM', 'name': 'Oman'}, 'id': 'OM'}, - {'snippet': {'gl': 'PK', 'name': 'Pakistan'}, 'id': 'PK'}, {'snippet': {'gl': 'PE', 'name': 'Peru'}, 'id': 'PE'}, {'snippet': {'gl': 'PH', 'name': 'Philippines'}, 'id': 'PH'}, {'snippet': {'gl': 'PL', 'name': 'Poland'}, 'id': 'PL'}, - {'snippet': {'gl': 'PT', 'name': 'Portugal'}, 'id': 'PT'}, {'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}, 'id': 'PR'}, {'snippet': {'gl': 'QA', 'name': 'Qatar'}, 'id': 'QA'}, {'snippet': {'gl': 'RO', 'name': 'Romania'}, 'id': 'RO'}, - {'snippet': {'gl': 'RU', 'name': 'Russia'}, 'id': 'RU'}, {'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}, 'id': 'SA'}, {'snippet': {'gl': 'SN', 'name': 'Senegal'}, 'id': 'SN'}, {'snippet': {'gl': 'RS', 'name': 'Serbia'}, 'id': 'RS'}, - {'snippet': {'gl': 'SG', 'name': 'Singapore'}, 'id': 'SG'}, {'snippet': {'gl': 'SK', 'name': 'Slovakia'}, 'id': 'SK'}, {'snippet': {'gl': 'SI', 'name': 'Slovenia'}, 'id': 'SI'}, {'snippet': {'gl': 'ZA', 'name': 'South Africa'}, 'id': 'ZA'}, - {'snippet': {'gl': 'KR', 'name': 'South Korea'}, 'id': 'KR'}, {'snippet': {'gl': 'ES', 'name': 'Spain'}, 'id': 'ES'}, {'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}, 'id': 'LK'}, {'snippet': {'gl': 'SE', 'name': 'Sweden'}, 'id': 'SE'}, - {'snippet': {'gl': 'CH', 'name': 'Switzerland'}, 'id': 'CH'}, {'snippet': {'gl': 'TW', 'name': 'Taiwan'}, 'id': 'TW'}, {'snippet': {'gl': 'TZ', 'name': 'Tanzania'}, 'id': 'TZ'}, {'snippet': {'gl': 'TH', 'name': 'Thailand'}, 'id': 'TH'}, - {'snippet': {'gl': 'TN', 'name': 'Tunisia'}, 'id': 'TN'}, {'snippet': {'gl': 'TR', 'name': 'Turkey'}, 'id': 'TR'}, {'snippet': {'gl': 'UG', 'name': 'Uganda'}, 'id': 'UG'}, {'snippet': {'gl': 'UA', 'name': 'Ukraine'}, 'id': 'UA'}, - {'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}, 'id': 'AE'}, {'snippet': {'gl': 'GB', 'name': 'United Kingdom'}, 'id': 'GB'}, {'snippet': {'gl': 'US', 'name': 'United States'}, 'id': 'US'}, {'snippet': {'gl': 'VN', 'name': 'Vietnam'}, 'id': 'VN'}, - {'snippet': {'gl': 'YE', 'name': 'Yemen'}, 'id': 'YE'}, {'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}, 'id': 'ZW'}]} +DEFAULT_LANGUAGES = {'items': [ + {'snippet': {'name': 'Afrikaans', 'hl': 'af'}, 'id': 'af'}, + {'snippet': {'name': 'Azerbaijani', 'hl': 'az'}, 'id': 'az'}, + {'snippet': {'name': 'Indonesian', 'hl': 'id'}, 'id': 'id'}, + {'snippet': {'name': 'Malay', 'hl': 'ms'}, 'id': 'ms'}, + {'snippet': {'name': 'Catalan', 'hl': 'ca'}, 'id': 'ca'}, + {'snippet': {'name': 'Czech', 'hl': 'cs'}, 'id': 'cs'}, + {'snippet': {'name': 'Danish', 'hl': 'da'}, 'id': 'da'}, + {'snippet': {'name': 'German', 'hl': 'de'}, 'id': 'de'}, + {'snippet': {'name': 'Estonian', 'hl': 'et'}, 'id': 'et'}, + {'snippet': {'name': 'English (United Kingdom)', 'hl': 'en-GB'}, 'id': 'en-GB'}, + {'snippet': {'name': 'English', 'hl': 'en'}, 'id': 'en'}, + {'snippet': {'name': 'Spanish (Spain)', 'hl': 'es'}, 'id': 'es'}, + {'snippet': {'name': 'Spanish (Latin America)', 'hl': 'es-419'}, 'id': 'es-419'}, + {'snippet': {'name': 'Basque', 'hl': 'eu'}, 'id': 'eu'}, + {'snippet': {'name': 'Filipino', 'hl': 'fil'}, 'id': 'fil'}, + {'snippet': {'name': 'French', 'hl': 'fr'}, 'id': 'fr'}, + {'snippet': {'name': 'French (Canada)', 'hl': 'fr-CA'}, 'id': 'fr-CA'}, + {'snippet': {'name': 'Galician', 'hl': 'gl'}, 'id': 'gl'}, + {'snippet': {'name': 'Croatian', 'hl': 'hr'}, 'id': 'hr'}, + {'snippet': {'name': 'Zulu', 'hl': 'zu'}, 'id': 'zu'}, + {'snippet': {'name': 'Icelandic', 'hl': 'is'}, 'id': 'is'}, + {'snippet': {'name': 'Italian', 'hl': 'it'}, 'id': 'it'}, + {'snippet': {'name': 'Swahili', 'hl': 'sw'}, 'id': 'sw'}, + {'snippet': {'name': 'Latvian', 'hl': 'lv'}, 'id': 'lv'}, + {'snippet': {'name': 'Lithuanian', 'hl': 'lt'}, 'id': 'lt'}, + {'snippet': {'name': 'Hungarian', 'hl': 'hu'}, 'id': 'hu'}, + {'snippet': {'name': 'Dutch', 'hl': 'nl'}, 'id': 'nl'}, + {'snippet': {'name': 'Norwegian', 'hl': 'no'}, 'id': 'no'}, + {'snippet': {'name': 'Uzbek', 'hl': 'uz'}, 'id': 'uz'}, + {'snippet': {'name': 'Polish', 'hl': 'pl'}, 'id': 'pl'}, + {'snippet': {'name': 'Portuguese (Portugal)', 'hl': 'pt-PT'}, 'id': 'pt-PT'}, + {'snippet': {'name': 'Portuguese (Brazil)', 'hl': 'pt'}, 'id': 'pt'}, + {'snippet': {'name': 'Romanian', 'hl': 'ro'}, 'id': 'ro'}, + {'snippet': {'name': 'Albanian', 'hl': 'sq'}, 'id': 'sq'}, + {'snippet': {'name': 'Slovak', 'hl': 'sk'}, 'id': 'sk'}, + {'snippet': {'name': 'Slovenian', 'hl': 'sl'}, 'id': 'sl'}, + {'snippet': {'name': 'Finnish', 'hl': 'fi'}, 'id': 'fi'}, + {'snippet': {'name': 'Swedish', 'hl': 'sv'}, 'id': 'sv'}, + {'snippet': {'name': 'Vietnamese', 'hl': 'vi'}, 'id': 'vi'}, + {'snippet': {'name': 'Turkish', 'hl': 'tr'}, 'id': 'tr'}, + {'snippet': {'name': 'Bulgarian', 'hl': 'bg'}, 'id': 'bg'}, + {'snippet': {'name': 'Kyrgyz', 'hl': 'ky'}, 'id': 'ky'}, + {'snippet': {'name': 'Kazakh', 'hl': 'kk'}, 'id': 'kk'}, + {'snippet': {'name': 'Macedonian', 'hl': 'mk'}, 'id': 'mk'}, + {'snippet': {'name': 'Mongolian', 'hl': 'mn'}, 'id': 'mn'}, + {'snippet': {'name': 'Russian', 'hl': 'ru'}, 'id': 'ru'}, + {'snippet': {'name': 'Serbian', 'hl': 'sr'}, 'id': 'sr'}, + {'snippet': {'name': 'Ukrainian', 'hl': 'uk'}, 'id': 'uk'}, + {'snippet': {'name': 'Greek', 'hl': 'el'}, 'id': 'el'}, + {'snippet': {'name': 'Armenian', 'hl': 'hy'}, 'id': 'hy'}, + {'snippet': {'name': 'Hebrew', 'hl': 'iw'}, 'id': 'iw'}, + {'snippet': {'name': 'Urdu', 'hl': 'ur'}, 'id': 'ur'}, + {'snippet': {'name': 'Arabic', 'hl': 'ar'}, 'id': 'ar'}, + {'snippet': {'name': 'Persian', 'hl': 'fa'}, 'id': 'fa'}, + {'snippet': {'name': 'Nepali', 'hl': 'ne'}, 'id': 'ne'}, + {'snippet': {'name': 'Marathi', 'hl': 'mr'}, 'id': 'mr'}, + {'snippet': {'name': 'Hindi', 'hl': 'hi'}, 'id': 'hi'}, + {'snippet': {'name': 'Bengali', 'hl': 'bn'}, 'id': 'bn'}, + {'snippet': {'name': 'Punjabi', 'hl': 'pa'}, 'id': 'pa'}, + {'snippet': {'name': 'Gujarati', 'hl': 'gu'}, 'id': 'gu'}, + {'snippet': {'name': 'Tamil', 'hl': 'ta'}, 'id': 'ta'}, + {'snippet': {'name': 'Telugu', 'hl': 'te'}, 'id': 'te'}, + {'snippet': {'name': 'Kannada', 'hl': 'kn'}, 'id': 'kn'}, + {'snippet': {'name': 'Malayalam', 'hl': 'ml'}, 'id': 'ml'}, + {'snippet': {'name': 'Sinhala', 'hl': 'si'}, 'id': 'si'}, + {'snippet': {'name': 'Thai', 'hl': 'th'}, 'id': 'th'}, + {'snippet': {'name': 'Lao', 'hl': 'lo'}, 'id': 'lo'}, + {'snippet': {'name': 'Myanmar (Burmese)', 'hl': 'my'}, 'id': 'my'}, + {'snippet': {'name': 'Georgian', 'hl': 'ka'}, 'id': 'ka'}, + {'snippet': {'name': 'Amharic', 'hl': 'am'}, 'id': 'am'}, + {'snippet': {'name': 'Khmer', 'hl': 'km'}, 'id': 'km'}, + {'snippet': {'name': 'Chinese', 'hl': 'zh-CN'}, 'id': 'zh-CN'}, + {'snippet': {'name': 'Chinese (Taiwan)', 'hl': 'zh-TW'}, 'id': 'zh-TW'}, + {'snippet': {'name': 'Chinese (Hong Kong)', 'hl': 'zh-HK'}, 'id': 'zh-HK'}, + {'snippet': {'name': 'Japanese', 'hl': 'ja'}, 'id': 'ja'}, + {'snippet': {'name': 'Korean', 'hl': 'ko'}, 'id': 'ko'}, +]} +DEFAULT_REGIONS = {'items': [ + {'snippet': {'gl': 'DZ', 'name': 'Algeria'}, 'id': 'DZ'}, + {'snippet': {'gl': 'AR', 'name': 'Argentina'}, 'id': 'AR'}, + {'snippet': {'gl': 'AU', 'name': 'Australia'}, 'id': 'AU'}, + {'snippet': {'gl': 'AT', 'name': 'Austria'}, 'id': 'AT'}, + {'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}, 'id': 'AZ'}, + {'snippet': {'gl': 'BH', 'name': 'Bahrain'}, 'id': 'BH'}, + {'snippet': {'gl': 'BY', 'name': 'Belarus'}, 'id': 'BY'}, + {'snippet': {'gl': 'BE', 'name': 'Belgium'}, 'id': 'BE'}, + {'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}, 'id': 'BA'}, + {'snippet': {'gl': 'BR', 'name': 'Brazil'}, 'id': 'BR'}, + {'snippet': {'gl': 'BG', 'name': 'Bulgaria'}, 'id': 'BG'}, + {'snippet': {'gl': 'CA', 'name': 'Canada'}, 'id': 'CA'}, + {'snippet': {'gl': 'CL', 'name': 'Chile'}, 'id': 'CL'}, + {'snippet': {'gl': 'CO', 'name': 'Colombia'}, 'id': 'CO'}, + {'snippet': {'gl': 'HR', 'name': 'Croatia'}, 'id': 'HR'}, + {'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}, 'id': 'CZ'}, + {'snippet': {'gl': 'DK', 'name': 'Denmark'}, 'id': 'DK'}, + {'snippet': {'gl': 'EG', 'name': 'Egypt'}, 'id': 'EG'}, + {'snippet': {'gl': 'EE', 'name': 'Estonia'}, 'id': 'EE'}, + {'snippet': {'gl': 'FI', 'name': 'Finland'}, 'id': 'FI'}, + {'snippet': {'gl': 'FR', 'name': 'France'}, 'id': 'FR'}, + {'snippet': {'gl': 'GE', 'name': 'Georgia'}, 'id': 'GE'}, + {'snippet': {'gl': 'DE', 'name': 'Germany'}, 'id': 'DE'}, + {'snippet': {'gl': 'GH', 'name': 'Ghana'}, 'id': 'GH'}, + {'snippet': {'gl': 'GR', 'name': 'Greece'}, 'id': 'GR'}, + {'snippet': {'gl': 'HK', 'name': 'Hong Kong'}, 'id': 'HK'}, + {'snippet': {'gl': 'HU', 'name': 'Hungary'}, 'id': 'HU'}, + {'snippet': {'gl': 'IS', 'name': 'Iceland'}, 'id': 'IS'}, + {'snippet': {'gl': 'IN', 'name': 'India'}, 'id': 'IN'}, + {'snippet': {'gl': 'ID', 'name': 'Indonesia'}, 'id': 'ID'}, + {'snippet': {'gl': 'IQ', 'name': 'Iraq'}, 'id': 'IQ'}, + {'snippet': {'gl': 'IE', 'name': 'Ireland'}, 'id': 'IE'}, + {'snippet': {'gl': 'IL', 'name': 'Israel'}, 'id': 'IL'}, + {'snippet': {'gl': 'IT', 'name': 'Italy'}, 'id': 'IT'}, + {'snippet': {'gl': 'JM', 'name': 'Jamaica'}, 'id': 'JM'}, + {'snippet': {'gl': 'JP', 'name': 'Japan'}, 'id': 'JP'}, + {'snippet': {'gl': 'JO', 'name': 'Jordan'}, 'id': 'JO'}, + {'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}, 'id': 'KZ'}, + {'snippet': {'gl': 'KE', 'name': 'Kenya'}, 'id': 'KE'}, + {'snippet': {'gl': 'KW', 'name': 'Kuwait'}, 'id': 'KW'}, + {'snippet': {'gl': 'LV', 'name': 'Latvia'}, 'id': 'LV'}, + {'snippet': {'gl': 'LB', 'name': 'Lebanon'}, 'id': 'LB'}, + {'snippet': {'gl': 'LY', 'name': 'Libya'}, 'id': 'LY'}, + {'snippet': {'gl': 'LT', 'name': 'Lithuania'}, 'id': 'LT'}, + {'snippet': {'gl': 'LU', 'name': 'Luxembourg'}, 'id': 'LU'}, + {'snippet': {'gl': 'MK', 'name': 'Macedonia'}, 'id': 'MK'}, + {'snippet': {'gl': 'MY', 'name': 'Malaysia'}, 'id': 'MY'}, + {'snippet': {'gl': 'MX', 'name': 'Mexico'}, 'id': 'MX'}, + {'snippet': {'gl': 'ME', 'name': 'Montenegro'}, 'id': 'ME'}, + {'snippet': {'gl': 'MA', 'name': 'Morocco'}, 'id': 'MA'}, + {'snippet': {'gl': 'NP', 'name': 'Nepal'}, 'id': 'NP'}, + {'snippet': {'gl': 'NL', 'name': 'Netherlands'}, 'id': 'NL'}, + {'snippet': {'gl': 'NZ', 'name': 'New Zealand'}, 'id': 'NZ'}, + {'snippet': {'gl': 'NG', 'name': 'Nigeria'}, 'id': 'NG'}, + {'snippet': {'gl': 'NO', 'name': 'Norway'}, 'id': 'NO'}, + {'snippet': {'gl': 'OM', 'name': 'Oman'}, 'id': 'OM'}, + {'snippet': {'gl': 'PK', 'name': 'Pakistan'}, 'id': 'PK'}, + {'snippet': {'gl': 'PE', 'name': 'Peru'}, 'id': 'PE'}, + {'snippet': {'gl': 'PH', 'name': 'Philippines'}, 'id': 'PH'}, + {'snippet': {'gl': 'PL', 'name': 'Poland'}, 'id': 'PL'}, + {'snippet': {'gl': 'PT', 'name': 'Portugal'}, 'id': 'PT'}, + {'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}, 'id': 'PR'}, + {'snippet': {'gl': 'QA', 'name': 'Qatar'}, 'id': 'QA'}, + {'snippet': {'gl': 'RO', 'name': 'Romania'}, 'id': 'RO'}, + {'snippet': {'gl': 'RU', 'name': 'Russia'}, 'id': 'RU'}, + {'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}, 'id': 'SA'}, + {'snippet': {'gl': 'SN', 'name': 'Senegal'}, 'id': 'SN'}, + {'snippet': {'gl': 'RS', 'name': 'Serbia'}, 'id': 'RS'}, + {'snippet': {'gl': 'SG', 'name': 'Singapore'}, 'id': 'SG'}, + {'snippet': {'gl': 'SK', 'name': 'Slovakia'}, 'id': 'SK'}, + {'snippet': {'gl': 'SI', 'name': 'Slovenia'}, 'id': 'SI'}, + {'snippet': {'gl': 'ZA', 'name': 'South Africa'}, 'id': 'ZA'}, + {'snippet': {'gl': 'KR', 'name': 'South Korea'}, 'id': 'KR'}, + {'snippet': {'gl': 'ES', 'name': 'Spain'}, 'id': 'ES'}, + {'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}, 'id': 'LK'}, + {'snippet': {'gl': 'SE', 'name': 'Sweden'}, 'id': 'SE'}, + {'snippet': {'gl': 'CH', 'name': 'Switzerland'}, 'id': 'CH'}, + {'snippet': {'gl': 'TW', 'name': 'Taiwan'}, 'id': 'TW'}, + {'snippet': {'gl': 'TZ', 'name': 'Tanzania'}, 'id': 'TZ'}, + {'snippet': {'gl': 'TH', 'name': 'Thailand'}, 'id': 'TH'}, + {'snippet': {'gl': 'TN', 'name': 'Tunisia'}, 'id': 'TN'}, + {'snippet': {'gl': 'TR', 'name': 'Turkey'}, 'id': 'TR'}, + {'snippet': {'gl': 'UG', 'name': 'Uganda'}, 'id': 'UG'}, + {'snippet': {'gl': 'UA', 'name': 'Ukraine'}, 'id': 'UA'}, + {'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}, 'id': 'AE'}, + {'snippet': {'gl': 'GB', 'name': 'United Kingdom'}, 'id': 'GB'}, + {'snippet': {'gl': 'US', 'name': 'United States'}, 'id': 'US'}, + {'snippet': {'gl': 'VN', 'name': 'Vietnam'}, 'id': 'VN'}, + {'snippet': {'gl': 'YE', 'name': 'Yemen'}, 'id': 'YE'}, + {'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}, 'id': 'ZW'}, +]} def _process_language(provider, context): - if not context.get_ui().on_yes_no_input(context.localize('setup_wizard.adjust'), - context.localize('setup_wizard.adjust.language_and_region')): + if not context.get_ui().on_yes_no_input( + context.localize('setup_wizard.adjust'), + context.localize('setup_wizard.adjust.language_and_region') + ): return client = provider.get_client(context) kodi_language = context.get_language() json_data = client.get_supported_languages(kodi_language) - if 'items' not in json_data: - items = DEFAULT_LANGUAGES['items'] - else: - items = json_data['items'] - language_list = [] + items = json_data.get('items') or DEFAULT_LANGUAGES['items'] invalid_ids = ['es-419'] # causes hl not a valid language error. Issue #418 - for item in items: - if item['id'] in invalid_ids: - continue - language_name = item['snippet']['name'] - hl = item['snippet']['hl'] - language_list.append((language_name, hl)) - language_list = sorted(language_list, key=lambda x: x[0]) + language_list = sorted([ + (item['snippet']['name'], item['snippet']['hl']) + for item in items + if item['id'] not in invalid_ids + ]) language_id = context.get_ui().on_select( - context.localize('setup_wizard.select_language'), language_list) + context.localize('setup_wizard.select_language'), + language_list, + ) if language_id == -1: return json_data = client.get_supported_regions(language=language_id) - if 'items' not in json_data: - items = DEFAULT_REGIONS['items'] - else: - items = json_data['items'] - region_list = [] - for item in items: - region_name = item['snippet']['name'] - gl = item['snippet']['gl'] - region_list.append((region_name, gl)) - region_list = sorted(region_list, key=lambda x: x[0]) - region_id = context.get_ui().on_select(context.localize('setup_wizard.select_region'), - region_list) + items = json_data.get('items') or DEFAULT_REGIONS['items'] + region_list = sorted([ + (item['snippet']['name'], item['snippet']['gl']) + for item in items + ]) + region_id = context.get_ui().on_select( + context.localize('setup_wizard.select_region'), + region_list, + ) if region_id == -1: return @@ -108,14 +230,16 @@ def _process_language(provider, context): def _process_geo_location(context): - if not context.get_ui().on_yes_no_input(context.get_name(), context.localize('perform_geolocation')): + if not context.get_ui().on_yes_no_input( + context.get_name(), context.localize('perform_geolocation') + ): return locator = Locator() locator.locate_requester() - coordinates = locator.coordinates() - if coordinates: - context.get_settings().set_location('{0[lat]},{0[lon]}'.format(coordinates)) + coords = locator.coordinates() + if coords: + context.get_settings().set_location('{0[lat]},{0[lon]}'.format(coords)) def process(provider, context): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index f388a5d84..f63ad1fa5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -30,6 +30,7 @@ def _process_related_videos(provider, context): _refresh=params.get('refresh'), video_id=video_id, page_token=params.get('page_token', ''), + offset=params.get('offset', 0), ) else: json_data = function_cache.run( diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index 2d40944b4..348ab5217 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -29,7 +29,7 @@ def _process_list(provider, context): def _process_add(provider, context): - listitem_subscription_id = context.get_infolabel('Container.ListItem(0).Property(subscription_id)') + listitem_subscription_id = context.get_listitem_detail('subscription_id') subscription_id = context.get_param('subscription_id', '') if (not subscription_id and listitem_subscription_id @@ -53,28 +53,34 @@ def _process_add(provider, context): def _process_remove(provider, context): - listitem_subscription_id = context.get_infolabel('Container.ListItem(0).Property(channel_subscription_id)') + listitem_subscription_id = context.get_listitem_detail('channel_subscription_id') + listitem_channel_id = context.get_listitem_detail('channel_id') subscription_id = context.get_param('subscription_id', '') if not subscription_id and listitem_subscription_id: subscription_id = listitem_subscription_id - if subscription_id: - json_data = provider.get_client(context).unsubscribe(subscription_id) - if not json_data: - return False - - context.get_ui().refresh_container() - - context.get_ui().show_notification( - context.localize('unsubscribed.from.channel'), - time_ms=2500, - audible=False - ) - - return True + channel_id = context.get_param('channel_id', '') + if not channel_id and listitem_channel_id: + channel_id = listitem_channel_id - return False + if subscription_id: + success = provider.get_client(context).unsubscribe(subscription_id) + elif channel_id: + success = provider.get_client(context).unsubscribe_channel(channel_id) + else: + success = False + + if not success: + return False + + context.get_ui().refresh_container() + context.get_ui().show_notification( + context.localize('unsubscribed.from.channel'), + time_ms=2500, + audible=False + ) + return True def process(method, provider, context): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index c1e3bd527..2085d11e3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -16,7 +16,7 @@ def _process_rate_video(provider, context, re_match): - listitem_path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + listitem_path = context.get_listitem_detail('FileNameAndPath', attr=True) ratings = ['like', 'dislike', 'none'] rating_param = context.get_param('rating', '') diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 52e3f1ce9..d083d1ccd 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -385,9 +385,7 @@ def _on_channel_live(self, context, re_match): @RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$') def _on_channel(self, context, re_match): - listitem_channel_id = context.get_infolabel( - 'Container.ListItem(0).Property(channel_id)' - ) + listitem_channel_id = context.get_listitem_detail('channel_id') client = self.get_client(context) localize = context.localize @@ -591,7 +589,7 @@ def on_play(self, context, re_match): if ({'channel_id', 'live', 'playlist_id', 'playlist_ids', 'video_id'} .isdisjoint(params.keys())): - path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + path = context.get_listitem_detail('FileNameAndPath', attr=True) if context.is_plugin_path(path, 'play/'): video_id = find_video_id(path) if video_id: diff --git a/resources/settings.xml b/resources/settings.xml index fbe786f5c..45e7a332d 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -263,7 +263,14 @@ 0 - avc1,vp9,av01,hdr,hfr,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter + + avc1,vp9,av01,hdr,hfr,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter @@ -272,6 +279,8 @@ + +