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

Code before:
        playlist_items = []
        for key, value in json_content.get('video').get('clipsByCategory').items():
            # We need to do a case insensitive match. If anyone has a better way, feel free to improve.
            if key.lower() == playlist_id:
                for video in value.get('items'):
                    playlist_items.append({
                        '_type': 'url',
                        'url': 'https://www.cbc.ca/player/play/%s' % video['id'],
                        'ie_key': 'CBCPlayer',
                        'timestamp': video['airDate'],
                        'categories': [video['contentArea']],
                        'description': video['description'],
                        'duration': video['duration'],
                        'id': video['id'],
                        'is_live': video['isLive'],
                        'series': video['showName'],
                        'thumbnail': video['thumbnail'],
                        'title': video['title'],
                    })
                break
        return {
            '_type': 'playlist',
            'id': playlist_id,
            'ie_key': 'CBCPlayer',
            'entries': playlist_items,
        }


class CBCGemIE(InfoExtractor):
    IE_NAME = 'gem.cbc.ca'
    _VALID_URL = r'https?://gem\.cbc\.ca/(?:media/)?(?P<id>[0-9a-z-]+/s[0-9]+[a-z][0-9]+)'
    _TESTS = [{
        # This is a normal, public, TV show video
        'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
        'md5': '93dbb31c74a8e45b378cf13bd3f6f11e',
        'info_dict': {
            'id': 'schitts-creek/s06e01',
            'ext': 'mp4',
            'title': 'Smoke Signals',
            'description': 'md5:929868d20021c924020641769eb3e7f1',
            'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_06e01_thumbnail_v01.jpg?im=Resize=(Size)',
            'duration': 1314,
            'categories': ['comedy'],
            'series': 'Schitt\'s Creek',
            'season': 'Season 6',
            'season_number': 6,
            'episode': 'Smoke Signals',
            'episode_number': 1,
            'episode_id': 'schitts-creek/s06e01',
        },
        'params': {'format': 'bv'},

Review comment (refactor):
- Do not do `a.get('b').get('c')`, use `traverse_obj(a, ('b', 'c'))`
- Use `traverse_obj` with for loop, it will be non-fatal and it can be used to filter
- Metadata extraction should be non-fatal (not risking a KeyError), again use `traverse_obj` here
- Use `int_or_none` or `float_or_none` with metadata fields that need to be int/float
- video `id` should be string, use `str_or_none` (import the `or_none` helpers from utils)
- f-string is preferred for `url`
- Use a generator func instead of append loop
- Don't pass the video extractor's `ie_key` in the playlist result dict

```suggestion
        def entries():
            for video in traverse_obj(json_content, (
                'video', 'clipsByCategory', lambda k, _: k.lower() == playlist_id, 'items', lambda _, v: v['id']
            )):
                yield self.url_result(
                    f'https://www.cbc.ca/player/play/{video["id"]}', CBCPlayerIE,
                    **traverse_obj(video, {
                        'id': ('id', {str_or_none}),
                        'title': 'title',
                        'description': 'description',
                        'series': 'showName',
                        'categories': ('contentArea', {lambda x: [x] if x else None}),
                        'timestamp': ('airDate', {int_or_none}),
                        'duration': ('duration', {int_or_none}),
                        'is_live': ('isLive', {bool}),
                        'thumbnail': ('thumbnail', {url_or_none}),
                    }))

        return self.playlist_result(entries(), playlist_id)
```

Code after:
        return self.playlist_result(entries(), playlist_id)


class CBCGemIE(InfoExtractor):
    IE_NAME = 'gem.cbc.ca'
    _VALID_URL = r'https?://gem\.cbc\.ca/(?:media/)?(?P<id>[0-9a-z-]+/s[0-9]+[a-z][0-9]+)'
    _TESTS = [{
        # This is a normal, public, TV show video
        'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
        'md5': '93dbb31c74a8e45b378cf13bd3f6f11e',
        'info_dict': {
            'id': 'schitts-creek/s06e01',
            'ext': 'mp4',
            'title': 'Smoke Signals',
            'description': 'md5:929868d20021c924020641769eb3e7f1',
            'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_06e01_thumbnail_v01.jpg?im=Resize=(Size)',
            'duration': 1314,
            'categories': ['comedy'],
            'series': 'Schitt\'s Creek',
            'season': 'Season 6',
            'season_number': 6,
            'episode': 'Smoke Signals',
            'episode_number': 1,
            'episode_id': 'schitts-creek/s06e01',
        },
        'params': {'format': 'bv'},
        'skip': 'Geo-restricted to Canada',
    }, {
        # This video requires an account in the browser, but works fine in yt-dlp
        'url': 'https://gem.cbc.ca/media/schitts-creek/s01e01',
        'md5': '297a9600f554f2258aed01514226a697',
        'info_dict': {
            'id': 'schitts-creek/s01e01',
            'ext': 'mp4',
            'title': 'The Cup Runneth Over',
            'description': 'md5:9bca14ea49ab808097530eb05a29e797',
            'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_01e01_thumbnail_v01.jpg?im=Resize=(Size)',
            'series': 'Schitt\'s Creek',
            'season_number': 1,
            'season': 'Season 1',
            'episode_number': 1,
            'episode': 'The Cup Runneth Over',
            'episode_id': 'schitts-creek/s01e01',
            'duration': 1309,
            'categories': ['comedy'],
        },
        'params': {'format': 'bv'},
        'skip': 'Geo-restricted to Canada',
    }, {
        'url': 'https://gem.cbc.ca/nadiyas-family-favourites/s01e01',
        'only_matching': True,