- def _real_extract(self, url):
- url, smuggled_data = unsmuggle_url(url, {})
- video_id = self._match_id(url)
-
- is_music_url = smuggled_data.get('is_music_url') or self.is_music_url(url)
-
- base_url = self.http_scheme() + '//www.youtube.com/'
- webpage_url = base_url + 'watch?v=' + video_id
- webpage = self._download_webpage(
- webpage_url + '&bpctr=9999999999&has_verified=1', video_id, fatal=False)
-
- ytcfg = self._extract_ytcfg(video_id, webpage) or self._get_default_ytcfg()
- identity_token = self._extract_identity_token(webpage, video_id)
- syncid = self._extract_account_syncid(ytcfg)
- headers = self._generate_api_headers(ytcfg, identity_token, syncid)
-
- player_url = self._extract_player_url(ytcfg, webpage)
-
- player_client = (self._configuration_arg('player_client') or [''])[0]
- if player_client not in ('web', 'android', ''):
- self.report_warning(f'Invalid player_client {player_client} given. Falling back to WEB')
- force_mobile_client = player_client == 'android'
- player_skip = self._configuration_arg('player_skip')
-
- def get_text(x):
- if not x:
- return
- text = x.get('simpleText')
- if text and isinstance(text, compat_str):
- return text
- runs = x.get('runs')
- if not isinstance(runs, list):
- return
- return ''.join([r['text'] for r in runs if isinstance(r.get('text'), compat_str)])
-
- ytm_streaming_data = {}
- if is_music_url:
- ytm_webpage = None
- sts = self._extract_signature_timestamp(video_id, player_url, ytcfg, fatal=False)
- if sts and not force_mobile_client and 'configs' not in player_skip:
- ytm_webpage = self._download_webpage(
- 'https://music.youtube.com',
- video_id, fatal=False, note="Downloading remix client config")
-
- ytm_cfg = self._extract_ytcfg(video_id, ytm_webpage) or {}
- ytm_client = 'WEB_REMIX'
- if not sts or force_mobile_client:
- # Android client already has signature descrambled
- # See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
- if not sts:
- self.report_warning('Falling back to mobile remix client for player API.')
- ytm_client = 'ANDROID_MUSIC'
- ytm_cfg = {}
-
- ytm_headers = self._generate_api_headers(
- ytm_cfg, identity_token, syncid,
- client=ytm_client)
- ytm_query = {'videoId': video_id}
- ytm_query.update(self._generate_player_context(sts))
-
- ytm_player_response = self._extract_response(
- item_id=video_id, ep='player', query=ytm_query,
- ytcfg=ytm_cfg, headers=ytm_headers, fatal=False,
- default_client=ytm_client,
- note='Downloading %sremix player API JSON' % ('mobile ' if force_mobile_client else ''))
-
- ytm_streaming_data = try_get(ytm_player_response, lambda x: x['streamingData']) or {}
- player_response = None
- if webpage:
- player_response = self._extract_yt_initial_variable(
- webpage, self._YT_INITIAL_PLAYER_RESPONSE_RE,
- video_id, 'initial player response')
-
- if not player_response or force_mobile_client:
- sts = self._extract_signature_timestamp(video_id, player_url, ytcfg, fatal=False)
- yt_client = 'WEB'
- ytpcfg = ytcfg
- ytp_headers = headers
- if not sts or force_mobile_client:
- # Android client already has signature descrambled
- # See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
- if not sts:
- self.report_warning('Falling back to mobile client for player API.')
- yt_client = 'ANDROID'
- ytpcfg = {}
- ytp_headers = self._generate_api_headers(ytpcfg, identity_token, syncid, yt_client)
-
- yt_query = {'videoId': video_id}
- yt_query.update(self._generate_player_context(sts))
- player_response = self._extract_response(
- item_id=video_id, ep='player', query=yt_query,
- ytcfg=ytpcfg, headers=ytp_headers, fatal=False,
- default_client=yt_client,
- note='Downloading %splayer API JSON' % ('mobile ' if force_mobile_client else '')
- )
-
- # Age-gate workarounds
- playability_status = player_response.get('playabilityStatus') or {}
- if playability_status.get('reason') in self._AGE_GATE_REASONS:
- pr = self._parse_json(try_get(compat_parse_qs(
- self._download_webpage(
- base_url + 'get_video_info', video_id,
- 'Refetching age-gated info webpage', 'unable to download video info webpage',
- query=self._get_video_info_params(video_id), fatal=False)),
- lambda x: x['player_response'][0],
- compat_str) or '{}', video_id)
- if not pr:
- self.report_warning('Falling back to embedded-only age-gate workaround.')
- embed_webpage = None
- sts = self._extract_signature_timestamp(video_id, player_url, ytcfg, fatal=False)
- if sts and not force_mobile_client and 'configs' not in player_skip:
- embed_webpage = self._download_webpage(
- 'https://www.youtube.com/embed/%s?html5=1' % video_id,
- video_id=video_id, note='Downloading age-gated embed config')
-
- ytcfg_age = self._extract_ytcfg(video_id, embed_webpage) or {}
- # If we extracted the embed webpage, it'll tell us if we can view the video
- embedded_pr = self._parse_json(
- try_get(ytcfg_age, lambda x: x['PLAYER_VARS']['embedded_player_response'], str) or '{}',
- video_id=video_id)
- embedded_ps_reason = try_get(embedded_pr, lambda x: x['playabilityStatus']['reason'], str) or ''
- if embedded_ps_reason not in self._AGE_GATE_REASONS:
- yt_client = 'WEB_EMBEDDED_PLAYER'
- if not sts or force_mobile_client:
- # Android client already has signature descrambled
- # See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
- if not sts:
- self.report_warning(
- 'Falling back to mobile embedded client for player API (note: some formats may be missing).')
- yt_client = 'ANDROID_EMBEDDED_PLAYER'
- ytcfg_age = {}
-
- ytage_headers = self._generate_api_headers(
- ytcfg_age, identity_token, syncid, client=yt_client)
- yt_age_query = {'videoId': video_id}
- yt_age_query.update(self._generate_player_context(sts))
- pr = self._extract_response(
- item_id=video_id, ep='player', query=yt_age_query,
- ytcfg=ytcfg_age, headers=ytage_headers, fatal=False,
- default_client=yt_client,
- note='Downloading %sage-gated player API JSON' % ('mobile ' if force_mobile_client else '')
- ) or {}
-
- if pr:
- player_response = pr
-
- trailer_video_id = try_get(
- playability_status,
- lambda x: x['errorScreen']['playerLegacyDesktopYpcTrailerRenderer']['trailerVideoId'],
- compat_str)
- if trailer_video_id:
- return self.url_result(
- trailer_video_id, self.ie_key(), trailer_video_id)
-
- search_meta = (
- lambda x: self._html_search_meta(x, webpage, default=None)) \
- if webpage else lambda x: None
-
- video_details = player_response.get('videoDetails') or {}
- microformat = try_get(
- player_response,
- lambda x: x['microformat']['playerMicroformatRenderer'],
- dict) or {}
- video_title = video_details.get('title') \
- or get_text(microformat.get('title')) \
- or search_meta(['og:title', 'twitter:title', 'title'])
- video_description = video_details.get('shortDescription')
-
- if not smuggled_data.get('force_singlefeed', False):
- if not self.get_param('noplaylist'):
- multifeed_metadata_list = try_get(
- player_response,
- lambda x: x['multicamera']['playerLegacyMulticameraRenderer']['metadataList'],
- compat_str)
- if multifeed_metadata_list:
- entries = []
- feed_ids = []
- for feed in multifeed_metadata_list.split(','):
- # Unquote should take place before split on comma (,) since textual
- # fields may contain comma as well (see
- # https://github.com/ytdl-org/youtube-dl/issues/8536)
- feed_data = compat_parse_qs(
- compat_urllib_parse_unquote_plus(feed))
-
- def feed_entry(name):
- return try_get(
- feed_data, lambda x: x[name][0], compat_str)
-
- feed_id = feed_entry('id')
- if not feed_id:
- continue
- feed_title = feed_entry('title')
- title = video_title
- if feed_title:
- title += ' (%s)' % feed_title
- entries.append({
- '_type': 'url_transparent',
- 'ie_key': 'Youtube',
- 'url': smuggle_url(
- base_url + 'watch?v=' + feed_data['id'][0],
- {'force_singlefeed': True}),
- 'title': title,
- })
- feed_ids.append(feed_id)
- self.to_screen(
- 'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
- % (', '.join(feed_ids), video_id))
- return self.playlist_result(
- entries, video_id, video_title, video_description)
+ @staticmethod
+ def _is_agegated(player_response):
+ if traverse_obj(player_response, ('playabilityStatus', 'desktopLegacyAgeGateReason')):
+ return True
+
+ reasons = traverse_obj(player_response, ('playabilityStatus', ('status', 'reason')), default=[])
+ AGE_GATE_REASONS = (
+ 'confirm your age', 'age-restricted', 'inappropriate', # reason
+ 'age_verification_required', 'age_check_required', # status
+ )
+ return any(expected in reason for expected in AGE_GATE_REASONS for reason in reasons)
+
+ @staticmethod
+ def _is_unplayable(player_response):
+ return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
+
+ def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, player_url, initial_pr):
+
+ session_index = self._extract_session_index(player_ytcfg, master_ytcfg)
+ syncid = self._extract_account_syncid(player_ytcfg, master_ytcfg, initial_pr)
+ sts = self._extract_signature_timestamp(video_id, player_url, master_ytcfg, fatal=False) if player_url else None
+ headers = self.generate_api_headers(
+ ytcfg=player_ytcfg, account_syncid=syncid, session_index=session_index, default_client=client)
+
+ yt_query = {'videoId': video_id}
+ yt_query.update(self._generate_player_context(sts))
+ return self._extract_response(
+ item_id=video_id, ep='player', query=yt_query,
+ ytcfg=player_ytcfg, headers=headers, fatal=True,
+ default_client=client,
+ note='Downloading %s player API JSON' % client.replace('_', ' ').strip()
+ ) or None
+
+ def _get_requested_clients(self, url, smuggled_data):
+ requested_clients = []
+ allowed_clients = sorted(
+ [client for client in INNERTUBE_CLIENTS.keys() if client[:1] != '_'],
+ key=lambda client: INNERTUBE_CLIENTS[client]['priority'], reverse=True)
+ for client in self._configuration_arg('player_client'):
+ if client in allowed_clients:
+ requested_clients.append(client)
+ elif client == 'all':
+ requested_clients.extend(allowed_clients)