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

Code before:
                thumbnails.append(thumbnail)

        chapter_marks = t.get('streamAnchorTag') or []
        chapter_marks.append({'anchorOffset': int_or_none(t.get('duration'))})
        chapters = [{
            'start_time': chap.get('anchorOffset'),
            'end_time': next_chap.get('anchorOffset'),
            'title': chap.get('anchorLabel'),
        } for chap, next_chap in zip(chapter_marks, chapter_marks[1:])]

        target_info = traverse_obj(content, ('programmeItem', 0, 'http://zdf.de/rels/target'))
        return merge_dicts(info, {
            'title': title,
            'description': content.get('leadParagraph') or content.get('teasertext'),
            'duration': int_or_none(t.get('duration')),
            'timestamp': unified_timestamp(content.get('editorialDate')),
            'thumbnails': thumbnails,
            'chapters': chapters or None,
            'seriesId': traverse_obj(target_info, ('http://zdf.de/rels/cmdm/series', 'seriesUuid')),
            'series': traverse_obj(target_info, ('http://zdf.de/rels/cmdm/series', 'seriesTitle')),
            'season': traverse_obj(target_info, ('http://zdf.de/rels/cmdm/season', 'seasonTitle')),
            'season_number': traverse_obj(target_info, ('http://zdf.de/rels/cmdm/season', 'seasonNumber')),
            'season_id': traverse_obj(target_info, ('http://zdf.de/rels/cmdm/season', 'seasonUuid')),
            'episode': title,
            'episode_number': traverse_obj(target_info, 'episodeNumber'),
            'episode_id': traverse_obj(target_info, 'contentId'),
        })

    def _extract_regular(self, url, player, video_id):
        player_content_v2 = player['content']
        player_content_v3 = update_url_query(player_content_v2, {'profile': 'player-3'})

        content = try_call(
            lambda: self._call_api(player_content_v3, video_id, 'content', player['apiToken'], url),
            lambda: self._call_api(player_content_v2, video_id, 'content', player['apiToken'], url),
        )

        return self._extract_entry(player_content_v2, player, content, video_id)

    def _extract_mobile(self, video_id):
        video = self._download_json(
            'https://zdf-cdn.live.cellular.de/mediathekV2/document/%s' % video_id,
            video_id)

        formats = []
        formitaeten = try_get(video, lambda x: x['document']['formitaeten'], list)
        document = formitaeten and video['document']
        if formitaeten:
            title = document['titel']
            content_id = document['basename']


Review comment (suggestion):
You could do

```py
            'episode': title,
            **traverse_obj(content, ('programmeItem', 0, 'http://zdf.de/rels/target', {
                'series_id': ('http://zdf.de/rels/cmdm/series', 'seriesUuid'),
                'series': ('http://zdf.de/rels/cmdm/series', 'seriesTitle'),
                ...
            })),
```

not mandatory though

Code after:
                    thumbnail.update({
                        'width': int(mobj.group('width')),
                        'height': int(mobj.group('height')),
                    })
                thumbnails.append(thumbnail)

        chapter_marks = t.get('streamAnchorTag') or []
        chapter_marks.append({'anchorOffset': int_or_none(t.get('duration'))})
        chapters = [{
            'start_time': chap.get('anchorOffset'),
            'end_time': next_chap.get('anchorOffset'),
            'title': chap.get('anchorLabel'),
        } for chap, next_chap in zip(chapter_marks, chapter_marks[1:])]

        return merge_dicts(info, {
            'title': title,
            'description': content.get('leadParagraph') or content.get('teasertext'),
            'duration': int_or_none(t.get('duration')),
            'timestamp': unified_timestamp(content.get('editorialDate')),
            'thumbnails': thumbnails,
            'chapters': chapters or None,
            'episode': title,
            **traverse_obj(content, ('programmeItem', 0, 'http://zdf.de/rels/target', {
                'series_id': ('http://zdf.de/rels/cmdm/series', 'seriesUuid', {str}),
                'series': ('http://zdf.de/rels/cmdm/series', 'seriesTitle', {str}),
                'season': ('http://zdf.de/rels/cmdm/season', 'seasonTitle', {str}),
                'season_number': ('http://zdf.de/rels/cmdm/season', 'seasonNumber', {int_or_none}),
                'season_id': ('http://zdf.de/rels/cmdm/season', 'seasonUuid', {str}),
                'episode_number': ('episodeNumber', {int_or_none}),
                'episode_id': ('contentId', {str}),
            })),
        })

    def _extract_regular(self, url, player, video_id):
        player_url = player['content']

        try:
            content = self._call_api(
                update_url_query(player_url, {'profile': 'player-3'}),
                video_id, 'content', player['apiToken'], url)
        except ExtractorError as e:
            self.report_warning(f'{video_id}: {e.orig_msg}; retrying with v2 profile')
            content = self._call_api(
                player_url, video_id, 'content', player['apiToken'], url)

        return self._extract_entry(player_url, player, content, video_id)

    def _extract_mobile(self, video_id):
        video = self._download_v2_doc(video_id)

        formats = []