diff --git a/addon.xml b/addon.xml index 90191fc5e..a661ccc34 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index eb13564cd..4655d741a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,4 @@ -## v7.1.1.3 +## v7.1.1.4 ### Fixed - Fix http server not listening on any interface if listen IP is 0.0.0.0 #927 - Standardise return type of LoginClient.refresh_token #932 @@ -40,6 +40,8 @@ - Fix missing "Ask" translation string - Fix incorrect parameter name breaking auto-remove from Watch Later #993 - Fix processing of "q" and "channelId" search query params #1004 +- Fix errors with progress dialogs in Kodi 18 #1000 +- Fix remote watch history not updating #1008 ### Changed - Improve display and update of bookmarks diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py index e3e11c33f..c1fb80d3a 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_progress_dialog.py @@ -87,10 +87,9 @@ def update(self, steps=1, position=None, message=None, **template_params): message = self._message_template.format(**self._template_params) self._message = message - self._dialog.update( - percent=position, - message=message, - ) + # Kodi 18 renamed XbmcProgressDialog.update argument line1 to message. + # Only use positional arguments to maintain compatibility + self._dialog.update(position, self._message) def is_aborted(self): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 1f535f47e..d6012445a 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -172,6 +172,7 @@ class YouTubeRequestClient(BaseRequestsClass): }, 'ios': { '_id': 5, + '_auth_type': False, '_os': { 'major': '17', 'minor': '5', @@ -261,6 +262,7 @@ class YouTubeRequestClient(BaseRequestsClass): }, '_common': { '_access_token': None, + '_access_token_tv': None, 'json': { 'contentCheckOk': True, 'context': { @@ -286,13 +288,11 @@ class YouTubeRequestClient(BaseRequestsClass): 'videoId': None, }, 'headers': { - 'Origin': 'https://www.youtube.com', - 'Referer': 'https://www.youtube.com/watch?v={json[videoId]}', 'Accept-Encoding': 'gzip, deflate', 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.5', - 'Authorization': 'Bearer {_access_token}', + 'Authorization': None, }, 'params': { 'key': ValueError, @@ -369,31 +369,53 @@ def json_traverse(cls, json_data, path, default=None): def build_client(cls, client_name=None, data=None): templates = {} - client = None + base_client = None if client_name: - client = cls.CLIENTS.get(client_name) - if client and client.get('_disabled'): + base_client = cls.CLIENTS.get(client_name) + if base_client and base_client.get('_disabled'): return None - if not client: - client = YouTubeRequestClient.CLIENTS['web'] - client = client.copy() + if not base_client: + base_client = YouTubeRequestClient.CLIENTS['web'] + base_client = base_client.copy() if data: - client = merge_dicts(client, data) + client = merge_dicts(base_client, data) client = merge_dicts(cls.CLIENTS['_common'], client, templates) client['_name'] = client_name + if base_client.get('_auth_required'): + client['_auth_required'] = True for values, template_id, template in templates.values(): if template_id in values: values[template_id] = template.format(**client) + has_auth = False try: params = client['params'] - if client.get('_access_token'): + auth_required = client.get('_auth_required') + auth_requested = client.get('_auth_requested') + auth_type = client.get('_auth_type') + if auth_type == 'tv' and auth_requested != 'personal': + auth_token = client.get('_access_token_tv') + elif auth_type is not False: + auth_token = client.get('_access_token') + else: + auth_token = None + + if auth_token and (auth_required or auth_requested): + headers = client['headers'] + if 'Authorization' in headers: + headers = headers.copy() + headers['Authorization'] = 'Bearer {0}'.format(auth_token) + client['headers'] = headers + has_auth = True + if 'key' in params: params = params.copy() del params['key'] client['params'] = params + elif auth_required: + return None else: headers = client['headers'] if 'Authorization' in headers: @@ -407,5 +429,6 @@ def build_client(cls, client_name=None, data=None): client['params'] = params except KeyError: pass + client['_has_auth'] = has_auth return client diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index dafbaa44a..c68cdd160 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -32,7 +32,7 @@ class YouTube(LoginClient): CLIENTS = { - 1: { + 'v1': { 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', 'method': None, 'json': { @@ -47,13 +47,13 @@ class YouTube(LoginClient): 'Host': 'www.youtube.com', }, }, - 3: { + 'v3': { + '_auth_required': True, 'url': 'https://www.googleapis.com/youtube/v3/{_endpoint}', 'method': None, 'headers': { 'Host': 'www.googleapis.com', }, - 'auth_required': True, }, 'tv': { 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', @@ -85,8 +85,27 @@ class YouTube(LoginClient): 'Host': 'www.youtube.com', }, }, + 'watch_history': { + '_auth_required': True, + '_auth_type': 'personal', + '_video_id': None, + 'headers': { + 'Host': 's.youtube.com', + 'Referer': 'https://www.youtube.com/watch?v={_video_id}', + }, + 'params': { + 'referrer': 'https://accounts.google.com/', + 'ns': 'yt', + 'el': 'detailpage', + 'ver': '2', + 'fs': '0', + 'volume': '100', + 'muted': '0', + }, + }, '_common': { '_access_token': None, + '_access_token_tv': None, 'json': { 'context': { 'client': { @@ -108,7 +127,7 @@ class YouTube(LoginClient): 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.5', - 'Authorization': 'Bearer {_access_token}', + 'Authorization': None, 'DNT': '1', 'User-Agent': ('Mozilla/5.0 (Linux; Android 10; SM-G981B)' ' AppleWebKit/537.36 (KHTML, like Gecko)' @@ -154,29 +173,13 @@ def update_watch_history(self, context, video_id, url, status=None): et=et, state=state)) - headers = { - 'Host': 's.youtube.com', - 'Connection': 'keep-alive', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', - 'Accept': '*/*', - 'Accept-Language': 'en-US,en;q=0.5', - 'DNT': '1', - 'Referer': 'https://www.youtube.com/watch?v=' + video_id, - 'User-Agent': ('Mozilla/5.0 (Linux; Android 10; SM-G981B)' - ' AppleWebKit/537.36 (KHTML, like Gecko)' - ' Chrome/80.0.3987.162 Mobile Safari/537.36'), - } - params = { - 'docid': video_id, - 'referrer': 'https://accounts.google.com/', - 'ns': 'yt', - 'el': 'detailpage', - 'ver': '2', - 'fs': '0', - 'volume': '100', - 'muted': '0', + client_data = { + '_video_id': video_id, + 'url': url, + 'error_title': 'Failed to update watch history', } + + params = {} if cmt is not None: params['cmt'] = format(cmt, '.3f') if st is not None: @@ -185,11 +188,11 @@ def update_watch_history(self, context, video_id, url, status=None): params['et'] = format(et, '.3f') if state is not None: params['state'] = state - if self._access_token: - params['access_token'] = self._access_token - self.request(url, params=params, headers=headers, - error_msg='Failed to update watch history') + self.api_request(client='watch_history', + client_data=client_data, + params=params, + no_content=True) def get_streams(self, context, @@ -325,7 +328,7 @@ def unsubscribe(self, subscription_id, **kwargs): def unsubscribe_channel(self, channel_id, **kwargs): post_data = {'channelIds': [channel_id]} - return self.api_request(version=1, + return self.api_request(client='v1', method='POST', path='subscription/unsubscribe', post_data=post_data, @@ -453,7 +456,7 @@ def get_recommended_for_home(self, } post_data['context'] = context - result = self.api_request(version=1, + result = self.api_request(client='v1', method='POST', path='browse', post_data=post_data) @@ -1067,8 +1070,9 @@ def get_related_videos(self, if page_token: post_data['continuation'] = page_token - result = self.api_request(version=('tv' if retry == 1 else - 'tv_embed' if retry == 2 else 1), + result = self.api_request(client=('tv' if retry == 1 else + 'tv_embed' if retry == 2 else + 'v1'), method='POST', path='next', post_data=post_data, @@ -1972,7 +1976,7 @@ def _perform(_playlist_idx, _page_token, _offset, _result): else: _post_data['browseId'] = 'FEmy_youtube' - _json_data = self.api_request(version=1, + _json_data = self.api_request(client='v1', method='POST', path='browse', post_data=_post_data) @@ -2067,7 +2071,7 @@ def _perform(_playlist_idx, _page_token, _offset, _result): } playlist_index = None - json_data = self.api_request(version=1, + json_data = self.api_request(client='v1', method='POST', path='browse', post_data=_en_post_data) @@ -2167,18 +2171,20 @@ def _error_hook(self, **kwargs): return '', info, details, data, False, exception def api_request(self, - version=3, + client='v3', method='GET', + client_data=None, path=None, params=None, post_data=None, headers=None, no_login=False, **kwargs): - client_data = { - '_endpoint': path.strip('/'), - 'method': method, - } + if not client_data: + client_data = {} + client_data.setdefault('method', method) + if path: + client_data['_endpoint'] = path.strip('/') if headers: client_data['headers'] = headers if method in {'POST', 'PUT'}: @@ -2191,37 +2197,37 @@ def api_request(self, client_data['params'] = params abort = False - if no_login: - pass - # a config can decide if a token is allowed - elif self._access_token and self._config.get('token-allowed', True): - client_data['_access_token'] = self._access_token - elif self._access_token_tv: - client_data['_access_token'] = self._access_token_tv - # abort if authentication is required but not available for request - elif self.CLIENTS.get(version, {}).get('auth_required'): + if not no_login: + client_data.setdefault('_auth_required', True) + # a config can decide if a token is allowed + if self._access_token and self._config.get('token-allowed', True): + client_data['_access_token'] = self._access_token + if self._access_token_tv: + client_data['_access_token_tv'] = self._access_token_tv + + client = self.build_client(client, client_data) + if not client: + client = {} abort = True - client = self.build_client(version, client_data) + if clear_data and 'json' in client: + del client['json'] params = client.get('params') - if 'key' in params: - if params['key']: - abort = False - else: - params = params.copy() - key = self._config.get('key') or self._config_tv.get('key') - if key: + if params: + if 'key' in params: + if params['key']: abort = False - params['key'] = key else: - del params['key'] - client['params'] = params - - if clear_data and 'json' in client: - del client['json'] + params = params.copy() + key = self._config.get('key') or self._config_tv.get('key') + if key: + abort = False + params['key'] = key + else: + del params['key'] + client['params'] = params - if params: log_params = params.copy() if 'location' in log_params: log_params['location'] = '|xx.xxxx,xx.xxxx|' @@ -2241,13 +2247,13 @@ def api_request(self, context = self._context context.log_debug('API request:' - '\n\tversion: |{version}|' + '\n\ttype: |{type}|' '\n\tmethod: |{method}|' '\n\tpath: |{path}|' '\n\tparams: |{params}|' '\n\tpost_data: |{data}|' '\n\theaders: |{headers}|' - .format(version=version, + .format(type=client.get('_name'), method=method, path=path, params=log_params, @@ -2255,7 +2261,10 @@ def api_request(self, headers=log_headers)) if abort: if kwargs.get('notify', True): - context.get_ui().on_ok(context.get_name(), context.localize('key.requirement')) + context.get_ui().on_ok( + context.get_name(), + context.localize('key.requirement'), + ) context.log_warning('API request: aborted') return {} return self.request(response_hook=self._response_hook, diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index e44833366..27c902f8c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -703,7 +703,8 @@ def __init__(self, self._calculate_n = True self._cipher = None - self._selected_client = None + self._auth_client = {} + self._selected_client = {} self._client_groups = { 'custom': clients if clients else (), # Access "premium" streams, HLS and DASH @@ -1357,6 +1358,7 @@ def load_stream_info(self, video_id): audio_only = self._audio_only ask_for_quality = self._ask_for_quality use_mpd = self._use_mpd + use_remote_history = settings.use_remote_history() client_name = None _client = None @@ -1394,20 +1396,24 @@ def load_stream_info(self, video_id): } abort = False - client_data = {'json': {'videoId': video_id}} - access_token = self._access_token - auth = False + has_access_token = bool(self._access_token) + client_data = { + 'json': { + 'videoId': video_id, + }, + '_auth_required': False, + '_auth_requested': 'personal' if use_remote_history else False, + '_access_token': self._access_token, + } for name, clients in self._client_groups.items(): if not clients: continue - if name == 'mpd' and not use_mpd: + if name == 'mpd' and not (use_mpd or use_remote_history): continue if name == 'ask' and use_mpd and not ask_for_quality: continue - status = None - restart = False while 1: for client_name in clients: @@ -1424,7 +1430,7 @@ def load_stream_info(self, video_id): error_hook_kwargs={ 'video_id': video_id, 'client': client_name, - 'auth': bool(_client.get('_access_token')), + 'auth': _client.get('_has_auth', False), }, **_client ) or {} @@ -1469,14 +1475,13 @@ def load_stream_info(self, video_id): reason=reason or 'UNKNOWN', video_id=video_id, client=_client['_name'], - auth=auth, + auth=_client.get('_has_auth', False), ) ) compare_reason = reason.lower() if any(why in compare_reason for why in reauth_reasons): - if access_token and not auth: - auth = True - client_data['_access_token'] = access_token + if has_access_token: + client_data['_auth_required'] = True restart = True break if any(why in compare_reason for why in retry_reasons): @@ -1510,14 +1515,19 @@ def load_stream_info(self, video_id): .format( video_id=video_id, client=client_name, - auth=bool(_client.get('_access_token')), + auth=_client.get('_has_auth', False), ) ) if not self._selected_client: - client = self._selected_client = _client.copy() - result = _result - video_details = result.get('videoDetails', {}) - playability = result.get('playabilityStatus', {}) + self._selected_client = { + 'client': _client.copy(), + 'result': _result, + } + if not self._auth_client and _client.get('_has_auth'): + self._auth_client = { + 'client': _client.copy(), + 'result': _result, + } _streaming_data = _result.get('streamingData', {}) if audio_only or ask_for_quality or not use_mpd: @@ -1549,6 +1559,9 @@ def load_stream_info(self, video_id): reason = self._get_error_details(playability) raise YouTubeException(reason or 'UNKNOWN') + client = self._selected_client['client'] + result = self._selected_client['result'] + if 'Authorization' in client['headers']: del client['headers']['Authorization'] # Make a set of URL-quoted headers to be sent to Kodi when requesting @@ -1557,8 +1570,7 @@ def load_stream_info(self, video_id): # curl_headers = self._make_curl_headers(headers, cookies) curl_headers = self._prepare_headers(client['headers']) - microformat = (result.get('microformat', {}) - .get('playerMicroformatRenderer', {})) + video_details = result.get('videoDetails', {}) is_live = video_details.get('isLiveContent', False) if is_live: is_live = video_details.get('isLive', False) @@ -1568,6 +1580,8 @@ def load_stream_info(self, video_id): live_dvr = False thumb_suffix = '' + microformat = (result.get('microformat', {}) + .get('playerMicroformatRenderer', {})) meta_info = { 'id': video_id, 'title': unescape(video_details.get('title', '') @@ -1597,12 +1611,14 @@ def load_stream_info(self, video_id): 'subtitles': None, } - if settings.use_remote_history(): + if use_remote_history and self._auth_client: playback_stats = { 'playback_url': 'videostatsPlaybackUrl', 'watchtime_url': 'videostatsWatchtimeUrl', } - playback_tracking = result.get('playbackTracking', {}) + playback_tracking = (self._auth_client + .get('result', {}) + .get('playbackTracking', {})) cpn = self._generate_cpn() for key, url_key in playback_stats.items(): @@ -1637,7 +1653,7 @@ def load_stream_info(self, video_id): '', '', )) + '||R{{SSM}}|R', - 'token': access_token, + 'token': self._access_token, } break else: @@ -1699,7 +1715,7 @@ def load_stream_info(self, video_id): error_hook_kwargs={ 'video_id': video_id, 'client': client_name, - 'auth': bool(caption_client.get('_access_token')), + 'auth': _client.get('_has_auth', False), }, **caption_client )