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

Code before:
    def _real_extract(self, url):
        # podcast url is always after last backlash
        video_id = self._match_id(url)
        data = self._download_json(url + '.json', None)

        title = data.get('title')
        description = data.get('description')
        duration = data.get('duration')
        thumbnail = traverse_obj(data, ('image', 'url'), ('series', 'image', 'url'))
        creators = [traverse_obj(data, ('series', 'author'))]

        video_url = join_nonempty('https', self._search_regex(r'redirect.mp3/(.*)', data['url'], 'redirect'), delim='://')
        if not video_url:
            raise ExtractorError('URL to podcast not found', expected=True)
        formats = [{
            'url': video_url,
            'ext': determine_ext(video_url, default_ext=''),
        }]
        return {
            'id': video_id,
            'thumbnail': thumbnail,
            'title': title,
            'creators': creators,
            'description': description,
            'duration': duration,
            'formats': formats,
        }


Review comment (security):
We can unpack a single `traverse_obj` call in the return dict

- use `{str}` to check the type of the `title`
- import `clean_html` from utils; we need this to remove the html tags from the `description`
- when extracting `duration` it's best to transform it with `int_or_none` so that we end up with an `int` or...`None`. you'll need to import this from utils
- import `url_or_none` from utils as well. And we don't want `thumbnail` to be a list, so we need to use `any` at the end of the traversal path to reset branching and return only the first result
- for `creators` we can use `all` to always return a list -- like `any` it will reset branching but then it achieves the opposite effect in that it will always return a list of all results. We can `filter` out empty strings before `all` and `filter` out empty lists after `all`
```suggestion
            **traverse_obj(data, {
                'title': ('title', {str}),
                'description': ('description', {clean_html}),
                'duration': ('duration', {int_or_none}),
                'thumbnail': (('image', ('series', 'image')), 'url', {url_or_none}, any),
                'creators': ('series', 'author', {str}, filter, all, filter),
            }),
```

Code after:
            'duration': 1510,
            'display_id': 'md5:b52ecacaefab891b59db69721bfd9b13',
            'description': 'md5:52a39e36d08d8919527454f152ad3c25',
            'timestamp': 1659102055,
            'upload_date': '20220729',
        },
    }]

    def _real_extract(self, url):
        display_id, url = self._match_valid_url(url).group('id', 'url')
        data = self._download_json(f'{url}.json', display_id)

        return {
            'display_id': display_id,
            'vcodec': 'none',
            **traverse_obj(data, {
                'id': ('id', {int}, {str_or_none}),
                'url': ('url', {clean_podcast_url}),
                'title': ('title', {str}),
                'description': ('description', {clean_html}),
                'duration': ('duration', {int_or_none}),
                'thumbnail': (('image', ('series', 'image')), 'url', {url_or_none}, any),
                'filesize': ('size', {int_or_none}),
                'timestamp': ('publishedAt', {int_or_none}),
                'creators': ('series', 'author', {str}, filter, all, filter),
            }),
        }