Repository: yt-dlp/yt-dlp

Issue:
Afreecatv Failed to parse XML
### 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 that yt-dlp is broken on a **supported** site
- [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 all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [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)
- [X] 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 I'm willing to share it if required

### Region

United States

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

The afreecatv website has just stopped working since six hours ago. There is the prompt:

yt-dlp https://vod.afreecatv.com/player/117026071
[afreecatv] Extracting URL: https://vod.afreecatv.com/player/117026071
[afreecatv] 117026071: Downloading JSON metadata
Extracting cookies from firefox
Extracted 1509 cookies from firefox
[afreecatv] 117026071: Downloading video info XML
ERROR: [afreecatv] 117026071: 117026071: Failed to parse XML (caused by ParseError('no element found: line 1, column 0')); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] 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://vod.afreecatv.com/player/117026071']
[debug] Portable config "C:\Users\syazyz\scoop\apps\yt-dlp\current\yt-dlp.conf": []
[debug] Home config "yt-dlp.conf": ['--proxy', 'socks5://127.0.0.1:7888', '--merge-output-format', 'mkv', '--continue', '-o', '%(uploader)s\\%(upload_date)s - %(title)s.%(ext)s', '--no-check-certificate', '--prefer-insecure', '--user-agent', 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', '--concurrent-fragments', '3', '--cookies-from-browser', 'firefox:C:/GreenSoft/System/Profiles/FFP', '--downloader-args', 'ffmpeg_i:-hwaccel cuda -c:v vp9_cuvid']
[debug] Encodings: locale cp936, fs utf-8, pref cp936, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version master@2024.03.22.222728 from yt-dlp/yt-dlp-master-builds [e5d4f1110] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.22631-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg 2024-03-20-git-e04c638f5f-full_build-www.gyan.dev (setts), ffprobe 2024-03-20-git-e04c638f5f-full_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.20.0, brotli-1.1.0, certifi-2024.02.02, curl_cffi-0.5.10, mutagen-1.47.0, requests-2.31.0, sqlite3-3.35.5, urllib3-2.2.1, websockets-12.0
[debug] Proxy map: {'all': 'socks5://127.0.0.1:7888'}
Extracting cookies from firefox
[debug] Extracting cookies from: "C:\GreenSoft\System\Profiles\FFP\cookies.sqlite"
Extracted 1514 cookies from firefox
[debug] Request Handlers: urllib, requests, websockets, curl_cffi
[debug] Loaded 1807 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp-master-builds/releases/latest
Latest version: master@2024.03.22.222728 from yt-dlp/yt-dlp-master-builds
yt-dlp is up to date (master@2024.03.22.222728 from yt-dlp/yt-dlp-master-builds)
[afreecatv] Extracting URL: https://vod.afreecatv.com/player/117026071
[afreecatv] 117026071: Downloading JSON metadata
[afreecatv] 117026071: Downloading video info XML
ERROR: [afreecatv] 117026071: 117026071: Failed to parse XML (caused by ParseError('no element found: line 1, column 0')); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "yt_dlp\extractor\common.py", line 732, in extract
  File "yt_dlp\extractor\afreecatv.py", line 202, in _real_extract
  File "yt_dlp\extractor\common.py", line 1086, in download_content
  File "yt_dlp\extractor\common.py", line 1056, in download_handle
  File "yt_dlp\extractor\common.py", line 1046, in parse
  File "yt_dlp\extractor\common.py", line 1026, in _parse_xml
  File "yt_dlp\extractor\common.py", line 1016, in __print_error

  File "yt_dlp\extractor\common.py", line 1024, in _parse_xml
  File "yt_dlp\compat\__init__.py", line 24, in compat_etree_fromstring
  File "xml\etree\ElementTree.py", line 1321, in XML
xml.etree.ElementTree.ParseError: no element found: line 1, column 0
```

When downloading a VOD from afreecaTV, the upload_date would be changed to NA at the Concat
### Checklist

- [X] I'm reporting a bug unrelated to a specific site
- [X] I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [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 all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched 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)

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

The filename should start with the upload date, but ends up being NA. The upload date is correct right before the Concat.

URL used for testing: https://vod.afreecatv.com/player/90100147

As a side note, the folder I used is called youtube-dl, but the .exe inside is yt-dlp (just in case anyone had it confused)

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', '-N', '10', 'https://vod.afreecatv.com/player/90100147', '-o', '%(upload_date)s-%(uploader)s-%(title)s.%(ext)s']
[debug] Encodings: locale cp950, fs utf-8, pref cp950, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.07.18 [135f05e] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 4.2, ffprobe 4.2
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.07.18, Current version: 2022.07.18
yt-dlp is up to date (2022.07.18)
[debug] [afreecatv] Extracting URL: https://vod.afreecatv.com/player/90100147
[afreecatv] 90100147: Downloading webpage
[afreecatv] 90100147: Downloading video info XML
[afreecatv] 90100147: Downloading part 1 m3u8 information
[debug] Formats sorted by: hasvid, ie_pref, lang, quality, res, fps, hdr:12(7), vcodec:vp9.2(10), acodec, filesize, fs_approx, tbr, vbr, abr, asr, proto, vext, aext, hasaud, source, id
[download] Downloading multi_video: 다들 너무 감사합니다 ^_^* 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! 
[afreecatv] Playlist 다들 너무 감사합니다 ^_^* 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! : Downloading 1 videos of 1
[debug] The information of all playlist entries will be held in memory
[download] Downloading video 1 of 1
[debug] Default format spec: bestvideo*+bestaudio/best
[info] 20220723_4D451E93_241780835_33: Downloading 1 format(s): hls-original
[debug] Invoking hlsnative downloader on "http://vod-archive-global-cdn-z02.afreecatv.com/v101/hls/vod/20220723/835/241780835/REGL_4D451E93_241780835_33.smil/original/both/playlist.m3u8"
[hlsnative] Downloading m3u8 manifest
[hlsnative] Total fragments: 9
[download] Destination: 20220723-크레용팝엘린-다들 너무 감사합니다 ^_^_ 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! .mp4
[debug] File locking is not supported. Proceeding without locking
WARNING: The download speed shown is only of one thread. This is a known issue and patches are welcome
[download] 100% of 24.50MiB in 00:01
[debug] ffprobe command line: ffprobe -hide_banner -show_format -show_streams -print_format json "file:20220723-크레용팝엘린-다들 너무 감사합니다 ^_^_ 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! .mp4"
[FixupM3u8] Fixing MPEG-TS in MP4 container of "20220723-크레용팝엘린-다들 너무 감사합니다 ^_^_ 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! .mp4"
[debug] ffmpeg command line: ffmpeg -y -loglevel "repeat+info" -i "file:20220723-크레용팝엘린-다들 너무 감사합니다 ^_^_ 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! .mp4" -map 0 -dn -ignore_unknown -c copy -f mp4 "-bsf:a" aac_adtstoasc -movflags "+faststart" "file:20220723-크레용팝엘린-다들 너무 감사합니다 ^_^_ 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! .temp.mp4"
[Concat] Moving "K:\youtube-dl\20220723-크레용팝엘린-다들 너무 감사합니다 ^_^_ 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! .mp4" to "NA-크레용팝엘린-다들 너무 감사합니다 ^_^_ 고생하셨어요 ♥ 오늘 춤 영상 따지마세요! .mp4"
[download] Finished downloading playlist: 다들 너무 감사합니다 ^_^* 고생하셨어요 ♥ 오늘 춤 영상 따지마세요!
```



Discussion:
i'm having the same issue as well, region: europe

> (base) x@Xs-MacBook-Pro ~ % yt-dlp "https://vod.afreecatv.com/player/120332459" --verbose
[debug] Command-line config: ['https://vod.afreecatv.com/player/120332459', '--verbose']
[debug] User config "/Users/x/.config/yt-dlp/config": ['-P', '~/Downloads', '-o', '%(title)s.%(ext)s', '-f', 'bv*+ba/b', '--merge-output-format', 'mp4', '--alias', '--insta', '-o "Instagram %(title)s %(upload_date)s %(id)s.%(ext)s"', '--alias', '--ori', '-o "%(title)s %(upload_date)s.%(ext)s"', '--live-from-start', '--write-subs', '--sub-langs', 'en']
[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.03.10 from yt-dlp/yt-dlp [615a84447] (pip)
[debug] Python 3.8.12 (CPython x86_64 64bit) - macOS-10.16-x86_64-i386-64bit (OpenSSL 1.1.1w  11 Sep 2023)
[debug] exe versions: ffmpeg 5.1.2 (setts), ffprobe 5.1.2
[debug] Optional libraries: Cryptodome-3.14.1, brotli-1.0.9, certifi-2023.11.17, mutagen-1.45.1, requests-2.31.0, sqlite3-3.37.0, urllib3-2.1.0, websockets-12.0
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1803 extractors
[afreecatv] Extracting URL: https://vod.afreecatv.com/player/120332459
[afreecatv] 120332459: Downloading JSON metadata
[afreecatv] 120332459: Downloading video info XML
ERROR: [afreecatv] 120332459: 120332459: Failed to parse XML (caused by ParseError('no element found: line 1, column 0')); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 732, in extract
    ie_result = self._real_extract(url)
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/afreecatv.py", line 202, in _real_extract
    video_xml = self._download_xml(
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 1086, in download_content
    res = getattr(self, download_handle.__name__)(url_or_request, video_id, **kwargs)
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 1056, in download_handle
    return parse(self, content, video_id, transform_source=transform_source, fatal=fatal, errnote=errnote), urlh
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 1046, in parse
    return getattr(ie, parser)(content, *args, **kwargs)
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 1026, in _parse_xml
    self.__print_error('Failed to parse XML' if errnote is None else errnote, fatal, video_id, ve)
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 1016, in __print_error
    raise ExtractorError(f'{video_id}: {errnote}', cause=err)

> File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 1024, in _parse_xml
    return compat_etree_fromstring(xml_string.encode('utf-8'))
  File "/Users/x/opt/miniconda3/lib/python3.8/site-packages/yt_dlp/compat/__init__.py", line 24, in compat_etree_fromstring
    return etree.XML(text, parser=etree.XMLParser(target=_TreeBuilder()))
  File "/Users/x/opt/miniconda3/lib/python3.8/xml/etree/ElementTree.py", line 1321, in XML
    return parser.close()
xml.etree.ElementTree.ParseError: no element found: line 1, column 0
Looking at the page dumps, seems like all the information may be received in the initial JSON response now and the XML request is unnecessary


Resolving patch:
diff --git a/yt_dlp/extractor/afreecatv.py b/yt_dlp/extractor/afreecatv.py
--- a/yt_dlp/extractor/afreecatv.py
+++ b/yt_dlp/extractor/afreecatv.py
@@ -1,20 +1,16 @@
 import functools
-import re
 
 from .common import InfoExtractor
 from ..utils import (
     ExtractorError,
     OnDemandPagedList,
     UserNotLive,
-    date_from_str,
     determine_ext,
     filter_dict,
     int_or_none,
-    unified_strdate,
     unified_timestamp,
     url_or_none,
     urlencode_postdata,
-    xpath_text,
 )
 from ..utils.traversal import traverse_obj
 
@@ -76,7 +72,6 @@ class AfreecaTVIE(AfreecaTVBaseIE):
                         )
                         (?P<id>\d+)
                     '''
-    _NETRC_MACHINE = 'afreecatv'
     _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',
@@ -129,6 +124,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
             'uploader': '♥이슬이',
             'uploader_id': 'dasl8121',
             'upload_date': '20170411',
+            'timestamp': 1491929865,
             'duration': 213,
         },
         'params': {
@@ -162,176 +158,97 @@ class AfreecaTVIE(AfreecaTVBaseIE):
             'uploader_id': 'rlantnghks',
             'uploader': '페이즈으',
             'duration': 10840,
-            'thumbnail': 'http://videoimg.afreecatv.com/php/SnapshotLoad.php?rowKey=20230108_9FF5BEE1_244432674_1_r',
+            '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',
+        'only_matching': True,
+    }, {
+        # subscribers only
+        'url': 'https://vod.afreecatv.com/player/104647403',
+        'only_matching': True,
+    }, {
+        # private
+        'url': 'https://vod.afreecatv.com/player/81669846',
+        'only_matching': True,
     }]
 
-    @staticmethod
-    def parse_video_key(key):
-        video_key = {}
-        m = re.match(r'^(?P<upload_date>\d{8})_\w+_(?P<part>\d+)$', key)
-        if m:
-            video_key['upload_date'] = m.group('upload_date')
-            video_key['part'] = int(m.group('part'))
-        return video_key
-
     def _real_extract(self, url):
         video_id = self._match_id(url)
-
-        partial_view = False
-        adult_view = False
-        for _ in range(2):
-            data = self._download_json(
-                'https://api.m.afreecatv.com/station/video/a/view',
-                video_id, headers={'Referer': url}, data=urlencode_postdata({
-                    'nTitleNo': video_id,
-                    'nApiLevel': 10,
-                }))['data']
-            if traverse_obj(data, ('code', {int})) == -6221:
-                raise ExtractorError('The VOD does not exist', expected=True)
-            query = {
+        data = self._download_json(
+            'https://api.m.afreecatv.com/station/video/a/view', video_id,
+            headers={'Referer': url}, data=urlencode_postdata({
                 'nTitleNo': video_id,
-                'nStationNo': data['station_no'],
-                'nBbsNo': data['bbs_no'],
-            }
-            if partial_view:
-                query['partialView'] = 'SKIP_ADULT'
-            if adult_view:
-                query['adultView'] = 'ADULT_VIEW'
-            video_xml = self._download_xml(
-                'http://afbbs.afreecatv.com:8080/api/video/get_video_info.php',
-                video_id, 'Downloading video info XML%s'
-                % (' (skipping adult)' if partial_view else ''),
-                video_id, headers={
-                    'Referer': url,
-                }, query=query)
-
-            flag = xpath_text(video_xml, './track/flag', 'flag', default=None)
-            if flag and flag == 'SUCCEED':
-                break
-            if flag == 'PARTIAL_ADULT':
-                self.report_warning(
-                    'In accordance with local laws and regulations, underage users are restricted from watching adult content. '
-                    'Only content suitable for all ages will be downloaded. '
-                    'Provide account credentials if you wish to download restricted content.')
-                partial_view = True
-                continue
-            elif flag == 'ADULT':
-                if not adult_view:
-                    adult_view = True
-                    continue
-                error = 'Only users older than 19 are able to watch this video. Provide account credentials to download this content.'
-            else:
-                error = flag
-            raise ExtractorError(
-                '%s said: %s' % (self.IE_NAME, error), expected=True)
-        else:
-            raise ExtractorError('Unable to download video info')
-
-        video_element = video_xml.findall('./track/video')[-1]
-        if video_element is None or video_element.text is None:
-            raise ExtractorError(
-                'Video %s does not exist' % video_id, expected=True)
-
-        video_url = video_element.text.strip()
-
-        title = xpath_text(video_xml, './track/title', 'title', fatal=True)
-
-        uploader = xpath_text(video_xml, './track/nickname', 'uploader')
-        uploader_id = xpath_text(video_xml, './track/bj_id', 'uploader id')
-        duration = int_or_none(xpath_text(
-            video_xml, './track/duration', 'duration'))
-        thumbnail = xpath_text(video_xml, './track/titleImage', 'thumbnail')
-
-        common_entry = {
-            'uploader': uploader,
-            'uploader_id': uploader_id,
-            'thumbnail': thumbnail,
-        }
-
-        info = common_entry.copy()
-        info.update({
-            'id': video_id,
-            'title': title,
-            'duration': duration,
+                'nApiLevel': 10,
+            }))['data']
+
+        error_code = traverse_obj(data, ('code', {int}))
+        if error_code == -6221:
+            raise ExtractorError('The VOD does not exist', expected=True)
+        elif error_code == -6205:
+            raise ExtractorError('This VOD is private', expected=True)
+
+        common_info = traverse_obj(data, {
+            'title': ('title', {str}),
+            'uploader': ('writer_nick', {str}),
+            'uploader_id': ('bj_id', {str}),
+            'duration': ('total_file_duration', {functools.partial(int_or_none, scale=1000)}),
+            'thumbnail': ('thumb', {url_or_none}),
         })
 
-        if not video_url:
-            entries = []
-            file_elements = video_element.findall('./file')
-            one = len(file_elements) == 1
-            for file_num, file_element in enumerate(file_elements, start=1):
-                file_url = url_or_none(file_element.text)
-                if not file_url:
-                    continue
-                key = file_element.get('key', '')
-                upload_date = unified_strdate(self._search_regex(
-                    r'^(\d{8})_', key, 'upload date', default=None))
-                if upload_date is not None:
-                    # sometimes the upload date isn't included in the file name
-                    # instead, another random ID is, which may parse as a valid
-                    # date but be wildly out of a reasonable range
-                    parsed_date = date_from_str(upload_date)
-                    if parsed_date.year < 2000 or parsed_date.year >= 2100:
-                        upload_date = None
-                file_duration = int_or_none(file_element.get('duration'))
-                format_id = key if key else '%s_%s' % (video_id, file_num)
-                if determine_ext(file_url) == 'm3u8':
-                    formats = self._extract_m3u8_formats(
-                        file_url, video_id, 'mp4', entry_protocol='m3u8_native',
-                        m3u8_id='hls',
-                        note='Downloading part %d m3u8 information' % file_num)
-                else:
-                    formats = [{
-                        'url': file_url,
-                        'format_id': 'http',
-                    }]
-                if not formats and not self.get_param('ignore_no_formats'):
-                    continue
-                file_info = common_entry.copy()
-                file_info.update({
-                    'id': format_id,
-                    'title': title if one else '%s (part %d)' % (title, file_num),
-                    'upload_date': upload_date,
-                    'duration': file_duration,
-                    'formats': formats,
+        entries = []
+        for file_num, file_element in enumerate(
+                traverse_obj(data, ('files', lambda _, v: url_or_none(v['file']))), start=1):
+            file_url = file_element['file']
+            if determine_ext(file_url) == 'm3u8':
+                formats = self._extract_m3u8_formats(
+                    file_url, video_id, 'mp4', m3u8_id='hls',
+                    note=f'Downloading part {file_num} m3u8 information')
+            else:
+                formats = [{
+                    'url': file_url,
+                    'format_id': 'http',
+                }]
+
+            entries.append({
+                **common_info,
+                'id': file_element.get('file_info_key') or f'{video_id}_{file_num}',
+                'title': f'{common_info.get("title") or "Untitled"} (part {file_num})',
+                'formats': formats,
+                **traverse_obj(file_element, {
+                    'duration': ('duration', {functools.partial(int_or_none, scale=1000)}),
+                    'timestamp': ('file_start', {unified_timestamp}),
                 })
-                entries.append(file_info)
-            entries_info = info.copy()
-            entries_info.update({
-                '_type': 'multi_video',
-                'entries': entries,
             })
-            return entries_info
-
-        info = {
-            'id': video_id,
-            'title': title,
-            'uploader': uploader,
-            'uploader_id': uploader_id,
-            'duration': duration,
-            'thumbnail': thumbnail,
-        }
 
-        if determine_ext(video_url) == 'm3u8':
-            info['formats'] = self._extract_m3u8_formats(
-                video_url, video_id, 'mp4', entry_protocol='m3u8_native',
-                m3u8_id='hls')
-        else:
-            app, playpath = video_url.split('mp4:')
-            info.update({
-                'url': app,
-                'ext': 'flv',
-                'play_path': 'mp4:' + playpath,
-                'rtmp_live': True,  # downloading won't end without this
-            })
+        if traverse_obj(data, ('adult_status', {str})) == 'notLogin':
+            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 info
+        return self.playlist_result(entries, video_id, multi_video=True, **common_info)
 
 
 class AfreecaTVLiveIE(AfreecaTVBaseIE):


Test patch: