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

Code before:
            if not entries:
                self.raise_login_required(
                    'Only users older than 19 are able to watch this video', method='password')
            self.report_warning(
                'In accordance with local laws and regulations, underage users are '
                'restricted from watching adult content. Only content suitable for all '
                f'ages will be downloaded. {self._login_hint("password")}')

        if not entries and traverse_obj(data, ('sub_upload_type', {str})):
            self.raise_login_required('This VOD is for subscribers only', method='password')

        if len(entries) == 1:
            return {
                **entries[0],
                'title': common_info.get('title'),
            }

        common_info['timestamp'] = traverse_obj(entries, (..., 'timestamp'), get_all=False)

        return self.playlist_result(entries, video_id, multi_video=True, **common_info)


class AfreecaTVCatchStoryIE(AfreecaTVBaseIE):
    IE_NAME = 'soop:catchstory'
    IE_DESC = 'sooplive.co.kr catch story'
    _VALID_URL = r'https?://vod\.sooplive\.co\.kr/player/(?P<id>\d+)/catchstory'
    _TESTS = [{
        'url': 'https://vod.sooplive.co.kr/player/103247/catchstory',
        'info_dict': {
            'id': '103247',
        },
        'playlist_count': 2,
    }]

    def _real_extract(self, url):
        video_id = self._match_id(url)
        data = self._call_api(
            '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)}),

Review comment (suggestion):
support both domains
```suggestion
    _VALID_URL = r'https?://vod\.(?:sooplive\.co\.kr|afreecatv\.com)/player/(?P<id>\d+)/catchstory'
```

Code after:
                '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|afreecatv\.com)/(?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
        'gs_cdn_pc_web',  # pc-web.stream.sooplive.co.kr
    ]
    _BAD_CDNS = [
        'gs_cdn',  # chromecast.afreeca.gscdn.com (cannot resolve)
        '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,
        ])