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

Code before:
                'thumbnail': thumbnail,
                'series': show_info.get('title'),
                'season_number': season_info.get('season'),
                'season': season_info['title'],
            }

        elif re.fullmatch(self._VIDEO_ID, url_id) is None:
            # Not a playlist or video
            raise ExtractorError(f"Could't recognize ID '{url_id}' as a playlist or video")

        # It's a single video

        video_id = url_id
        video_info = self._download_json(self._API_VIDEO + video_id, video_id)
        m3u8_info = self._download_json(video_info['playSession']['url'], video_id)

        if m3u8_info.get('errorCode') == 1:
            # errorCode 1 means geo-blocked
            self.raise_geo_restricted(countries=['CA'])
        elif m3u8_info.get('errorCode') not in [35, 0, None]:
            # 35 means media unavailable, which is handled below. 0 means no
            # error. None is there just in case. Any other errorCode can't be
            # handled as we don't know what it is.
            raise ExtractorError(
                f"Unknown errorCode {m3u8_info.get('errorCode')}, with message: {m3u8_info.get('message')}"
            )

        m3u8_url = m3u8_info.get('url')

        # Sometimes the m3u8 URL is a not available, and errorCode is 35.
        # This might only happen with member content. But just retrying seems
        # to work fine.
        i = 0
        while m3u8_url is None:
            if i == 15:
                # Didn't work after 15 tries, give up
                raise ExtractorError("Couldn't retrieve m3u8 URL")
            m3u8_url = self._download_json(
                video_info['playSession']['url'], video_id).get('url')
            i += 1

        formats = self._extract_m3u8_formats_and_subtitles(
            m3u8_url, video_id, m3u8_id='hls')[0]
        self._remove_duplicate_formats(formats)

        # Post-process format list
        for i, format in enumerate(formats):
            if format.get('vcodec') == 'none':
                # Audio-only

                # Only AAC audio is used

Review comment (suggestion):
Error message should be like `IE_NAME said: code - message`. Also set `expected=True`

Code after:
            'duration': video_info.get('duration'),
            'categories': [video_info.get('category')],
            'formats': formats,
            'release_timestamp': video_info.get('airDate'),
            'timestamp': video_info.get('availableDate'),
        }


class CBCGemPlaylistIE(InfoExtractor):
    IE_NAME = 'gem.cbc.ca:playlist'
    _VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>(?P<show>[0-9a-z-]+)/s(?P<season>[0-9]+))/?(?:[?#]|$)'
    _TESTS = [{
        # geo-restricted to Canada, bypassable
        # TV show playlist, all public videos
        'url': 'https://gem.cbc.ca/media/schitts-creek/s06',
        'playlist_count': 16,
        'info_dict': {
            'id': 'schitts-creek/s06',
            'title': 'Season 6',
            'description': 'md5:6a92104a56cbeb5818cc47884d4326a2',
        },
        'skip': 'Geo-restricted to Canada',
    }]
    _API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/shows/'

    def _real_extract(self, url):
        match = self._match_valid_url(url)
        season_id = match.group('id')
        show = match.group('show')
        show_info = self._download_json(self._API_BASE + show, season_id)
        season = int(match.group('season'))
        season_info = try_get(show_info, lambda x: x['seasons'][season - 1])

        if season_info is None:
            raise ExtractorError(f'Couldn\'t find season {season} of {show}')

        episodes = []
        for episode in season_info['assets']:
            episodes.append({
                '_type': 'url_transparent',
                'ie_key': 'CBCGem',
                'url': 'https://gem.cbc.ca/media/' + episode['id'],
                'id': episode['id'],
                'title': episode.get('title'),
                'description': episode.get('description'),
                'thumbnail': episode.get('image'),
                'series': episode.get('series'),
                'season_number': episode.get('season'),
                'season': season_info['title'],
                'season_id': season_info.get('id'),
                'episode_number': episode.get('episode'),