Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/youtube.py (Python)

Code before:
    def _extract_player_responses(self, clients, video_id, webpage, master_ytcfg, player_url, identity_token):
        initial_pr = None
        if webpage:
            initial_pr = self._extract_yt_initial_variable(
                webpage, self._YT_INITIAL_PLAYER_RESPONSE_RE,
                video_id, 'initial player response')

        original_clients = clients
        clients = clients[::-1]
        creator_client_appended = False

        while clients:
            client = clients.pop()
            player_ytcfg = master_ytcfg if client == 'web' else {}
            if 'configs' not in self._configuration_arg('player_skip'):
                player_ytcfg = self._extract_player_ytcfg(client, video_id) or player_ytcfg

            pr = (
                initial_pr if client == 'web' and initial_pr
                else self._extract_player_response(
                    client, video_id, player_ytcfg or master_ytcfg, player_ytcfg, identity_token, player_url, initial_pr))
            if pr:
                yield pr

            if self._is_agegated(pr):
                if client.endswith('_agegate'):
                    # _creator clients can bypass AGE_VERIFICATION_REQUIRED if logged in
                    if not self._generate_sapisidhash_header():
                        continue
                    client = '%s_creator' % client.remove_end('_agegate')
                else:
                    client = f'{client}_agegate'
                if bypass_client in INNERTUBE_CLIENTS and bypass_client not in original_clients:
                    clients.append(client)

        # Android player_response does not have microFormats which are needed for
        # extraction of some data. So we return the initial_pr with formats
        # stripped out even if not requested by the user
        # See: https://github.com/yt-dlp/yt-dlp/issues/501
        if initial_pr and 'web' not in original_clients:
            initial_pr['streamingData'] = None
            yield initial_pr

    def _extract_formats(self, streaming_data, video_id, player_url, is_live):
        itags, stream_ids = [], []
        itag_qualities, res_qualities = {}, {}
        q = qualities([
            # Normally tiny is the smallest video-only formats. But
            # audio-only formats with unknown quality may get tagged as tiny
            'tiny',
            'audio_quality_ultralow', 'audio_quality_low', 'audio_quality_medium', 'audio_quality_high',  # Audio only formats

Review comment (bug):
The condition `is_agegated` AND `client.endswith('_agegate')` never seems to apply. Because if an agegate client fails, the status is `UNPLAYABLE` and `desktopLegacyAgeGateReason` does not exist in the response. This is not covered by the `_is_agegated()` function. In my case, the creator client is never tried.

Response from `clientScreen:EMBED` agegate client:
```
  "playabilityStatus": {
    "status": "UNPLAYABLE",
    "reason": "Video unavailable",
    "errorScreen": {
      "playerErrorMessageRenderer": {
        "subreason": {
          "runs": [
            {
              "text": "Playback on other websites has been disabled by the video owner."
            }
          ]
        },
        "reason": {
          "simpleText": "Video unavailable"
        },
        .....
```
... `_is_agegated()` returns `False` and the creator client will not be appended.

Output:
```
PS E:\repos\yt-dlp\yt_dlp> python __main__.py https://youtube.com/watch?v=Cr381pDsSsA --cookies "C:\Users\David\Downloads\youtube.com_cookies.txt"
[youtube] Cr381pDsSsA: Downloading webpage
[youtube] Cr381pDsSsA: Downloading android player API JSON
[youtube] Cr381pDsSsA: Downloading android agegate player API JSON
[youtube] Cr381pDsSsA: Downloading web agegate player API JSON
ERROR: Playback on other websites has been disabled by the video owner.
```

When I force the creator client with `--extractor-args youtube:player_client=web_creator`, it works.

Code after:
                video_id, 'initial player response')

        original_clients = clients
        clients = clients[::-1]

        def append_client(client_name):
            if client_name in INNERTUBE_CLIENTS and client_name not in original_clients:
                clients.append(client_name)

        while clients:
            client = clients.pop()
            player_ytcfg = master_ytcfg if client == 'web' else {}
            if 'configs' not in self._configuration_arg('player_skip'):
                player_ytcfg = self._extract_player_ytcfg(client, video_id) or player_ytcfg

            pr = (
                initial_pr if client == 'web' and initial_pr
                else self._extract_player_response(
                    client, video_id, player_ytcfg or master_ytcfg, player_ytcfg, identity_token, player_url, initial_pr))
            if pr:
                yield pr

            # creator clients can bypass AGE_VERIFICATION_REQUIRED if logged in
            if client.endswith('_agegate') and self._is_unplayable(pr) and self._generate_sapisidhash_header():
                append_client(client.replace('_agegate', '_creator'))
            elif self._is_agegated(pr):
                append_client(f'{client}_agegate')

        # Android player_response does not have microFormats which are needed for
        # extraction of some data. So we return the initial_pr with formats
        # stripped out even if not requested by the user
        # See: https://github.com/yt-dlp/yt-dlp/issues/501
        if initial_pr and 'web' not in original_clients:
            initial_pr['streamingData'] = None
            yield initial_pr

    def _extract_formats(self, streaming_data, video_id, player_url, is_live):
        itags, stream_ids = [], []
        itag_qualities, res_qualities = {}, {}
        q = qualities([
            # Normally tiny is the smallest video-only formats. But
            # audio-only formats with unknown quality may get tagged as tiny
            'tiny',
            'audio_quality_ultralow', 'audio_quality_low', 'audio_quality_medium', 'audio_quality_high',  # Audio only formats
            'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'
        ])
        streaming_formats = traverse_obj(streaming_data, (..., ('formats', 'adaptiveFormats'), ...), default=[])

        for fmt in streaming_formats:
            if fmt.get('targetDurationSec') or fmt.get('drmFamilies'):
                continue