Repository: yt-dlp/yt-dlp

Issue:
[Site-request] Add support for iflix.com
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting a new site support request
- [X] I've verified that I'm running yt-dlp version **2022.09.01** ([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 none of provided URLs [violate any copyrights](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
- [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)
- [ ] 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

Southeast Asia

### Example URLs

- Single video: https://www.iflix.com/en/play/t9rdwvmst6slsq8-Dr.-STONE-S2/g0040xyxyzs-EP1-%EF%BC%A4%EF%BD%92%EF%BC%8E%EF%BC%B3%EF%BC%B4%EF%BC%AF%EF%BC%AE%EF%BC%A5-S2

### Provide a description that is worded well enough to be understood

Iflix is Malaysian streaming service. iflix seems have collaboration with tencent. More info of iflix can be see in https://en.wikipedia.org/wiki/Iflix. The url request very similar with what wetv does. We can get most of video information from `vplay.iflix.com/getvinfo`. 

cc @elyse0 

### 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
>yt-dlp -v "https://www.iflix.com/id/play/t9rdwvmst6slsq8-Dr.-STONE-S2"
[debug] Command-line config: ['-v', 'https://www.iflix.com/id/play/t9rdwvmst6slsq8-Dr.-STONE-S2']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.09.01 [5d7c7d6] (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 5.1-full_build-www.gyan.dev (setts), ffprobe 5.1-full_build-www.gyan.dev
[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] Loaded 1670 extractors
[debug] [generic] Extracting URL: https://www.iflix.com/id/play/t9rdwvmst6slsq8-Dr.-STONE-S2
[generic] t9rdwvmst6slsq8-Dr: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] t9rdwvmst6slsq8-Dr: Extracting information
[debug] Looking for Brightcove embeds
[debug] Looking for embeds
ERROR: Unsupported URL: https://www.iflix.com/id/play/t9rdwvmst6slsq8-Dr.-STONE-S2
Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 1459, in wrapper
  File "yt_dlp\YoutubeDL.py", line 1535, in __extract_info
  File "yt_dlp\extractor\common.py", line 670, in extract
  File "yt_dlp\extractor\generic.py", line 3078, in _real_extract
yt_dlp.utils.UnsupportedError: Unsupported URL: https://www.iflix.com/id/play/t9rdwvmst6slsq8-Dr.-STONE-S2
```



Discussion:

Resolving patch:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1759,6 +1759,8 @@
 from .telewebion import TelewebionIE
 from .tempo import TempoIE
 from .tencent import (
+    IflixEpisodeIE,
+    IflixSeriesIE,
     VQQSeriesIE,
     VQQVideoIE,
     WeTvEpisodeIE,
diff --git a/yt_dlp/extractor/tencent.py b/yt_dlp/extractor/tencent.py
--- a/yt_dlp/extractor/tencent.py
+++ b/yt_dlp/extractor/tencent.py
@@ -262,6 +262,41 @@ def _get_webpage_metadata(self, webpage, video_id):
             traverse_obj(self._search_nextjs_data(webpage, video_id), ('props', 'pageProps', 'data')),
             video_id, fatal=False)
 
+    def _extract_episode(self, url):
+        video_id, series_id = self._match_valid_url(url).group('id', 'series_id')
+        webpage = self._download_webpage(url, video_id)
+        webpage_metadata = self._get_webpage_metadata(webpage, video_id)
+
+        formats, subtitles = self._extract_all_video_formats_and_subtitles(url, video_id, series_id)
+        return {
+            'id': video_id,
+            'title': self._get_clean_title(self._og_search_title(webpage)
+                                           or traverse_obj(webpage_metadata, ('coverInfo', 'title'))),
+            'description': (traverse_obj(webpage_metadata, ('coverInfo', 'description'))
+                            or self._og_search_description(webpage)),
+            'formats': formats,
+            'subtitles': subtitles,
+            'thumbnail': self._og_search_thumbnail(webpage),
+            'duration': int_or_none(traverse_obj(webpage_metadata, ('videoInfo', 'duration'))),
+            'series': traverse_obj(webpage_metadata, ('coverInfo', 'title')),
+            'episode_number': int_or_none(traverse_obj(webpage_metadata, ('videoInfo', 'episode'))),
+        }
+
+    def _extract_series(self, url, ie):
+        series_id = self._match_id(url)
+        webpage = self._download_webpage(url, series_id)
+        webpage_metadata = self._get_webpage_metadata(webpage, series_id)
+
+        episode_paths = ([f'/play/{series_id}/{episode["vid"]}' for episode in webpage_metadata.get('videoList')]
+                         or re.findall(r'<a[^>]+class="play-video__link"[^>]+href="(?P<path>[^"]+)', webpage))
+
+        return self.playlist_from_matches(
+            episode_paths, series_id, ie=ie, getter=functools.partial(urljoin, url),
+            title=self._get_clean_title(traverse_obj(webpage_metadata, ('coverInfo', 'title'))
+                                        or self._og_search_title(webpage)),
+            description=(traverse_obj(webpage_metadata, ('coverInfo', 'description'))
+                         or self._og_search_description(webpage)))
+
 
 class WeTvEpisodeIE(WeTvBaseIE):
     IE_NAME = 'wetv:episode'
@@ -312,24 +347,7 @@ class WeTvEpisodeIE(WeTvBaseIE):
     }]
 
     def _real_extract(self, url):
-        video_id, series_id = self._match_valid_url(url).group('id', 'series_id')
-        webpage = self._download_webpage(url, video_id)
-        webpage_metadata = self._get_webpage_metadata(webpage, video_id)
-
-        formats, subtitles = self._extract_all_video_formats_and_subtitles(url, video_id, series_id)
-        return {
-            'id': video_id,
-            'title': self._get_clean_title(self._og_search_title(webpage)
-                                           or traverse_obj(webpage_metadata, ('coverInfo', 'title'))),
-            'description': (traverse_obj(webpage_metadata, ('coverInfo', 'description'))
-                            or self._og_search_description(webpage)),
-            'formats': formats,
-            'subtitles': subtitles,
-            'thumbnail': self._og_search_thumbnail(webpage),
-            'duration': int_or_none(traverse_obj(webpage_metadata, ('videoInfo', 'duration'))),
-            'series': traverse_obj(webpage_metadata, ('coverInfo', 'title')),
-            'episode_number': int_or_none(traverse_obj(webpage_metadata, ('videoInfo', 'episode'))),
-        }
+        return self._extract_episode(url)
 
 
 class WeTvSeriesIE(WeTvBaseIE):
@@ -354,16 +372,77 @@ class WeTvSeriesIE(WeTvBaseIE):
     }]
 
     def _real_extract(self, url):
-        series_id = self._match_id(url)
-        webpage = self._download_webpage(url, series_id)
-        webpage_metadata = self._get_webpage_metadata(webpage, series_id)
+        return self._extract_series(url, WeTvEpisodeIE)
 
-        episode_paths = ([f'/play/{series_id}/{episode["vid"]}' for episode in webpage_metadata.get('videoList')]
-                         or re.findall(r'<a[^>]+class="play-video__link"[^>]+href="(?P<path>[^"]+)', webpage))
 
-        return self.playlist_from_matches(
-            episode_paths, series_id, ie=WeTvEpisodeIE, getter=functools.partial(urljoin, url),
-            title=self._get_clean_title(traverse_obj(webpage_metadata, ('coverInfo', 'title'))
-                                        or self._og_search_title(webpage)),
-            description=(traverse_obj(webpage_metadata, ('coverInfo', 'description'))
-                         or self._og_search_description(webpage)))
+class IflixBaseIE(WeTvBaseIE):
+    _VALID_URL_BASE = r'https?://(?:www\.)?iflix\.com/(?:[^?#]+/)?play'
+
+    _API_URL = 'https://vplay.iflix.com/getvinfo'
+    _APP_VERSION = '3.5.57'
+    _PLATFORM = '330201'
+    _HOST = 'www.iflix.com'
+    _REFERER = 'www.iflix.com'
+
+
+class IflixEpisodeIE(IflixBaseIE):
+    IE_NAME = 'iflix:episode'
+    _VALID_URL = IflixBaseIE._VALID_URL_BASE + r'/(?P<series_id>\w+)(?:-[^?#]+)?/(?P<id>\w+)(?:-[^?#]+)?'
+
+    _TESTS = [{
+        'url': 'https://www.iflix.com/en/play/daijrxu03yypu0s/a0040kvgaza',
+        'md5': '9740f9338c3a2105290d16b68fb3262f',
+        'info_dict': {
+            'id': 'a0040kvgaza',
+            'ext': 'mp4',
+            'title': 'EP1: Put Your Head On My Shoulder 2021',
+            'description': 'md5:c095a742d3b7da6dfedd0c8170727a42',
+            'thumbnail': r're:^https?://[^?#]+daijrxu03yypu0s',
+            'series': 'Put Your Head On My Shoulder 2021',
+            'episode': 'Episode 1',
+            'episode_number': 1,
+            'duration': 2639,
+        },
+    }, {
+        'url': 'https://www.iflix.com/en/play/fvvrcc3ra9lbtt1-Take-My-Brother-Away/i0029sd3gm1-EP1%EF%BC%9ATake-My-Brother-Away',
+        'md5': '375c9b8478fdedca062274b2c2f53681',
+        'info_dict': {
+            'id': 'i0029sd3gm1',
+            'ext': 'mp4',
+            'title': 'EP1：Take My Brother Away',
+            'description': 'md5:f0f7be1606af51cd94d5627de96b0c76',
+            'thumbnail': r're:^https?://[^?#]+fvvrcc3ra9lbtt1',
+            'series': 'Take My Brother Away',
+            'episode': 'Episode 1',
+            'episode_number': 1,
+            'duration': 228,
+        },
+    }]
+
+    def _real_extract(self, url):
+        return self._extract_episode(url)
+
+
+class IflixSeriesIE(IflixBaseIE):
+    _VALID_URL = IflixBaseIE._VALID_URL_BASE + r'/(?P<id>\w+)(?:-[^/?#]+)?/?(?:[?#]|$)'
+
+    _TESTS = [{
+        'url': 'https://www.iflix.com/en/play/g21a6qk4u1s9x22-You-Are-My-Hero',
+        'info_dict': {
+            'id': 'g21a6qk4u1s9x22',
+            'title': 'You Are My Hero',
+            'description': 'md5:9c4d844bc0799cd3d2b5aed758a2050a',
+        },
+        'playlist_count': 40,
+    }, {
+        'url': 'https://www.iflix.com/play/0s682hc45t0ohll',
+        'info_dict': {
+            'id': '0s682hc45t0ohll',
+            'title': 'Miss Gu Who Is Silent',
+            'description': 'md5:a9651d0236f25af06435e845fa2f8c78',
+        },
+        'playlist_count': 20,
+    }]
+
+    def _real_extract(self, url):
+        return self._extract_series(url, IflixEpisodeIE)


Test patch: