Repository: yt-dlp/yt-dlp
Issue category: Feature Request

Issue:
[afreecaTV]support for soop
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting a new site support request
- [X] I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
- [X] I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [ ] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required

### Region

South Korea

### Example URLs

https://www.sooplive.co.kr/
- Catch(Shorts like service): https://vod.sooplive.co.kr/player/137220647/catch
- Live: https://play.sooplive.co.kr/vf3366/277793184

### Provide a description that is worded well enough to be understood

Afreeca TV will change its name and domain to Soop starting 10/15/2024 12:00 (KST).
Now, we need to remove the existing Afreeca TV implementation and change to the new implementation for Soop.

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', 'https://play.sooplive.co.kr/vf3366/277793184']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2024.10.07 from yt-dlp/yt-dlp [1a176d874] (source)
[debug] Git HEAD: 1de886665
[debug] Python 3.12.4 (CPython x86_64 64bit) - Linux-6.11.0-8-generic-x86_64-with-glibc2.40 (OpenSSL 3.0.14 4 Jun 2024, glibc 2.40)
[debug] exe versions: ffmpeg 7.0.2 (setts), ffprobe 7.0.2
[debug] Optional libraries: Cryptodome-3.20.0, brotli-1.0.9, certifi-2024.06.02, mutagen-1.47.0, requests-2.32.2, secretstorage-3.3.1, sqlite3-3.45.3, urllib3-2.2.2, websockets-13.0
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1838 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2024.10.07 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2024.10.07 from yt-dlp/yt-dlp)
[generic] Extracting URL: https://play.sooplive.co.kr/vf3366/277793184
[generic] 277793184: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] 277793184: Extracting information
[debug] Looking for embeds
[debug] Identified a twitter:player iframe
[generic] Extracting URL: https://play.sooplive.co.kr/vf3366/embed
[generic] embed: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] embed: Extracting information
[debug] Looking for embeds
ERROR: Unsupported URL: https://play.sooplive.co.kr/vf3366/embed
Traceback (most recent call last):
  File "/data/projects/yt-dlp/yt_dlp/YoutubeDL.py", line 1626, in wrapper
    return func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/projects/yt-dlp/yt_dlp/YoutubeDL.py", line 1761, in __extract_info
    ie_result = ie.extract(url)
                ^^^^^^^^^^^^^^^
  File "/data/projects/yt-dlp/yt_dlp/extractor/common.py", line 741, in extract
    ie_result = self._real_extract(url)
                ^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/projects/yt-dlp/yt_dlp/extractor/generic.py", line 2526, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://play.sooplive.co.kr/vf3366/embed
```



Hints:

Patch:
diff --git a/yt_dlp/extractor/afreecatv.py b/yt_dlp/extractor/afreecatv.py
index 815d20537fa4..83e510d1a275 100644
--- a/yt_dlp/extractor/afreecatv.py
+++ b/yt_dlp/extractor/afreecatv.py
@@ -33,21 +33,21 @@ def _perform_login(self, username, password):
         }
 
         response = self._download_json(
-            'https://login.afreecatv.com/app/LoginAction.php', None,
+            'https://login.sooplive.co.kr/app/LoginAction.php', None,
             'Logging in', data=urlencode_postdata(login_form))
 
         _ERRORS = {
             -4: 'Your account has been suspended due to a violation of our terms and policies.',
-            -5: 'https://member.afreecatv.com/app/user_delete_progress.php',
-            -6: 'https://login.afreecatv.com/membership/changeMember.php',
-            -8: "Hello! AfreecaTV here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.",
-            -9: 'https://member.afreecatv.com/app/pop_login_block.php',
-            -11: 'https://login.afreecatv.com/afreeca/second_login.php',
-            -12: 'https://member.afreecatv.com/app/user_security.php',
+            -5: 'https://member.sooplive.co.kr/app/user_delete_progress.php',
+            -6: 'https://login.sooplive.co.kr/membership/changeMember.php',
+            -8: "Hello! Soop here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.",
+            -9: 'https://member.sooplive.co.kr/app/pop_login_block.php',
+            -11: 'https://login.sooplive.co.kr/afreeca/second_login.php',
+            -12: 'https://member.sooplive.co.kr/app/user_security.php',
             0: 'The username does not exist or you have entered the wrong password.',
             -1: 'The username does not exist or you have entered the wrong password.',
             -3: 'You have entered your username/password incorrectly.',
-            -7: 'You cannot use your Global AfreecaTV account to access Korean AfreecaTV.',
+            -7: 'You cannot use your Global Soop account to access Korean Soop.',
             -10: 'Sorry for the inconvenience. \nYour account has been blocked due to an unauthorized access. \nPlease contact our Help Center for assistance.',
             -32008: 'You have failed to log in. Please contact our Help Center.',
         }
@@ -61,76 +61,40 @@ def _perform_login(self, username, password):
 
     def _call_api(self, endpoint, display_id, data=None, headers=None, query=None):
         return self._download_json(Request(
-            f'https://api.m.afreecatv.com/{endpoint}',
+            f'https://api.m.sooplive.co.kr/{endpoint}',
             data=data, headers=headers, query=query,
             extensions={'legacy_ssl': True}), display_id,
             'Downloading API JSON', 'Unable to download API JSON')
 
 
 class AfreecaTVIE(AfreecaTVBaseIE):
-    IE_NAME = 'afreecatv'
-    IE_DESC = 'afreecatv.com'
-    _VALID_URL = r'''(?x)
-                    https?://
-                        (?:
-                            (?:(?:live|afbbs|www)\.)?afreeca(?:tv)?\.com(?::\d+)?
-                            (?:
-                                /app/(?:index|read_ucc_bbs)\.cgi|
-                                /player/[Pp]layer\.(?:swf|html)
-                            )\?.*?\bnTitleNo=|
-                            vod\.afreecatv\.com/(PLAYER/STATION|player)/
-                        )
-                        (?P<id>\d+)/?(?:$|[?#&])
-                    '''
+    IE_NAME = 'soop'
+    IE_DESC = 'sooplive.co.kr'
+    _VALID_URL = r'https?://vod\.(?:sooplive\.co\.kr|afreecatv\.com)/(?:PLAYER/STATION|player)/(?P<id>\d+)/?(?:$|[?#&])'
     _TESTS = [{
-        'url': 'http://live.afreecatv.com:8079/app/index.cgi?szType=read_ucc_bbs&szBjId=dailyapril&nStationNo=16711924&nBbsNo=18605867&nTitleNo=36164052&szSkin=',
-        'md5': 'f72c89fe7ecc14c1b5ce506c4996046e',
+        'url': 'https://vod.sooplive.co.kr/player/96753363',
         'info_dict': {
-            'id': '36164052',
+            'id': '20230108_9FF5BEE1_244432674_1',
             'ext': 'mp4',
-            'title': '데일리 에이프릴 요정들의 시상식!',
-            'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
-            'uploader': 'dailyapril',
-            'uploader_id': 'dailyapril',
-            'upload_date': '20160503',
+            'uploader_id': 'rlantnghks',
+            'uploader': '페이즈으',
+            'duration': 10840,
+            'thumbnail': r're:https?://videoimg\.sooplive\.co/.kr/.+',
+            'upload_date': '20230108',
+            'timestamp': 1673218805,
+            'title': '젠지 페이즈',
         },
-        'skip': 'Video is gone',
-    }, {
-        'url': 'http://afbbs.afreecatv.com:8080/app/read_ucc_bbs.cgi?nStationNo=16711924&nTitleNo=36153164&szBjId=dailyapril&nBbsNo=18605867',
-        'info_dict': {
-            'id': '36153164',
-            'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
-            'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
-            'uploader': 'dailyapril',
-            'uploader_id': 'dailyapril',
+        'params': {
+            'skip_download': True,
         },
-        'playlist_count': 2,
-        'playlist': [{
-            'md5': 'd8b7c174568da61d774ef0203159bf97',
-            'info_dict': {
-                'id': '36153164_1',
-                'ext': 'mp4',
-                'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
-                'upload_date': '20160502',
-            },
-        }, {
-            'md5': '58f2ce7f6044e34439ab2d50612ab02b',
-            'info_dict': {
-                'id': '36153164_2',
-                'ext': 'mp4',
-                'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
-                'upload_date': '20160502',
-            },
-        }],
-        'skip': 'Video is gone',
     }, {
         # non standard key
-        'url': 'http://vod.afreecatv.com/PLAYER/STATION/20515605',
+        'url': 'http://vod.sooplive.co.kr/PLAYER/STATION/20515605',
         'info_dict': {
             'id': '20170411_BE689A0E_190960999_1_2_h',
             'ext': 'mp4',
             'title': '혼자사는여자집',
-            'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
+            'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
             'uploader': '♥이슬이',
             'uploader_id': 'dasl8121',
             'upload_date': '20170411',
@@ -142,12 +106,12 @@ class AfreecaTVIE(AfreecaTVBaseIE):
         },
     }, {
         # adult content
-        'url': 'https://vod.afreecatv.com/player/97267690',
+        'url': 'https://vod.sooplive.co.kr/player/97267690',
         'info_dict': {
             'id': '20180327_27901457_202289533_1',
             'ext': 'mp4',
             'title': '[생]빨개요♥ (part 1)',
-            'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
+            'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
             'uploader': '[SA]서아',
             'uploader_id': 'bjdyrksu',
             'upload_date': '20180327',
@@ -157,36 +121,17 @@ class AfreecaTVIE(AfreecaTVBaseIE):
             'skip_download': True,
         },
         'skip': 'The VOD does not exist',
-    }, {
-        'url': 'http://www.afreecatv.com/player/Player.swf?szType=szBjId=djleegoon&nStationNo=11273158&nBbsNo=13161095&nTitleNo=36327652',
-        'only_matching': True,
-    }, {
-        'url': 'https://vod.afreecatv.com/player/96753363',
-        'info_dict': {
-            'id': '20230108_9FF5BEE1_244432674_1',
-            'ext': 'mp4',
-            'uploader_id': 'rlantnghks',
-            'uploader': '페이즈으',
-            'duration': 10840,
-            'thumbnail': r're:https?://videoimg\.afreecatv\.com/.+',
-            'upload_date': '20230108',
-            'timestamp': 1673218805,
-            'title': '젠지 페이즈',
-        },
-        'params': {
-            'skip_download': True,
-        },
     }, {
         # adult content
-        'url': 'https://vod.afreecatv.com/player/70395877',
+        'url': 'https://vod.sooplive.co.kr/player/70395877',
         'only_matching': True,
     }, {
         # subscribers only
-        'url': 'https://vod.afreecatv.com/player/104647403',
+        'url': 'https://vod.sooplive.co.kr/player/104647403',
         'only_matching': True,
     }, {
         # private
-        'url': 'https://vod.afreecatv.com/player/81669846',
+        'url': 'https://vod.sooplive.co.kr/player/81669846',
         'only_matching': True,
     }]
 
@@ -262,11 +207,11 @@ def _real_extract(self, url):
 
 
 class AfreecaTVCatchStoryIE(AfreecaTVBaseIE):
-    IE_NAME = 'afreecatv:catchstory'
-    IE_DESC = 'afreecatv.com catch story'
-    _VALID_URL = r'https?://vod\.afreecatv\.com/player/(?P<id>\d+)/catchstory'
+    IE_NAME = 'soop:catchstory'
+    IE_DESC = 'sooplive.co.kr catch story'
+    _VALID_URL = r'https?://vod\.(?:sooplive\.co\.kr|afreecatv\.com)/player/(?P<id>\d+)/catchstory'
     _TESTS = [{
-        'url': 'https://vod.afreecatv.com/player/103247/catchstory',
+        'url': 'https://vod.sooplive.co.kr/player/103247/catchstory',
         'info_dict': {
             'id': '103247',
         },
@@ -299,11 +244,11 @@ def _entries(data):
 
 
 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+))?'
+    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.afreecatv.com/pyh3646/237852185',
+        'url': 'https://play.sooplive.co.kr/pyh3646/237852185',
         'info_dict': {
             'id': '237852185',
             'ext': 'mp4',
@@ -315,30 +260,30 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
         },
         'skip': 'Livestream has ended',
     }, {
-        'url': 'https://play.afreecatv.com/pyh3646/237852185',
+        'url': 'https://play.sooplive.co.kr/pyh3646/237852185',
         'only_matching': True,
     }, {
-        'url': 'https://play.afreecatv.com/pyh3646',
+        'url': 'https://play.sooplive.co.kr/pyh3646',
         'only_matching': True,
     }]
 
-    _LIVE_API_URL = 'https://live.afreecatv.com/afreeca/player_live_api.php'
+    _LIVE_API_URL = 'https://live.sooplive.co.kr/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
+        '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.afreecatv.com (HTTP Error 400)
-        'azure_cdn',  # live-global-cdn-v01.afreecatv.com (cannot resolve)
-        'aws_cf',  # live-global-cdn-v03.afreecatv.com (cannot resolve)
-        'kt_cdn',  # kt.stream.afreecatv.com (HTTP Error 400)
+        '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.afreecatv.com'
+        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([
@@ -358,7 +303,7 @@ def _extract_formats(self, channel_info, broadcast_no, aid):
             try:
                 return self._extract_m3u8_formats(
                     m3u8_url, broadcast_no, 'mp4', m3u8_id='hls', query={'aid': aid},
-                    headers={'Referer': 'https://play.afreecatv.com/'})
+                    headers={'Referer': 'https://play.sooplive.co.kr/'})
             except ExtractorError as e:
                 if attempt == len(cdn_ids):
                     raise
@@ -374,7 +319,13 @@ def _real_extract(self, url):
         broadcaster_id = channel_info.get('BJID') or broadcaster_id
         broadcast_no = channel_info.get('BNO') or broadcast_no
         if not broadcast_no:
-            raise UserNotLive(video_id=broadcaster_id)
+            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')
 
         password = self.get_param('videopassword')
         if channel_info.get('BPWD') == 'Y' and password is None:
@@ -403,7 +354,7 @@ def _real_extract(self, url):
         formats = self._extract_formats(channel_info, broadcast_no, aid)
 
         station_info = traverse_obj(self._download_json(
-            'https://st.afreecatv.com/api/get_station_status.php', broadcast_no,
+            'https://st.sooplive.co.kr/api/get_station_status.php', broadcast_no,
             'Downloading channel metadata', 'Unable to download channel metadata',
             query={'szBjId': broadcaster_id}, fatal=False), {dict}) or {}
 
@@ -419,11 +370,11 @@ def _real_extract(self, url):
         }
 
 
-class AfreecaTVUserIE(InfoExtractor):
-    IE_NAME = 'afreecatv:user'
-    _VALID_URL = r'https?://bj\.afreeca(?:tv)?\.com/(?P<id>[^/]+)/vods/?(?P<slug_type>[^/]+)?'
+class AfreecaTVUserIE(AfreecaTVBaseIE):
+    IE_NAME = 'soop:user'
+    _VALID_URL = r'https?://ch\.(?:sooplive\.co\.kr|afreecatv\.com)/(?P<id>[^/?#]+)/vods/?(?P<slug_type>[^/?#]+)?'
     _TESTS = [{
-        'url': 'https://bj.afreecatv.com/ryuryu24/vods/review',
+        'url': 'https://ch.sooplive.co.kr/ryuryu24/vods/review',
         'info_dict': {
             '_type': 'playlist',
             'id': 'ryuryu24',
@@ -431,7 +382,7 @@ class AfreecaTVUserIE(InfoExtractor):
         },
         'playlist_count': 218,
     }, {
-        'url': 'https://bj.afreecatv.com/parang1995/vods/highlight',
+        'url': 'https://ch.sooplive.co.kr/parang1995/vods/highlight',
         'info_dict': {
             '_type': 'playlist',
             'id': 'parang1995',
@@ -439,7 +390,7 @@ class AfreecaTVUserIE(InfoExtractor):
         },
         'playlist_count': 997,
     }, {
-        'url': 'https://bj.afreecatv.com/ryuryu24/vods',
+        'url': 'https://ch.sooplive.co.kr/ryuryu24/vods',
         'info_dict': {
             '_type': 'playlist',
             'id': 'ryuryu24',
@@ -447,7 +398,7 @@ class AfreecaTVUserIE(InfoExtractor):
         },
         'playlist_count': 221,
     }, {
-        'url': 'https://bj.afreecatv.com/ryuryu24/vods/balloonclip',
+        'url': 'https://ch.sooplive.co.kr/ryuryu24/vods/balloonclip',
         'info_dict': {
             '_type': 'playlist',
             'id': 'ryuryu24',
@@ -459,12 +410,12 @@ class AfreecaTVUserIE(InfoExtractor):
 
     def _fetch_page(self, user_id, user_type, page):
         page += 1
-        info = self._download_json(f'https://bjapi.afreecatv.com/api/{user_id}/vods/{user_type}', user_id,
+        info = self._download_json(f'https://chapi.sooplive.co.kr/api/{user_id}/vods/{user_type}', user_id,
                                    query={'page': page, 'per_page': self._PER_PAGE, 'orderby': 'reg_date'},
                                    note=f'Downloading {user_type} video page {page}')
         for item in info['data']:
             yield self.url_result(
-                f'https://vod.afreecatv.com/player/{item["title_no"]}/', AfreecaTVIE, item['title_no'])
+                f'https://vod.sooplive.co.kr/player/{item["title_no"]}/', AfreecaTVIE, item['title_no'])
 
     def _real_extract(self, url):
         user_id, user_type = self._match_valid_url(url).group('id', 'slug_type')


Test patch: