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

Code before:
            'http://api.m.afreecatv.com/catchstory/a/view', video_id, headers={'Referer': url},
            query={'aStoryListIdx': '', 'nStoryIdx': video_id}).get('data', [])
        entries = []
        for story in data:
            if story.get('story_type') != 'catch':
                break
            for catch in story.get('catch_list', []):
                # files is always a list of length 1
                files = catch.get('files', [])
                if not files:
                    continue
                file = files[0]
                entries.append({
                    'id': file.get('file_info_key'),
                    'title': catch.get('title'),
                    'uploader': catch.get('writer_nick'),
                    'uploader_id': catch.get('writer_id'),
                    'thumbnail': catch.get('thumb'),
                    'duration': file.get('duration'),
                    'formats': [{
                        'url': file.get('file'),
                        'format_id': 'http',
                    }],
                    'timestamp': file.get('write_timestamp'),
                })
        return self.playlist_result(entries, video_id)


class AfreecaTVLiveIE(AfreecaTVBaseIE):
    IE_NAME = 'afreecatv:live'
    IE_DESC = 'afreecatv.com livestreams'
    _VALID_URL = r'https?://play\.afreeca(?:tv)?\.com/(?P<id>[^/]+)(?:/(?P<bno>\d+))?'
    _TESTS = [{
        'url': 'https://play.afreecatv.com/pyh3646/237852185',
        'info_dict': {
            'id': '237852185',
            'ext': 'mp4',
            'title': '【 우루과이 오늘은 무슨일이? 】',
            'uploader': '박진우[JINU]',
            'uploader_id': 'pyh3646',
            'timestamp': 1640661495,
            'is_live': True,
        },
        'skip': 'Livestream has ended',
    }, {
        'url': 'https://play.afreecatv.com/pyh3646/237852185',
        'only_matching': True,
    }, {
        'url': 'https://play.afreecatv.com/pyh3646',
        'only_matching': True,
    }]

Review comment (suggestion):
- avoid append loops for entries, use a generator/PagedList when possible
- we can use traversal to filter for what we need, check value types, and transform values
- `write_timestamp` is found in `catch`, not `file`
- `duration` is in milliseconds, we need to convert
- the URLs are m3u8s, so `http` is not correct as `format_id`
- we can just return `url` in the info dict root instead of a `formats` field
- we can move the entries generator to a static method

```suggestion

        return self.playlist_result(self._entries(data), video_id)

    @staticmethod
    def _entries(data):
        # 'files' is always a list with 1 element
        yield from traverse_obj(data, (
            'data', lambda _, v: v['story_type'] == 'catch',
            'catch_list', lambda _, v: v['files'][0]['file'], {
                'id': ('files', 0, 'file_info_key', {str}),
                'url': ('files', 0, 'file', {url_or_none}),
                'duration': ('files', 0, 'duration', {functools.partial(int_or_none, scale=1000)}),
                'title': ('title', {str}),
                'uploader': ('writer_nick', {str}),
                'uploader_id': ('writer_id', {str}),
                'thumbnail': ('thumb', {url_or_none}),
                'timestamp': ('write_timestamp', {int_or_none}),
            }))
```

Code after:
            query={'aStoryListIdx': '', 'nStoryIdx': video_id}, impersonate=True)

        return self.playlist_result(self._entries(data), video_id)

    @staticmethod
    def _entries(data):
        # 'files' is always a list with 1 element
        yield from traverse_obj(data, (
            'data', lambda _, v: v['story_type'] == 'catch',
            'catch_list', lambda _, v: v['files'][0]['file'], {
                'id': ('files', 0, 'file_info_key', {str}),
                'url': ('files', 0, 'file', {url_or_none}),
                'duration': ('files', 0, 'duration', {functools.partial(int_or_none, scale=1000)}),
                'title': ('title', {str}),
                'uploader': ('writer_nick', {str}),
                'uploader_id': ('writer_id', {str}),
                'thumbnail': ('thumb', {url_or_none}),
                'timestamp': ('write_timestamp', {int_or_none}),
            }))


class AfreecaTVLiveIE(AfreecaTVBaseIE):
    IE_NAME = 'afreecatv:live'
    IE_DESC = 'afreecatv.com livestreams'
    _VALID_URL = r'https?://play\.afreeca(?:tv)?\.com/(?P<id>[^/]+)(?:/(?P<bno>\d+))?'
    _TESTS = [{
        'url': 'https://play.afreecatv.com/pyh3646/237852185',
        'info_dict': {
            'id': '237852185',
            'ext': 'mp4',
            'title': '【 우루과이 오늘은 무슨일이? 】',
            'uploader': '박진우[JINU]',
            'uploader_id': 'pyh3646',
            'timestamp': 1640661495,
            'is_live': True,
        },
        'skip': 'Livestream has ended',
    }, {
        'url': 'https://play.afreecatv.com/pyh3646/237852185',
        'only_matching': True,
    }, {
        'url': 'https://play.afreecatv.com/pyh3646',
        'only_matching': True,
    }]

    _LIVE_API_URL = 'https://live.afreecatv.com/afreeca/player_live_api.php'
    _WORKING_CDNS = [
        'gcp_cdn',  # live-global-cdn-v02.afreecatv.com
        'gs_cdn_pc_app',  # pc-app.stream.afreecatv.com
        'gs_cdn_mobile_web',  # mobile-web.stream.afreecatv.com
        'gs_cdn_pc_web',  # pc-web.stream.afreecatv.com