diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index d6012445a..4aec2b0a3 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -88,7 +88,7 @@ class YouTubeRequestClient(BaseRequestsClass): # Limited subtitle availability 'android_testsuite': { '_id': 30, - '_disabled': True, + '_auth_required': 'tv', '_query_subtitles': True, 'json': { 'params': _PLAYER_PARAMS['android_testsuite'], @@ -117,7 +117,7 @@ class YouTubeRequestClient(BaseRequestsClass): # Limited subtitle availability 'android_youtube_tv': { '_id': 29, - '_disabled': True, + '_auth_required': 'tv', '_query_subtitles': True, 'json': { 'params': _PLAYER_PARAMS['android'], @@ -172,7 +172,6 @@ class YouTubeRequestClient(BaseRequestsClass): }, 'ios': { '_id': 5, - '_auth_type': False, '_os': { 'major': '17', 'minor': '5', @@ -205,6 +204,7 @@ class YouTubeRequestClient(BaseRequestsClass): }, 'media_connect_frontend': { '_id': 95, + '_auth_required': 'tv', '_query_subtitles': True, 'json': { 'context': { @@ -288,6 +288,8 @@ 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': '*/*', @@ -369,21 +371,19 @@ def json_traverse(cls, json_data, path, default=None): def build_client(cls, client_name=None, data=None): templates = {} - base_client = None + client = None if client_name: - base_client = cls.CLIENTS.get(client_name) - if base_client and base_client.get('_disabled'): + client = cls.CLIENTS.get(client_name) + if client and client.get('_disabled'): return None - if not base_client: - base_client = YouTubeRequestClient.CLIENTS['web'] - base_client = base_client.copy() + if not client: + client = YouTubeRequestClient.CLIENTS['web'] + client = client.copy() if data: - client = merge_dicts(base_client, data) + client = merge_dicts(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: @@ -393,16 +393,12 @@ def build_client(cls, client_name=None, data=None): try: params = client['params'] 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': + if auth_required == 'tv': auth_token = client.get('_access_token_tv') - elif auth_type is not False: - auth_token = client.get('_access_token') else: - auth_token = None + auth_token = client.get('_access_token') - if auth_token and (auth_required or auth_requested): + if auth_token: headers = client['headers'] if 'Authorization' in headers: headers = headers.copy() diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 20249be96..fdcfafc4d 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 = { - 'v1': { + 1: { 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', 'method': None, 'json': { @@ -47,8 +47,8 @@ class YouTube(LoginClient): 'Host': 'www.youtube.com', }, }, - 'v3': { - '_auth_requested': True, + 3: { + '_auth_required': True, 'url': 'https://www.googleapis.com/youtube/v3/{_endpoint}', 'method': None, 'headers': { @@ -85,24 +85,6 @@ 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, @@ -173,13 +155,29 @@ def update_watch_history(self, context, video_id, url, status=None): et=et, state=state)) - client_data = { - '_video_id': video_id, - 'url': url, - 'error_title': 'Failed to update watch history', + 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', } - - params = {} if cmt is not None: params['cmt'] = format(cmt, '.3f') if st is not None: @@ -188,11 +186,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.api_request(client='watch_history', - client_data=client_data, - params=params, - no_content=True) + self.request(url, params=params, headers=headers, + error_msg='Failed to update watch history') def get_streams(self, context, @@ -202,7 +200,8 @@ def get_streams(self, use_mpd=True): return StreamInfo( context, - access_token=(self._access_token or self._access_token_tv), + access_token=self._access_token, + access_token_tv=self._access_token_tv, ask_for_quality=ask_for_quality, audio_only=audio_only, use_mpd=use_mpd, @@ -328,7 +327,7 @@ def unsubscribe(self, subscription_id, **kwargs): def unsubscribe_channel(self, channel_id, **kwargs): post_data = {'channelIds': [channel_id]} - return self.api_request(client='v1', + return self.api_request(version=1, method='POST', path='subscription/unsubscribe', post_data=post_data, @@ -456,7 +455,7 @@ def get_recommended_for_home(self, } post_data['context'] = context - result = self.api_request(client='v1', + result = self.api_request(version=1, method='POST', path='browse', post_data=post_data) @@ -1070,9 +1069,8 @@ def get_related_videos(self, if page_token: post_data['continuation'] = page_token - result = self.api_request(client=('tv' if retry == 1 else - 'tv_embed' if retry == 2 else - 'v1'), + 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, @@ -1976,7 +1974,7 @@ def _perform(_playlist_idx, _page_token, _offset, _result): else: _post_data['browseId'] = 'FEmy_youtube' - _json_data = self.api_request(client='v1', + _json_data = self.api_request(version=1, method='POST', path='browse', post_data=_post_data) @@ -2071,7 +2069,7 @@ def _perform(_playlist_idx, _page_token, _offset, _result): } playlist_index = None - json_data = self.api_request(client='v1', + json_data = self.api_request(version=1, method='POST', path='browse', post_data=_en_post_data) @@ -2171,20 +2169,18 @@ def _error_hook(self, **kwargs): return '', info, details, data, False, exception def api_request(self, - client='v3', + version=3, method='GET', - client_data=None, path=None, params=None, post_data=None, headers=None, no_login=False, **kwargs): - if not client_data: - client_data = {} - client_data.setdefault('method', method) - if path: - client_data['_endpoint'] = path.strip('/') + client_data = { + '_endpoint': path.strip('/'), + 'method': method, + } if headers: client_data['headers'] = headers if method in {'POST', 'PUT'}: @@ -2198,14 +2194,13 @@ def api_request(self, abort = False if not no_login: - client_data.setdefault('_auth_requested', 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) + client = self.build_client(version, client_data) if not client: client = {} abort = True @@ -2249,13 +2244,13 @@ def api_request(self, context = self._context context.log_debug('API request:' - '\n\ttype: |{type}|' + '\n\tversion: |{version}|' '\n\tmethod: |{method}|' '\n\tpath: |{path}|' '\n\tparams: |{params}|' '\n\tpost_data: |{data}|' '\n\theaders: |{headers}|' - .format(type=client.get('_name'), + .format(version=version, method=method, path=path, params=log_params, diff --git a/resources/lib/youtube_plugin/youtube/helper/stream_info.py b/resources/lib/youtube_plugin/youtube/helper/stream_info.py index 27c902f8c..621b684ed 100644 --- a/resources/lib/youtube_plugin/youtube/helper/stream_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/stream_info.py @@ -703,8 +703,7 @@ def __init__(self, self._calculate_n = True self._cipher = None - self._auth_client = {} - self._selected_client = {} + self._selected_client = None self._client_groups = { 'custom': clients if clients else (), # Access "premium" streams, HLS and DASH @@ -1358,7 +1357,6 @@ 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 @@ -1382,11 +1380,9 @@ def load_stream_info(self, video_id): 'country', 'not available', } - reauth_reasons = { + skip_reasons = { 'age', 'inappropriate', - } - skip_reasons = { 'latest version', } retry_reasons = { @@ -1396,112 +1392,98 @@ def load_stream_info(self, video_id): } abort = 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, - } + client_data = {'json': {'videoId': video_id}} + if self._access_token: + auth = True + client_data['_access_token'] = self._access_token + else: + auth = False for name, clients in self._client_groups.items(): if not clients: continue - if name == 'mpd' and not (use_mpd or use_remote_history): + if name == 'mpd' and not use_mpd: continue if name == 'ask' and use_mpd and not ask_for_quality: continue - restart = False - while 1: - for client_name in clients: - _client = self.build_client(client_name, client_data) - if not _client: - continue + status = None - _result = self.request( - video_info_url, - 'POST', - response_hook=self._response_hook_json, - error_title='Player request failed', - error_hook=self._error_hook, - error_hook_kwargs={ - 'video_id': video_id, - 'client': client_name, - 'auth': _client.get('_has_auth', False), - }, - **_client - ) or {} - - 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' - - if (age_gate_enabled - and playability.get('desktopLegacyAgeGateReason')): - abort = True + for client_name in clients: + _client = self.build_client(client_name, client_data) + if not _client: + continue + + _result = self.request( + video_info_url, + 'POST', + response_hook=self._response_hook_json, + error_title='Player request failed', + error_hook=self._error_hook, + error_hook_kwargs={ + 'video_id': video_id, + 'client': client_name, + 'auth': _client.get('_has_auth', False), + }, + **_client + ) or {} + + 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' + + if (age_gate_enabled + and playability.get('desktopLegacyAgeGateReason')): + abort = True + break + elif status == 'LIVE_STREAM_OFFLINE': + 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', + 'UNPLAYABLE', + }: + log_warning( + 'Failed to retrieve video info' + '\n\tStatus: {status}' + '\n\tReason: {reason}' + '\n\tvideo_id: |{video_id}|' + '\n\tClient: |{client}|' + '\n\tAuth: |{auth}|' + .format( + status=status, + reason=reason or 'UNKNOWN', + video_id=video_id, + client=_client['_name'], + auth=_client.get('_has_auth', False), + ) + ) + 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 - elif status == 'LIVE_STREAM_OFFLINE': + if any(why in compare_reason for why in abort_reasons): 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', - 'UNPLAYABLE', - }: - log_warning( - 'Failed to retrieve video info' - '\n\tStatus: {status}' - '\n\tReason: {reason}' - '\n\tvideo_id: |{video_id}|' - '\n\tClient: |{client}|' - '\n\tAuth: |{auth}|' - .format( - status=status, - reason=reason or 'UNKNOWN', - video_id=video_id, - client=_client['_name'], - auth=_client.get('_has_auth', False), - ) - ) - compare_reason = reason.lower() - if any(why in compare_reason for why in reauth_reasons): - if has_access_token: - client_data['_auth_required'] = True - restart = True - break - 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: - log_debug( - 'Unknown playabilityStatus in player response' - '\n\tplayabilityStatus: {0}' - .format(playability) - ) else: - break - if not restart: - break - restart = False + log_debug( + 'Unknown playabilityStatus in player response' + '\n\tplayabilityStatus: {0}' + .format(playability) + ) if abort: break @@ -1515,19 +1497,14 @@ def load_stream_info(self, video_id): .format( video_id=video_id, client=client_name, - auth=_client.get('_has_auth', False), + auth=bool(_client.get('_access_token')), ) ) if not self._selected_client: - 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, - } + 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: @@ -1559,9 +1536,6 @@ 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 @@ -1570,7 +1544,8 @@ def load_stream_info(self, video_id): # curl_headers = self._make_curl_headers(headers, cookies) curl_headers = self._prepare_headers(client['headers']) - video_details = result.get('videoDetails', {}) + microformat = (result.get('microformat', {}) + .get('playerMicroformatRenderer', {})) is_live = video_details.get('isLiveContent', False) if is_live: is_live = video_details.get('isLive', False) @@ -1580,8 +1555,6 @@ 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', '') @@ -1611,14 +1584,12 @@ def load_stream_info(self, video_id): 'subtitles': None, } - if use_remote_history and self._auth_client: + if settings.use_remote_history(): playback_stats = { 'playback_url': 'videostatsPlaybackUrl', 'watchtime_url': 'videostatsWatchtimeUrl', } - playback_tracking = (self._auth_client - .get('result', {}) - .get('playbackTracking', {})) + playback_tracking = result.get('playbackTracking', {}) cpn = self._generate_cpn() for key, url_key in playback_stats.items(): @@ -1715,7 +1686,7 @@ def load_stream_info(self, video_id): error_hook_kwargs={ 'video_id': video_id, 'client': client_name, - 'auth': _client.get('_has_auth', False), + 'auth': bool(caption_client.get('_access_token')), }, **caption_client )