diff --git a/addon.xml b/addon.xml index 82080ea3b..a93428c05 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 5d76f7b58..8ef55cb1d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +## v7.0.9+beta.6 +### Fixed +- Fix http server not working in Kodi 18 +- Fix issues with http server wakeup and sleep on initial start/restart + +### Changed +- Player requests no longer use configured plugin language +- Alternate client selections removed + ## v7.0.9+beta.5 ### Fixed - Fix disabling certification verification #841 diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index f3ed9d9b0..a0a47974e 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1254,15 +1254,15 @@ msgid "Shorts (1 minute or less)" msgstr "" msgctxt "#30737" -msgid "Use alternate client details" +msgid "" msgstr "" msgctxt "#30738" -msgid "Alternate #1" +msgid "" msgstr "" msgctxt "#30739" -msgid "Alternate #2" +msgid "" msgstr "" msgctxt "#30740" diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 93975ffb1..b3892d0d2 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -259,10 +259,11 @@ def reroute(self, context, path=None, params=None, uri=None): if not result: return False context.get_ui().set_property(REROUTE_PATH, path) - context.execute('ActivateWindow(Videos, {0}{1})'.format( + context.execute(''.join(( + 'ActivateWindow(Videos, ', context.create_uri(path, params), - ', return' if window_return else '', - )) + ', return)' if window_return else ')', + ))) return True @staticmethod @@ -364,7 +365,7 @@ def on_search(provider, context, re_match): @staticmethod def on_command(re_match, **_kwargs): command = re_match.group('command') - return UriItem('command://{0}'.format(command)) + return UriItem(''.join(('command://', command))) def handle_exception(self, context, exception_to_handle): return True diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index c02f1d1d2..68b624bfa 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -18,7 +18,7 @@ MPD_VIDEOS = 'kodion.mpd.videos' # (bool) MPD_STREAM_SELECT = 'kodion.mpd.stream.select' # (int) MPD_QUALITY_SELECTION = 'kodion.mpd.quality.selection' # (int) -MPD_STREAM_FEATURES = 'kodion.mpd.stream.features' # (list[string]) +MPD_STREAM_FEATURES = 'kodion.mpd.stream.features' # (list[str]) VIDEO_QUALITY_ASK = 'kodion.video.quality.ask' # (bool) VIDEO_QUALITY = 'kodion.video.quality' # (int) AUDIO_ONLY = 'kodion.audio_only' # (bool) @@ -27,21 +27,20 @@ SUBTITLE_DOWNLOAD = 'kodion.subtitle.download' # (bool) ITEMS_PER_PAGE = 'kodion.content.max_per_page' # (int) -HIDE_VIDEOS = 'youtube.view.hide_videos' # (list[string]) +HIDE_VIDEOS = 'youtube.view.hide_videos' # (list[str]) SAFE_SEARCH = 'kodion.safe.search' # (int) AGE_GATE = 'kodion.age.gate' # (bool) API_CONFIG_PAGE = 'youtube.api.config.page' # (bool) -API_KEY = 'youtube.api.key' # (string) -API_ID = 'youtube.api.id' # (string) -API_SECRET = 'youtube.api.secret' # (string) +API_KEY = 'youtube.api.key' # (str) +API_ID = 'youtube.api.id' # (str) +API_SECRET = 'youtube.api.secret' # (str) ALLOW_DEV_KEYS = 'youtube.allow.dev.keys' # (bool) WATCH_LATER_PLAYLIST = 'youtube.folder.watch_later.playlist' # (str) HISTORY_PLAYLIST = 'youtube.folder.history.playlist' # (str) -CLIENT_SELECTION = 'youtube.client.selection' # (int) SUPPORT_ALTERNATIVE_PLAYER = 'kodion.support.alternative_player' # (bool) DEFAULT_PLAYER_WEB_URLS = 'kodion.default_player.web_urls' # (bool) ALTERNATIVE_PLAYER_WEB_URLS = 'kodion.alternative_player.web_urls' # (bool) @@ -56,10 +55,10 @@ SEARCH_SIZE = 'kodion.search.size' # (int) CACHE_SIZE = 'kodion.cache.size' # (int) -CHANNEL_NAME_ALIASES = 'youtube.view.channel_name.aliases' # (list[string]) +CHANNEL_NAME_ALIASES = 'youtube.view.channel_name.aliases' # (list[str]) DETAILED_DESCRIPTION = 'youtube.view.description.details' # (bool) DETAILED_LABELS = 'youtube.view.label.details' # (bool) -LABEL_COLOR = 'youtube.view.label.color' # (string) +LABEL_COLOR = 'youtube.view.label.color' # (str) THUMB_SIZE = 'kodion.thumbnail.size' # (int) THUMB_SIZE_BEST = 2 @@ -78,7 +77,7 @@ CONNECT_TIMEOUT = 'requests.timeout.connect' # (int) READ_TIMEOUT = 'requests.timeout.read' # (int) -HTTPD_PORT = 'kodion.http.port' # (number) -HTTPD_LISTEN = 'kodion.http.listen' # (string) -HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (string) +HTTPD_PORT = 'kodion.http.port' # (int) +HTTPD_LISTEN = 'kodion.http.listen' # (str) +HTTPD_WHITELIST = 'kodion.http.ip.whitelist' # (str) HTTPD_IDLE_SLEEP = 'youtube.http.idle_sleep' # (bool) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 1fba2f4f0..ec0e824c3 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -257,7 +257,7 @@ def get_ui(self): def get_system_version(): return current_system_version - def create_uri(self, path=None, params=None): + def create_uri(self, path=None, params=None, run=False): if isinstance(path, (list, tuple)): uri = self.create_path(*path, is_uri=True) elif path: @@ -270,7 +270,11 @@ def create_uri(self, path=None, params=None): if params: uri = '?'.join((uri, urlencode(params))) - return uri + return ''.join(( + 'RunPlugin(', + uri, + ')' + )) if run else uri @staticmethod def create_path(*args, **kwargs): diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index ddd8ad333..076836681 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -179,15 +179,24 @@ def wrapper(*args, **kwargs): class_name = args[0].__name__ else: class_name = args[0].__class__.__name__ - name = '{0}.{1}'.format(class_name, func.__name__) + name = '.'.join(( + class_name, + func.__name__, + )) elif (func.__class__ and not isinstance(func.__class__, type) and func.__class__.__name__ != 'function'): - name = '{0}.{1}'.format(func.__class__.__name__, func.__name__) + name = '.'.join(( + func.__class__.__name__, + func.__name__, + )) elif func.__module__: - name = '{0}.{1}'.format(func.__module__, func.__name__) + name = '.'.join(( + func.__module__, + func.__name__, + )) else: name = func.__name__ diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index c193e28bd..3e44fc11a 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -115,7 +115,10 @@ def set_fanart(self, fanart): def get_fanart(self, default=True): if self._fanart or not default: return self._fanart - return '{0}/fanart.jpg'.format(MEDIA_PATH) + return '/'.join(( + MEDIA_PATH, + 'fanart.jpg', + )) def add_context_menu(self, context_menu, position='end', replace=False): context_menu = (item for item in context_menu if item) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 2e3ac4269..e8999fa71 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -28,59 +28,64 @@ def more_for_video(context, video_id, logged_in=False, refresh=False): params['refresh'] = context.get_param('refresh', 0) + 1 return ( context.localize('video.more'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('video', 'more',), params, - )) + run=True, + ), ) def related_videos(context, video_id): return ( context.localize('related_videos'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.ROUTE, 'special', 'related_videos',), { 'video_id': video_id, }, - )) + run=True, + ), ) def video_comments(context, video_id): return ( context.localize('video.comments'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.ROUTE, 'special', 'parent_comments',), { 'video_id': video_id, }, - )) + run=True, + ) ) def content_from_description(context, video_id): return ( context.localize('video.description.links'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.ROUTE, 'special', 'description_links',), { 'video_id': video_id, }, - )) + run=True, + ) ) def play_with(context, video_id): return ( context.localize('video.play.with'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.PLAY,), { 'video_id': video_id, PLAY_WITH: True, }, - )) + run=True, + ), ) @@ -88,10 +93,11 @@ def refresh(context): params = context.get_params() return ( context.localize('refresh'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.ROUTE, context.get_path(),), dict(params, refresh=params.get('refresh', 0) + 1), - )) + run=True, + ), ) @@ -106,43 +112,46 @@ def play_all_from_playlist(context, playlist_id, video_id=''): if video_id: return ( context.localize('playlist.play.from_here'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.PLAY,), { 'playlist_id': playlist_id, 'video_id': video_id, 'play': True, }, - )) + run=True, + ), ) return ( context.localize('playlist.play.all'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.PLAY,), { 'playlist_id': playlist_id, 'play': True, }, - )) + run=True, + ), ) def add_video_to_playlist(context, video_id): return ( context.localize('video.add_to_playlist'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'select', 'playlist',), { 'video_id': video_id, }, - )) + run=True, + ), ) def remove_video_from_playlist(context, playlist_id, video_id, video_name): return ( context.localize('remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'remove', 'video',), dict( context.get_params(), @@ -151,111 +160,120 @@ def remove_video_from_playlist(context, playlist_id, video_id, video_name): video_name=video_name, reload_path=context.get_path(), ), - )) + run=True, + ), ) def rename_playlist(context, playlist_id, playlist_name): return ( context.localize('rename'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'rename', 'playlist',), { 'playlist_id': playlist_id, 'playlist_name': playlist_name }, - )) + run=True, + ), ) def delete_playlist(context, playlist_id, playlist_name): return ( context.localize('delete'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'remove', 'playlist',), { 'playlist_id': playlist_id, 'playlist_name': playlist_name }, - )) + run=True, + ), ) def remove_as_watch_later(context, playlist_id, playlist_name): return ( context.localize('watch_later.list.remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'remove', 'watch_later',), { 'playlist_id': playlist_id, 'playlist_name': playlist_name }, - )) + run=True, + ), ) def set_as_watch_later(context, playlist_id, playlist_name): return ( context.localize('watch_later.list.set'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'set', 'watch_later',), { 'playlist_id': playlist_id, 'playlist_name': playlist_name }, - )) + run=True, + ), ) def remove_as_history(context, playlist_id, playlist_name): return ( context.localize('history.list.remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'remove', 'history',), { 'playlist_id': playlist_id, 'playlist_name': playlist_name }, - )) + run=True, + ), ) def set_as_history(context, playlist_id, playlist_name): return ( context.localize('history.list.set'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'set', 'history',), { 'playlist_id': playlist_id, 'playlist_name': playlist_name }, - )) + run=True, + ), ) def remove_my_subscriptions_filter(context, channel_name): return ( context.localize('my_subscriptions.filter.remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('my_subscriptions', 'filter',), { 'channel_name': channel_name, 'action': 'remove' }, - )) + run=True, + ), ) def add_my_subscriptions_filter(context, channel_name): return ( context.localize('my_subscriptions.filter.add'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('my_subscriptions', 'filter',), { 'channel_name': channel_name, 'action': 'add', }, - )) + run=True, + ), ) @@ -267,66 +285,72 @@ def rate_video(context, video_id, refresh=False): params['refresh'] = context.get_param('refresh', 0) + 1 return ( context.localize('video.rate'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('video', 'rate',), params, - )) + run=True, + ), ) def watch_later_add(context, playlist_id, video_id): return ( context.localize('watch_later.add'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('playlist', 'add', 'video',), { 'playlist_id': playlist_id, 'video_id': video_id, }, - )) + run=True, + ), ) def watch_later_local_add(context, item): return ( context.localize('watch_later.add'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.WATCH_LATER, 'add',), { 'video_id': item.video_id, 'item': repr(item), }, - )) + run=True, + ), ) def watch_later_local_remove(context, video_id): return ( context.localize('watch_later.remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.WATCH_LATER, 'remove',), { 'video_id': video_id, }, - )) + run=True, + ), ) def watch_later_local_clear(context): return ( context.localize('watch_later.clear'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.WATCH_LATER, 'clear',), - )) + run=True, + ), ) def go_to_channel(context, channel_id, channel_name): return ( context.localize('go_to_channel') % context.get_ui().bold(channel_name), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.ROUTE, 'channel', channel_id,), - )) + run=True, + ), ) @@ -335,146 +359,158 @@ def subscribe_to_channel(context, channel_id, 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( + context.create_uri( ('subscriptions', 'add',), { 'subscription_id': channel_id, }, - )) + run=True, + ), ) def unsubscribe_from_channel(context, channel_id=None, subscription_id=None): return ( context.localize('unsubscribe'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( ('subscriptions', 'remove',), { 'subscription_id': subscription_id, }, - )) if subscription_id else - 'RunPlugin({0})'.format(context.create_uri( + run=True, + ) if subscription_id else + context.create_uri( ('subscriptions', 'remove',), { 'channel_id': channel_id, }, - )) + run=True, + ), ) def play_with_subtitles(context, video_id): return ( context.localize('video.play.with_subtitles'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.PLAY,), { 'video_id': video_id, PLAY_PROMPT_SUBTITLES: True, }, - )) + run=True, + ), ) def play_audio_only(context, video_id): return ( context.localize('video.play.audio_only'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.PLAY,), { 'video_id': video_id, PLAY_FORCE_AUDIO: True, }, - )) + run=True, + ), ) def play_ask_for_quality(context, video_id): return ( context.localize('video.play.ask_for_quality'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.PLAY,), { 'video_id': video_id, PLAY_PROMPT_QUALITY: True, }, - )) + run=True, + ), ) def history_remove(context, video_id): return ( context.localize('history.remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.HISTORY,), { 'action': 'remove', 'video_id': video_id }, - )) + run=True, + ), ) def history_clear(context): return ( context.localize('history.clear'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.HISTORY,), { 'action': 'clear' }, - )) + run=True, + ), ) def history_mark_watched(context, video_id): return ( context.localize('history.mark.watched'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.HISTORY,), { 'video_id': video_id, 'action': 'mark_watched', }, - )) + run=True, + ), ) def history_mark_unwatched(context, video_id): return ( context.localize('history.mark.unwatched'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.HISTORY,), { 'video_id': video_id, 'action': 'mark_unwatched', }, - )) + run=True, + ), ) def history_reset_resume(context, video_id): return ( context.localize('history.reset.resume_point'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.HISTORY,), { 'video_id': video_id, 'action': 'reset_resume', }, - )) + run=True, + ), ) def bookmark_add(context, item): return ( context.localize('bookmark'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.BOOKMARKS, 'add',), { 'item_id': item.get_id(), 'item': repr(item), }, - )) + run=True, + ), ) @@ -484,67 +520,73 @@ def bookmark_add_channel(context, channel_id, channel_name=''): context.get_ui().bold(channel_name) if channel_name else context.localize(19029) )), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.BOOKMARKS, 'add',), { 'item_id': channel_id, 'item': None, }, - )) + run=True, + ), ) def bookmark_remove(context, item_id): return ( context.localize('bookmark.remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.BOOKMARKS, 'remove',), { 'item_id': item_id, }, - )) + run=True, + ), ) def bookmarks_clear(context): return ( context.localize('bookmarks.clear'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.BOOKMARKS, 'clear',), - )) + run=True, + ), ) def search_remove(context, query): return ( context.localize('search.remove'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.SEARCH, 'remove',), { 'q': query, }, - )) + run=True, + ), ) def search_rename(context, query): return ( context.localize('search.rename'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.SEARCH, 'rename',), { 'q': query, }, - )) + run=True, + ), ) def search_clear(context): return ( context.localize('search.clear'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.SEARCH, 'clear',), - )) + run=True, + ), ) @@ -558,29 +600,32 @@ def separator(): def goto_home(context): return ( context.localize(10000), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.ROUTE, PATHS.HOME,), { 'window_return': False, }, - )) + run=True, + ), ) def goto_quick_search(context): return ( context.localize('search.quick'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.ROUTE, PATHS.SEARCH, 'input',), - )) + run=True, + ), ) def goto_page(context, params=None): return ( context.localize('page.choose'), - 'RunPlugin({0})'.format(context.create_uri( + context.create_uri( (PATHS.GOTO_PAGE, context.get_path(),), params or context.get_params(), - )) + run=True, + ), ) diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index 5a35fffaf..b0cfc33d1 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -106,8 +106,9 @@ def onNotification(self, sender, method, data): self.interrupt = True elif target == SERVER_WAKEUP: if not self.httpd and self.httpd_required(): - self.httpd_sleep_allowed = None self.start_httpd() + if self.httpd_sleep_allowed: + self.httpd_sleep_allowed = None if data.get('response_required'): self.set_property(WAKEUP, target) elif event == REFRESH_CONTAINER: diff --git a/resources/lib/youtube_plugin/kodion/network/http_server.py b/resources/lib/youtube_plugin/kodion/network/http_server.py index e11141b2c..58c7f0e6d 100644 --- a/resources/lib/youtube_plugin/kodion/network/http_server.py +++ b/resources/lib/youtube_plugin/kodion/network/http_server.py @@ -43,7 +43,7 @@ def server_close(self): self.socket.close() -class RequestHandler(BaseHTTPRequestHandler): +class RequestHandler(BaseHTTPRequestHandler, object): _context = None requests = None BASE_PATH = xbmcvfs.translatePath(TEMP_PATH) @@ -104,8 +104,7 @@ def do_GET(self): self.send_error(403) elif stripped_path == PATHS.IP: - client_json = json.dumps({"ip": "{ip}" - .format(ip=self.client_address[0])}) + client_json = json.dumps({'ip': self.client_address[0]}) self.send_response(200) self.send_header('Content-Type', 'application/json; charset=utf-8') self.send_header('Content-Length', str(len(client_json))) @@ -579,9 +578,13 @@ def get_http_server(address, port, context): def httpd_status(context): address, port = get_connect_address(context) - url = 'http://{address}:{port}{path}'.format(address=address, - port=port, - path=PATHS.PING) + url = ''.join(( + 'http://', + address, + ':', + str(port), + PATHS.IP, + )) if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=context) response = RequestHandler.requests.request(url) @@ -599,9 +602,13 @@ def httpd_status(context): def get_client_ip_address(context): ip_address = None address, port = get_connect_address(context) - url = 'http://{address}:{port}{path}'.format(address=address, - port=port, - path=PATHS.IP) + url = ''.join(( + 'http://', + address, + ':', + str(port), + PATHS.IP, + )) if not RequestHandler.requests: RequestHandler.requests = BaseRequestsClass(context=context) response = RequestHandler.requests.request(url) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 3e51fa107..e6baeafac 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -426,11 +426,6 @@ def item_filter(self, update=None): types.update(update) return types - def client_selection(self, value=None): - if value is not None: - return self.set_int(SETTINGS.CLIENT_SELECTION, value) - return self.get_int(SETTINGS.CLIENT_SELECTION, 0) - def show_detailed_description(self, value=None): if value is not None: return self.set_bool(SETTINGS.DETAILED_DESCRIPTION, value) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 2997a7b03..b46887155 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -58,15 +58,10 @@ def to_unicode(text): def select_stream(context, stream_data_list, - ask_for_quality=None, - audio_only=None, + ask_for_quality, + audio_only, use_adaptive_formats=True): settings = context.get_settings() - if ask_for_quality is None: - ask_for_quality = settings.ask_for_video_quality() - if audio_only is None: - audio_only = settings.audio_only() - isa_capabilities = context.inputstream_adaptive_capabilities() use_adaptive = (use_adaptive_formats and settings.use_isa() diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index fdec14976..c7b5f87b4 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -82,13 +82,12 @@ def __init__(self, context): switch=switch)) if changed: self._context.log_debug('API key set changed: Signing out') - self._context.execute('RunPlugin({0})'.format( - self._context.create_uri( - ('sign', 'out'), - { - 'confirmed': True, - } - ) + self._context.execute(self._context.create_uri( + ('sign', 'out'), + { + 'confirmed': True, + }, + run=True, )) self._access_manager.set_last_key_hash(current_hash) diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 465cc3222..2279dc63d 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -15,32 +15,31 @@ class YouTubeRequestClient(BaseRequestsClass): - _ANDROID_PARAMS = 'CgIIAdgDAQ==' - # yt-dlp has chosen the following value, but this results in the android - # player response returning unexpected details sometimes. To be investigated - # _ANDROID_PARAMS = 'CgIIAQ==' _API_KEYS = { 'android': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', 'android_embedded': 'AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw', 'ios': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc', + 'smart_tv': 'AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8', 'web': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', } + _PLAYER_PARAMS = { + 'android': 'CgIIAdgDAQ==', + 'android_testsuite': '2AMB', + } CLIENTS = { - # 4k no VP9 HDR - # Limited subtitle availability - 'android_testsuite': { - '_id': 30, - '_query_subtitles': True, + 'android': { + '_id': 3, + '_disabled': True, + '_query_subtitles': 'optional', 'json': { - 'params': _ANDROID_PARAMS, 'context': { 'client': { - 'clientName': 'ANDROID_TESTSUITE', - 'clientVersion': '1.9', - 'androidSdkVersion': '34', + 'clientName': 'ANDROID', + 'clientVersion': '19.29.37', + 'androidSdkVersion': '30', 'osName': 'Android', - 'osVersion': '14', + 'osVersion': '11', 'platform': 'MOBILE', }, }, @@ -49,76 +48,68 @@ class YouTubeRequestClient(BaseRequestsClass): 'User-Agent': ('com.google.android.youtube/' '{json[context][client][clientVersion]}' ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), + ' {json[context][client][osVersion]}) gzip'), 'X-YouTube-Client-Name': '{_id}', 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', }, - 'params': { - 'key': _API_KEYS['android'], - }, }, - 'android': { - '_id': 3, + # Only for videos that allow embedding + # Limited to 720p on some videos + 'android_embedded': { + '_id': 55, '_disabled': True, '_query_subtitles': 'optional', 'json': { 'context': { 'client': { - 'clientName': 'ANDROID', - 'clientVersion': '19.17.34', - 'androidSdkVersion': '34', + 'clientName': 'ANDROID_EMBEDDED_PLAYER', + 'clientScreen': 'EMBED', + 'clientVersion': '19.29.37', + 'androidSdkVersion': '30', 'osName': 'Android', - 'osVersion': '14', + 'osVersion': '11', 'platform': 'MOBILE', }, }, + 'thirdParty': { + 'embedUrl': 'https://www.youtube.com/embed/{json[videoId]}', + }, }, 'headers': { 'User-Agent': ('com.google.android.youtube/' '{json[context][client][clientVersion]}' ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), + ' {json[context][client][osVersion]}) gzip'), 'X-YouTube-Client-Name': '{_id}', 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', }, }, - # Only for videos that allow embedding - # Limited to 720p on some videos - 'android_embedded': { - '_id': 55, - '_disabled': True, - '_query_subtitles': 'optional', + # 4k no VP9 HDR + # Limited subtitle availability + 'android_testsuite': { + '_id': 30, + '_query_subtitles': True, 'json': { - 'params': _ANDROID_PARAMS, + 'params': _PLAYER_PARAMS['android_testsuite'], 'context': { 'client': { - 'clientName': 'ANDROID_EMBEDDED_PLAYER', - 'clientScreen': 'EMBED', - 'clientVersion': '19.17.34', - 'androidSdkVersion': '34', + 'clientName': 'ANDROID_TESTSUITE', + 'clientVersion': '1.9', + 'androidSdkVersion': '30', 'osName': 'Android', - 'osVersion': '14', + 'osVersion': '11', 'platform': 'MOBILE', }, }, - 'thirdParty': { - 'embedUrl': 'https://www.youtube.com/embed/{json[videoId]}', - }, }, 'headers': { 'User-Agent': ('com.google.android.youtube/' '{json[context][client][clientVersion]}' ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), + ' {json[context][client][osVersion]}) gzip'), 'X-YouTube-Client-Name': '{_id}', 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', }, - 'params': { - 'key': _API_KEYS['android_embedded'], - }, }, # 4k with HDR # Some videos block this client, may also require embedding enabled @@ -127,14 +118,14 @@ class YouTubeRequestClient(BaseRequestsClass): '_id': 29, '_query_subtitles': True, 'json': { - 'params': _ANDROID_PARAMS, + 'params': _PLAYER_PARAMS['android'], 'context': { 'client': { 'clientName': 'ANDROID_UNPLUGGED', 'clientVersion': '8.12.0', - 'androidSdkVersion': '34', + 'androidSdkVersion': '30', 'osName': 'Android', - 'osVersion': '14', + 'osVersion': '11', 'platform': 'MOBILE', }, }, @@ -143,28 +134,25 @@ class YouTubeRequestClient(BaseRequestsClass): 'User-Agent': ('com.google.android.apps.youtube.unplugged/' '{json[context][client][clientVersion]}' ' (Linux; U; {json[context][client][osName]}' - ' {json[context][client][osVersion]};' - ' {json[context][client][gl]}) gzip'), + ' {json[context][client][osVersion]}) gzip'), 'X-YouTube-Client-Name': '{_id}', 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', }, - 'params': { - 'key': _API_KEYS['android'], - }, }, 'ios': { '_id': 5, '_os': { 'major': '17', - 'minor': '4', + 'minor': '5', 'patch': '1', - 'build': '21E236', + 'build': '21F90', }, 'json': { 'context': { 'client': { 'clientName': 'IOS', - 'clientVersion': '19.16.3', + 'clientVersion': '19.29.1', + 'deviceMake': 'Apple', 'deviceModel': 'iPhone16,2', 'osName': 'iOS', 'osVersion': '{_os[major]}.{_os[minor]}.{_os[patch]}.{_os[build]}', @@ -183,9 +171,22 @@ class YouTubeRequestClient(BaseRequestsClass): 'X-YouTube-Client-Version': '{json[context][client][clientVersion]}', }, }, + 'media_connect_frontend': { + '_id': 95, + '_query_subtitles': True, + 'json': { + 'context': { + 'client': { + 'clientName': 'MEDIA_CONNECT_FRONTEND', + 'clientVersion': '0.1', + }, + }, + }, + 'headers': {}, + }, # Used to requests captions for clients that don't provide them # Requires handling of nsig to overcome throttling (TODO) - 'smarttv_embedded': { + 'smart_tv_embedded': { '_id': 85, 'json': { 'context': { @@ -205,25 +206,6 @@ class YouTubeRequestClient(BaseRequestsClass): ' AppleWebKit/537.36 (KHTML, like Gecko)' ' 85.0.4183.93/6.5 TV Safari/537.36'), }, - 'params': { - 'key': _API_KEYS['web'], - }, - }, - 'media_connect_frontend': { - '_id': 95, - '_query_subtitles': True, - 'json': { - 'context': { - 'client': { - 'clientName': 'MEDIA_CONNECT_FRONTEND', - 'clientVersion': '0.1', - }, - }, - }, - 'headers': {}, - 'params': { - 'key': _API_KEYS['web'], - }, }, # Used for misc api requests by default # Requires handling of nsig to overcome throttling (TODO) @@ -233,7 +215,7 @@ class YouTubeRequestClient(BaseRequestsClass): 'context': { 'client': { 'clientName': 'WEB', - 'clientVersion': '2.20240304.00.00', + 'clientVersion': '2.20240726.00.00', }, }, }, @@ -245,9 +227,6 @@ class YouTubeRequestClient(BaseRequestsClass): ' Chrome/80.0.3987.162 Mobile Safari/537.36'), 'Referer': 'https://www.youtube.com/watch?v={json[videoId]}' }, - 'params': { - 'key': _API_KEYS['web'], - }, }, '_common': { '_access_token': None, @@ -374,10 +353,15 @@ def build_client(cls, client_name=None, data=None): client['_name'] = client_name try: + params = client['params'] if client.get('_access_token'): - del client['params']['key'] - elif 'Authorization' in client['headers']: - del client['headers']['Authorization'] + if 'key' in params: + del params['key'] + else: + if 'Authorization' in client['headers']: + del client['headers']['Authorization'] + if 'key' in params and not params['key']: + del params['key'] except KeyError: pass diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index b33a085d0..f467d11f5 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -45,9 +45,6 @@ class YouTube(LoginClient): 'headers': { 'Host': 'www.youtube.com', }, - 'params': { - 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', - }, }, 3: { 'url': 'https://www.googleapis.com/youtube/v3/{_endpoint}', @@ -70,9 +67,6 @@ class YouTube(LoginClient): 'headers': { 'Host': 'www.youtube.com', }, - 'params': { - 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', - }, }, 'tv_embed': { 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', @@ -88,9 +82,6 @@ class YouTube(LoginClient): 'headers': { 'Host': 'www.youtube.com', }, - 'params': { - 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', - }, }, '_common': { '_access_token': None, @@ -122,7 +113,6 @@ class YouTube(LoginClient): ' Chrome/80.0.3987.162 Mobile Safari/537.36'), }, 'params': { - 'key': None, 'prettyPrint': 'false' }, }, @@ -195,11 +185,17 @@ def update_watch_history(self, context, video_id, url, status=None): self.request(url, params=params, headers=headers, error_msg='Failed to update watch history') - def get_streams(self, context, video_id, audio_only=False): + def get_streams(self, + context, + video_id, + ask_for_quality=False, + audio_only=False, + use_mpd=True): return StreamInfo(context, access_token=self._access_token_tv, + ask_for_quality=ask_for_quality, audio_only=audio_only, - language=self._language).load_stream_infos(video_id) + use_mpd=use_mpd).load_stream_info(video_id) def remove_playlist(self, playlist_id, **kwargs): params = {'id': playlist_id, diff --git a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py index f6f59b0a7..b5aec8f0c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py +++ b/resources/lib/youtube_plugin/youtube/helper/signature/cipher.py @@ -18,7 +18,6 @@ class Cipher(object): def __init__(self, context, javascript): self._context = context - self._verify = context.get_settings().verify_ssl() self._javascript = javascript self._object_cache = {} diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index eea0b3c36..aeedbf1b3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -684,61 +684,44 @@ def __init__(self, context, access_token='', clients=None, + ask_for_quality=False, audio_only=False, + use_mpd=True, **kwargs): self.video_id = None self._context = context - self._language_base = kwargs.get('language', 'en_US')[0:2] + self._access_token = access_token + self._ask_for_quality = ask_for_quality self._audio_only = audio_only + self._language_base = kwargs.get('language', 'en_US')[0:2] + self._use_mpd = use_mpd + self._player_js = None self._calculate_n = True self._cipher = None self._selected_client = None - client_selection = context.get_settings().client_selection() - self._prioritised_clients = clients if clients else () - - # Default client selection uses the Android or iOS client as the first - # option to ensure that the age gate setting is enforced, regardless of - # login status - - # Alternate #1 - # Prefer iOS client to access premium streams, however other stream - # types are limited - if client_selection == 1: - self._prioritised_clients += ( + self._client_groups = { + 'custom': clients if clients else (), + # Access "premium" streams, HLS and DASH + # Limited video stream availability + 'default': ( 'ios', - 'android', + ), + # Will play most videos with subtitles at full resolution with HDR + # Some restricted videos require additional requests for subtitles + # Limited audio stream availability + 'mpd': ( 'android_youtube_tv', 'android_testsuite', + ), + # Progressive streams + # Limited video and audio stream availability + 'ask': ( 'media_connect_frontend', - 'android_embedded', - ) - # Alternate #2 - # Prefer use of non-adaptive formats. - elif client_selection == 2: - self._prioritised_clients += ( - 'media_connect_frontend', - 'android', - 'android_youtube_tv', - 'android_testsuite', - 'ios', - 'android_embedded', - ) - # Default - # Will play most videos with subtitles at full resolution with HDR - # Some restricted videos require additional requests for subtitles - # Fallback to iOS, media connect, and embedded clients - else: - self._prioritised_clients += ( - 'android', - 'android_youtube_tv', - 'android_testsuite', - 'ios', - 'media_connect_frontend', - 'android_embedded', - ) + ), + } super(StreamInfo, self).__init__(context=context, **kwargs) @@ -877,10 +860,6 @@ def _get_stream_format(self, itag, info=None, max_height=None, **kwargs): )) return yt_format - def load_stream_infos(self, video_id): - self.video_id = video_id - return self._get_stream_info() - def _get_player_page(self, client_name='web', embed=False): if embed: url = 'https://www.youtube.com/embed/{0}'.format(self.video_id) @@ -1018,12 +997,13 @@ def _normalize_url(url): url = urljoin('https://www.youtube.com', url) return url - def _load_hls_manifest(self, - url, - is_live=False, - meta_info=None, - headers=None, - playback_stats=None): + def _update_from_hls(self, + stream_list, + url, + is_live=False, + meta_info=None, + headers=None, + playback_stats=None): if not url: return [] @@ -1063,30 +1043,27 @@ def _load_hls_manifest(self, playback_stats = {} if is_live: - stream_list = [ - self._get_stream_format( + for yt_format in ('9995', '9996'): + stream_list[yt_format] = self._get_stream_format( itag=yt_format, title='', url=url, meta=meta_info, headers=curl_headers, playback_stats=playback_stats, - ) for yt_format in ('9995', '9996') - ] - else: - stream_list = [ - self._get_stream_format( - itag='9994', - title='', - url=url, - meta=meta_info, - headers=curl_headers, - playback_stats=playback_stats, ) - ] + else: + stream_list['9994'] = self._get_stream_format( + itag='9994', + title='', + url=url, + meta=meta_info, + headers=curl_headers, + playback_stats=playback_stats, + ) settings = self._context.get_settings() - if settings.use_mpd_videos(): + if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: @@ -1101,6 +1078,9 @@ def _load_hls_manifest(self, ) for match in re_playlist_data.finditer(result): itag = match.group('itag') + if itag in stream_list: + continue + yt_format = self._get_stream_format( itag=itag, max_height=selected_height, @@ -1122,16 +1102,16 @@ def _load_hls_manifest(self, if is_live: yt_format['live'] = True yt_format['title'] = 'Live ' + yt_format['title'] - stream_list.append(yt_format) - return stream_list + stream_list[itag] = yt_format - def _create_stream_list(self, - streams, - is_live=False, - meta_info=None, - headers=None, - playback_stats=None): + def _update_from_streams(self, + stream_list, + streams, + is_live=False, + meta_info=None, + headers=None, + playback_stats=None): if not headers and self._selected_client: headers = self._selected_client['headers'].copy() if 'Authorization' in headers: @@ -1151,21 +1131,24 @@ def _create_stream_list(self, playback_stats = {} settings = self._context.get_settings() - if settings.use_mpd_videos(): + if self._use_mpd: qualities = settings.mpd_video_qualities() selected_height = qualities[0]['nom_height'] else: selected_height = settings.fixed_video_quality() - stream_list = [] for stream_map in streams: + itag = str(stream_map['itag']) + if itag in stream_list: + continue + url = stream_map.get('url') conn = stream_map.get('conn') stream = stream_map.get('stream') if not url and conn and stream: new_url = '%s?%s' % (conn, unquote(stream)) - elif not url and self._cipher and 'signatureCipher' in stream_map: + elif not url and 'signatureCipher' in stream_map: new_url = self._process_signature_cipher(stream_map) else: new_url = url @@ -1174,7 +1157,6 @@ def _create_stream_list(self, continue new_url, _ = self._process_url_params(new_url) - itag = str(stream_map['itag']) stream_map['itag'] = itag yt_format = self._get_stream_format( itag=itag, @@ -1204,22 +1186,35 @@ def _create_stream_list(self, yt_format['live'] = True yt_format['title'] = 'Live ' + yt_format['title'] - if 'audioTrack' in stream_map: - audio_track = stream_map['audioTrack'] - display_name = audio_track['displayName'] - yt_format['title'] = '{0} {1}'.format( - yt_format['title'], display_name - ).strip() + audio_track = stream_map.get('audioTrack') + if audio_track: + track_id = audio_track['id'] + track_name = audio_track['displayName'] + itag = '.'.join(( + itag, + track_id, + )) + yt_format['title'] = ' '.join(( + yt_format['title'], + track_name, + )).strip() yt_format['sort'].extend(( - audio_track['id'].startswith(self._language_base), - 'original' in display_name, - display_name + track_id.startswith(self._language_base), + 'original' in track_name or audio_track['audioIsDefault'], + track_name, )) - stream_list.append(yt_format) - return stream_list + stream_list[itag] = yt_format def _process_signature_cipher(self, stream_map): + if self._cipher is None: + self._context.log_debug('signatureCipher detected') + if self._player_js is None: + self._player_js = self._get_player_js() + self._cipher = Cipher(self._context, javascript=self._player_js) + if not self._cipher: + return None + signature_cipher = parse_qs(stream_map['signatureCipher']) url = signature_cipher.get('url', [None])[0] encrypted_signature = signature_cipher.get('s', [None])[0] @@ -1243,6 +1238,7 @@ def _process_signature_cipher(self, stream_map): exc=exc, details=''.join(format_stack()) )) + self._cipher = False return None data_cache.set_item(encrypted_signature, {'sig': signature}) @@ -1261,7 +1257,8 @@ def _process_url_params(self, url): update_url = {} if self._calculate_n and 'n' in query: - self._player_js = self._player_js or self._get_player_js() + if self._player_js is None: + self._player_js = self._get_player_js() if self._calculate_n is True: self._context.log_debug('nsig detected') self._calculate_n = ratebypass.CalculateN(self._player_js) @@ -1343,47 +1340,68 @@ def _get_error_details(self, playability_status, details=None): return result['simpleText'] return None - def _get_stream_info(self): + def load_stream_info(self, video_id): + self.video_id = video_id + + settings = self._context.get_settings() + age_gate_enabled = settings.age_gate() + audio_only = self._audio_only + ask_for_quality = self._ask_for_quality + use_mpd = self._use_mpd + + client_name = None + _client = None + _result = None + playability = None + status = None + reason = None + + stream_list = {} + streaming_data = {} + adaptive_fmts = [] + progressive_fmts = [] + video_info_url = 'https://www.youtube.com/youtubei/v1/player' - _settings = self._context.get_settings() - video_id = self.video_id - client_name = reason = status = None - client = playability_status = result = None - - reasons = ( - self._context.localize(574, 'country').lower(), - # not available error appears to vary by language/region/video type - # disable this check for now - # self._context.localize(10005, 'not available').lower(), - ) + abort_reasons = { + 'country', + 'not available', + } + skip_reasons = { + 'age', + 'inappropriate', + 'latest version', + } + retry_reasons = { + 'try again later', + 'unavailable', + 'unknown', + } + abort = False client_data = {'json': {'videoId': video_id}} if self._access_token: + auth = True client_data['_access_token'] = self._access_token + else: + auth = False - last_status = None - num_errors = num_requests = 0 - while 1: - for client_name in self._prioritised_clients: - if last_status: - self._context.log_warning( - 'Failed to retrieve video info - ' - 'video_id: {0}, client: {1}, auth: {2},\n' - 'status: {3}, reason: {4}'.format( - video_id, - client['_name'], - bool(client.get('_access_token')), - last_status, - reason or 'UNKNOWN', - ) - ) - client = self.build_client(client_name, client_data) - if not client: - status = None + for name, clients in self._client_groups.items(): + if not clients: + continue + if name == 'mpd' and not use_mpd: + continue + if name == 'ask' and not ask_for_quality and self._selected_client: + continue + + status = None + + for client_name in clients: + _client = self.build_client(client_name, client_data) + if not _client: continue - result = self.request( + _result = self.request( video_info_url, 'POST', response_hook=self._response_hook_json, @@ -1392,89 +1410,94 @@ def _get_stream_info(self): error_hook_kwargs={ 'video_id': video_id, 'client': client_name, - 'auth': bool(client.get('_access_token')), + 'auth': bool(_client.get('_access_token')), }, - **client + **_client ) - num_requests += 1 - video_details = result.get('videoDetails', {}) - playability_status = result.get('playabilityStatus', {}) - status = playability_status.get('status', 'ERROR').upper() - reason = playability_status.get('reason', '') + video_details = _result.get('videoDetails', {}) + playability = _result.get('playabilityStatus', {}) + status = playability.get('status', 'ERROR').upper() + reason = playability.get('reason', 'UNKNOWN') if video_details and video_id != video_details.get('videoId'): status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' reason = 'Watch on the latest version of YouTube' - continue - if status == 'OK': + if (age_gate_enabled + and playability.get('desktopLegacyAgeGateReason')): + abort = True + break + elif status == 'OK': break elif status in { 'AGE_CHECK_REQUIRED', 'AGE_VERIFICATION_REQUIRED', 'CONTENT_CHECK_REQUIRED', + 'LOGIN_REQUIRED', 'CONTENT_NOT_AVAILABLE_IN_THIS_APP', 'ERROR', - 'LOGIN_REQUIRED', 'UNPLAYABLE', }: - if not last_status or status == last_status: - num_errors += 1 - last_status = status - if (playability_status.get('desktopLegacyAgeGateReason') - and _settings.age_gate()): - break - # Geo-blocked video with error reasons like: - # "This video contains content from XXX, who has blocked it in your country on copyright grounds" - # "The uploader has not made this video available in your country" - # Reason language will vary based on Accept-Language and - # client hl, Kodi localised language is used for comparison - # but may not match reason language - if (status == 'UNPLAYABLE' - and any(why in reason for why in reasons)): - break - if status != 'ERROR': - continue - # This is used to check for error like: - # "The following content is not available on this app." - # Reason language will vary based on Accept-Language and - # client hl, so YouTube support url is checked instead - url = self._get_error_details( - playability_status, - details=( - 'errorScreen', - 'playerErrorMessageRenderer', - 'learnMore', - 'runs', 0, - 'navigationEndpoint', - 'urlEndpoint', - 'url' + self._context.log_warning( + 'Failed to retrieve video info - ' + 'video_id: {0}, client: {1}, auth: {2},\n' + 'status: {3}, reason: {4}'.format( + video_id, + _client['_name'], + auth, + status, + reason or 'UNKNOWN', ) ) - if url and url.startswith('//support.google.com/youtube/answer/12318250'): - status = 'CONTENT_NOT_AVAILABLE_IN_THIS_APP' + compare_reason = reason.lower() + if any(why in compare_reason for why in retry_reasons): + continue + if any(why in compare_reason for why in skip_reasons): + break + if any(why in compare_reason for why in abort_reasons): + abort = True + break else: self._context.log_debug( 'Unknown playabilityStatus in player response:\n|{0}|' - .format(playability_status) + .format(playability) ) - # Only attempt to remove Authorization header if clients iterable - # was exhausted i.e. request attempted using all clients - else: - if num_errors == num_requests: - break - if '_access_token' in client_data: - del client_data['_access_token'] - continue - # Otherwise skip retrying clients without Authorization header - break - if status != 'OK': + if abort: + break + + if status == 'OK': + self._context.log_debug( + 'Retrieved video info - ' + 'video_id: {0}, client: {1}, auth: {2}'.format( + video_id, + client_name, + bool(_client.get('_access_token')), + ) + ) + if not self._selected_client: + client = self._selected_client = _client.copy() + result = _result + video_details = result.get('videoDetails', {}) + playability = result.get('playabilityStatus', {}) + + _streaming_data = _result.get('streamingData', {}) + if audio_only or ask_for_quality or not use_mpd: + progressive_fmts.extend( + _streaming_data.get('formats', []) + ) + if use_mpd: + adaptive_fmts.extend( + _streaming_data.get('adaptiveFormats', []) + ) + streaming_data.update(_streaming_data) + + if not self._selected_client: if status == 'LIVE_STREAM_OFFLINE': if not reason: reason = self._get_error_details( - playability_status, + playability, details=( 'liveStreamability', 'liveStreamabilityRenderer', @@ -1484,19 +1507,9 @@ def _get_stream_info(self): ) ) elif not reason: - reason = self._get_error_details(playability_status) + reason = self._get_error_details(playability) raise YouTubeException(reason or 'UNKNOWN') - self._context.log_debug( - 'Retrieved video info - ' - 'video_id: {0}, client: {1}, auth: {2}'.format( - video_id, - client_name, - bool(client.get('_access_token')), - ) - ) - self._selected_client = client.copy() - if 'Authorization' in client['headers']: del client['headers']['Authorization'] # Make a set of URL-quoted headers to be sent to Kodi when requesting @@ -1507,7 +1520,6 @@ def _get_stream_info(self): microformat = (result.get('microformat', {}) .get('playerMicroformatRenderer', {})) - streaming_data = result.get('streamingData', {}) is_live = video_details.get('isLiveContent', False) if is_live: is_live = video_details.get('isLive', False) @@ -1548,7 +1560,7 @@ def _get_stream_info(self): 'subtitles': None, } - if _settings.use_remote_history(): + if settings.use_remote_history(): playback_stats = { 'playback_url': 'videostatsPlaybackUrl', 'watchtime_url': 'videostatsWatchtimeUrl', @@ -1568,11 +1580,8 @@ def _get_stream_info(self): 'watchtime_url': '', } - use_mpd_vod = _settings.use_mpd_videos() - use_isa = _settings.use_isa() - pa_li_info = streaming_data.get('licenseInfos', []) - if any(pa_li_info) and not use_isa: + if any(pa_li_info) and not settings.use_isa(): raise YouTubeException('InputStream.Adaptive not enabled') for li_info in pa_li_info: if li_info.get('drmFamily') != 'WIDEVINE': @@ -1585,11 +1594,14 @@ def _get_stream_info(self): address, port = get_connect_address(self._context) license_info = { 'url': url, - 'proxy': 'http://{address}:{port}{path}||R{{SSM}}|'.format( - address=address, - port=port, - path=PATHS.DRM, - ), + 'proxy': ''.join(( + 'http://', + address, + ':', + str(port), + PATHS.DRM, + '||R{{SSM}}|', + )), 'token': self._access_token, } break @@ -1600,20 +1612,6 @@ def _get_stream_info(self): 'token': None } - stream_list = [] - if use_isa and use_mpd_vod: - adaptive_fmts = streaming_data.get('adaptiveFormats', []) - all_fmts = streaming_data.get('formats', []) + adaptive_fmts - else: - adaptive_fmts = None - all_fmts = streaming_data.get('formats', []) - - if any(True for fmt in all_fmts - if fmt and 'url' not in fmt and 'signatureCipher' in fmt): - self._context.log_debug('signatureCipher detected') - self._player_js = self._get_player_js() - self._cipher = Cipher(self._context, javascript=self._player_js) - if 'dashManifestUrl' in streaming_data: manifest_url = streaming_data['dashManifestUrl'] if '?' in manifest_url: @@ -1623,7 +1621,7 @@ def _get_stream_info(self): else: manifest_url += '/mpd_version/5' - stream_list.append(self._get_stream_format( + stream_list['9998'] = self._get_stream_format( itag='9998', title='', url=manifest_url, @@ -1631,15 +1629,21 @@ def _get_stream_info(self): headers=curl_headers, license_info=license_info, playback_stats=playback_stats, - )) - if 'hlsManifestUrl' in streaming_data: - stream_list.extend(self._load_hls_manifest( + ) + if 'hlsManifestUrl' in streaming_data and ( + is_live + or live_dvr + or ask_for_quality + or not use_mpd + ): + self._update_from_hls( + stream_list, streaming_data['hlsManifestUrl'], is_live, meta_info, client['headers'], playback_stats, - )) + ) subtitles = Subtitles(self._context, video_id) query_subtitles = client.get('_query_subtitles') @@ -1647,7 +1651,7 @@ def _get_stream_info(self): query_subtitles is True or (query_subtitles and subtitles.sub_selection == subtitles.LANG_ALL)): - for client_name in ('smarttv_embedded', 'web', 'android'): + for client_name in ('smart_tv_embedded', 'web', 'android'): caption_client = self.build_client(client_name, client_data) if not caption_client: continue @@ -1674,7 +1678,7 @@ def _get_stream_info(self): subtitles.load(captions, caption_client['headers']) default_lang = subtitles.get_lang_details() subs_data = subtitles.get_subtitles() - if subs_data and (not use_mpd_vod or subtitles.pre_download): + if subs_data and (not use_mpd or subtitles.pre_download): meta_info['subtitles'] = [ subtitle['url'] for subtitle in subs_data.values() ] @@ -1688,7 +1692,7 @@ def _get_stream_info(self): subs_data = None # extract adaptive streams and create MPEG-DASH manifest - if adaptive_fmts and not self._audio_only: + if adaptive_fmts and not audio_only: video_data, audio_data = self._process_stream_data( adaptive_fmts, default_lang['default'] @@ -1731,18 +1735,25 @@ def _get_stream_info(self): if len(title) > 1: yt_format['title'] = ''.join(yt_format['title']) - stream_list.append(yt_format) + stream_list['9999'] = yt_format # extract non-adaptive streams - if all_fmts: - stream_list.extend(self._create_stream_list( - all_fmts, is_live, meta_info, client['headers'], playback_stats - )) + if adaptive_fmts and (audio_only or ask_for_quality): + progressive_fmts.extend(adaptive_fmts) + if progressive_fmts: + self._update_from_streams( + stream_list, + progressive_fmts, + is_live, + meta_info, + client['headers'], + playback_stats, + ) if not stream_list: raise YouTubeException('No streams found') - return stream_list + return stream_list.values() def _process_stream_data(self, stream_data, default_lang_code='und'): _settings = self._context.get_settings() @@ -1784,7 +1795,7 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): continue url = stream.get('url') - if not url and self._cipher and 'signatureCipher' in stream: + if not url and 'signatureCipher' in stream: url = self._process_signature_cipher(stream) if not url: continue @@ -1912,11 +1923,14 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): else: frame_rate = None - mime_group = '{mime_type}_{codec}{hdr}'.format( - mime_type=mime_type, - codec=codec, - hdr='_hdr' if hdr else '' - ) + mime_group = '_'.join(( + mime_type, + codec, + 'hdr', + ) if hdr else ( + mime_type, + codec, + )) channels = language = role = role_type = sample_rate = None label = quality['label'].format( quality['nom_height'] or compare_height, @@ -2021,6 +2035,7 @@ def _generate_mpd_manifest(self, audio_data, subs_data, license_url): + # Following line can be uncommented if needed to use mpd for audio only # if (not video_data and not self._audio_only) or not audio_data: if not video_data or not audio_data: return None, None @@ -2135,10 +2150,10 @@ def _filter_group(previous_group, previous_stream, item): if 'auto' in stream_select or media_type == 'video': label = stream['label'] else: - label = '{0} {1}'.format( + label = ' '.join(( stream['langName'], - stream['label'] - ).strip() + stream['label'], + )).strip() if stream == main_stream[media_type]: default = True role = 'main' @@ -2324,10 +2339,12 @@ def _filter_group(previous_group, previous_stream, item): success = False if success: address, port = get_connect_address(self._context) - return 'http://{address}:{port}{path}{file}'.format( - address=address, - port=port, - path=PATHS.MPD, - file=filename, - ), main_stream + return ''.join(( + 'http://', + address, + ':', + str(port), + PATHS.MPD, + filename, + )), main_stream return None, None diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 9117de73f..a83d23a00 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -90,27 +90,33 @@ def make_comment_item(context, snippet, uri, total_replies=0): # Format the label of the comment item. if label_props: - label = '{author} ({props}) {body}'.format( - author=author, - props='|'.join(label_props), - body=body.replace('\n', ' ') - ) + label = ''.join(( + author, + ' (', + '|'.join(label_props), + ') ', + body.replace('\n', ' '), + )) else: - label = '{author} {body}'.format( - author=author, body=body.replace('\n', ' ') - ) + label = ' '.join(( + author, + body.replace('\n', ' '), + )) # Format the plot of the comment item. if plot_props: - plot = '{author} ({props}){body}'.format( - author=author, - props='|'.join(plot_props), - body=ui.new_line(body, cr_before=2) - ) + plot = ''.join(( + author, + ' (', + '|'.join(plot_props), + ')', + ui.new_line(body, cr_before=2), + )) else: - plot = '{author}{body}'.format( - author=author, body=ui.new_line(body, cr_before=2) - ) + plot = ''.join(( + author, + ui.new_line(body, cr_before=2), + )) comment_item = DirectoryItem(label, uri, plot=plot, action=(not uri)) @@ -518,12 +524,10 @@ def update_video_infos(provider, context, video_id_dict, type_label = localize('live') else: type_label = localize(335) # "Start" - start_at = '{type_label} {start_at}'.format( - type_label=type_label, - start_at=datetime_parser.get_scheduled_start( - context, local_datetime - ) - ) + start_at = ' '.join(( + type_label, + datetime_parser.get_scheduled_start(context, local_datetime), + )) label_stats = [] stats = [] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index 8b5b246fa..ff6645da7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -55,16 +55,20 @@ def _play_stream(provider, context): 'url': 'https://youtu.be/{0}'.format(video_id), } else: - ask_for_quality = None + ask_for_quality = settings.ask_for_video_quality() if ui.pop_property(PLAY_PROMPT_QUALITY) and not screensaver: ask_for_quality = True - audio_only = None + audio_only = settings.audio_only() if ui.pop_property(PLAY_FORCE_AUDIO): audio_only = True try: - streams = client.get_streams(context, video_id, audio_only) + streams = client.get_streams(context, + video_id, + ask_for_quality, + audio_only, + settings.use_mpd_videos()) except YouTubeException as exc: context.log_error('yt_play.play_video - {exc}:\n{details}'.format( exc=exc, details=''.join(format_stack()) @@ -335,6 +339,7 @@ def process(provider, context, **_kwargs): if force_play: context.execute('Action(Play)') return False + ui.clear_property(SERVER_POST_START) context.wakeup(SERVER_WAKEUP, timeout=5) video_item = _play_stream(provider, context) ui.set_property(SERVER_POST_START) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index e85045e3a..c28890577 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -12,7 +12,6 @@ from .utils import get_thumbnail from ...kodion import KodionException -from ...kodion.compatibility import parse_qsl, urlsplit from ...kodion.constants import CHANNEL_ID, PATHS, PLAYLISTITEM_ID, PLAYLIST_ID from ...kodion.utils import find_video_id 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 755b749ff..d0917caa7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -316,7 +316,6 @@ def process_default_settings(context, step, steps, **_kwargs): (localize('setup_wizard.prompt') % localize('setup_wizard.prompt.settings.defaults')) ): - settings.client_selection(0) settings.use_isa(True) settings.use_mpd_videos(True) settings.stream_select(4 if settings.ask_for_video_quality() else 3) @@ -370,7 +369,6 @@ def process_performance_settings(context, step, steps, **_kwargs): 'num_items': 10, 'settings': ( (settings.use_isa, (False,)), - (settings.client_selection, (2,)), (settings.use_mpd_videos, (False,)), (settings.set_subtitle_download, (True,)), ), @@ -379,9 +377,6 @@ def process_performance_settings(context, step, steps, **_kwargs): 'max_resolution': 4, # 1080p 'stream_features': ('avc1', 'vorbis', 'mp4a', 'filter'), 'num_items': 10, - 'settings': ( - (settings.client_selection, (2,)), - ), }, '1080p30': { 'max_resolution': 4, # 1080p diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index d9565fd00..df9518094 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -297,8 +297,6 @@ def _process_saved_playlists_tv(provider, context, client): def _process_my_subscriptions(provider, context, client, filtered=False): context.set_content(CONTENT.VIDEO_CONTENT) - function_cache = context.get_function_cache() - params = context.get_params() refresh = params.get('refresh') diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 9b8460877..f1fc9ff02 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -40,7 +40,6 @@ PATHS, ) from ..kodion.items import ( - BaseItem, DirectoryItem, NewSearchItem, SearchItem, diff --git a/resources/settings.xml b/resources/settings.xml index 1bd96ae23..3150c4105 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -575,18 +575,6 @@ - - 0 - 0 - - - - - - - - - 0 false