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

Code before:
            'catchstory/a/view', video_id, headers={'Referer': url},
            query={'aStoryListIdx': '', 'nStoryIdx': video_id})

        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 = 'soop:live'
    IE_DESC = 'sooplive.co.kr livestreams'
    _VALID_URL = r'https?://play\.sooplive\.co\.kr/(?P<id>[^/]+)(?:/(?P<bno>\d+))?'
    _TESTS = [{
        'url': 'https://play.sooplive.co.kr/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.sooplive.co.kr/pyh3646/237852185',
        'only_matching': True,
    }, {
        'url': 'https://play.sooplive.co.kr/pyh3646',
        'only_matching': True,
    }]

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

Review comment (suggestion):
support both domains
```suggestion
    _VALID_URL = r'https?://play\.(?:sooplive\.co\.kr|afreecatv\.com)/(?P<id>[^/?#]+)(?:/(?P<bno>\d+))?'
```

Code after:
        'gs_cdn_chromecast',  # chromecast.stream.sooplive.co.kr (HTTP Error 400)
        'azure_cdn',  # live-global-cdn-v01.sooplive.co.kr (cannot resolve)
        'aws_cf',  # live-global-cdn-v03.sooplive.co.kr (cannot resolve)
        'kt_cdn',  # kt.stream.sooplive.co.kr (HTTP Error 400)
    ]

    def _extract_formats(self, channel_info, broadcast_no, aid):
        stream_base_url = channel_info.get('RMD') or 'https://livestream-manager.sooplive.co.kr'

        # If user has not passed CDN IDs, try API-provided CDN ID followed by other working CDN IDs
        default_cdn_ids = orderedSet([
            *traverse_obj(channel_info, ('CDN', {str}, all, lambda _, v: v not in self._BAD_CDNS)),
            *self._WORKING_CDNS,
        ])
        cdn_ids = self._configuration_arg('cdn', default_cdn_ids)

        for attempt, cdn_id in enumerate(cdn_ids, start=1):
            m3u8_url = traverse_obj(self._download_json(
                urljoin(stream_base_url, 'broad_stream_assign.html'), broadcast_no,
                f'Downloading {cdn_id} stream info', f'Unable to download {cdn_id} stream info',
                fatal=False, query={
                    'return_type': cdn_id,
                    'broad_key': f'{broadcast_no}-common-master-hls',
                }), ('view_url', {url_or_none}))
            try:
                return self._extract_m3u8_formats(
                    m3u8_url, broadcast_no, 'mp4', m3u8_id='hls', query={'aid': aid},
                    headers={'Referer': 'https://play.sooplive.co.kr/'})
            except ExtractorError as e:
                if attempt == len(cdn_ids):
                    raise
                self.report_warning(
                    f'{e.cause or e.msg}. Retrying... (attempt {attempt} of {len(cdn_ids)})')

    def _real_extract(self, url):
        broadcaster_id, broadcast_no = self._match_valid_url(url).group('id', 'bno')
        channel_info = traverse_obj(self._download_json(
            self._LIVE_API_URL, broadcaster_id, data=urlencode_postdata({'bid': broadcaster_id})),
            ('CHANNEL', {dict})) or {}

        broadcaster_id = channel_info.get('BJID') or broadcaster_id
        broadcast_no = channel_info.get('BNO') or broadcast_no
        if not broadcast_no:
            result = channel_info.get('RESULT')
            if result == 0:
                raise UserNotLive(video_id=broadcaster_id)
            elif result == -6:
                self.raise_login_required(
                    'This channel is streaming for subscribers only', method='password')
            raise ExtractorError('Unable to extract broadcast number')