[extractor/tempo] Add IVXPlayer extractor
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 2fe15f6d286..a0534d2b9be 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1817,7 +1817,7 @@
 )
 from .teletask import TeleTaskIE
 from .telewebion import TelewebionIE
-from .tempo import TempoIE
+from .tempo import TempoIE, IVXPlayerIE
 from .tencent import (
     IflixEpisodeIE,
     IflixSeriesIE,
diff --git a/yt_dlp/extractor/tempo.py b/yt_dlp/extractor/tempo.py
index 1cfb956e50b..9318d6f9ad8 100644
--- a/yt_dlp/extractor/tempo.py
+++ b/yt_dlp/extractor/tempo.py
@@ -1,5 +1,81 @@
+import re
+
 from .common import InfoExtractor
-from ..utils import int_or_none, parse_iso8601, str_or_none, traverse_obj
+from ..utils import (
+    int_or_none,
+    parse_iso8601,
+    traverse_obj,
+    try_call
+)
+
+
+class IVXPlayerIE(InfoExtractor):
+    _VALID_URL = r'ivxplayer:(?P<video_id>\d+):(?P<player_key>\w+)'
+    _TESTS = [{
+        'url': 'ivxplayer:2366065:4a89dfe6bc8f002596b1dfbd600730b1',
+        'info_dict': {
+            'id': '2366065',
+            'ext': 'mp4',
+            'duration': 112,
+            'upload_date': '20221204',
+            'title': 'Film Indonesia di Disney Content Showcase Asia Pacific 2022',
+            'timestamp': 1670151746,
+            'thumbnail': 'https://ivx-image.ivideosmart.com/serve/image/video/2366065?width=300'
+        }
+    }]
+    _WEBPAGE_TESTS = [{
+        'url': 'https://www.cantika.com/video/31737/film-indonesia-di-disney-content-showcase-asia-pacific-2022',
+        'info_dict': {
+            'id': '2374200',
+            'ext': 'mp4',
+            'duration': 110,
+            'title': 'Serial Indonesia di Disney Content Showcase Asia Pacific 2022',
+            'timestamp': 1670639416,
+            'upload_date': '20221210',
+            'thumbnail': 'https://ivx-image.ivideosmart.com/serve/image/video/2374200?width=300'
+        }
+    }, {
+        'url': 'https://www.gooto.com/video/11437/wuling-suv-ramai-dikunjungi-di-giias-2018',
+        'info_dict': {
+            'id': '892109',
+            'ext': 'mp4',
+            'title': 'Wuling SUV Ramai Dikunjungi di GIIAS 2018',
+            'upload_date': '20180811',
+            'description': 'md5:6d901483d0aacc664aecb4489719aafa',
+            'duration': 75,
+            'timestamp': 1534011263,
+            'thumbnail': 'https://ivx-image.ivideosmart.com/serve/image/video/892109?width=300'
+        }
+    }]
+
+    @classmethod
+    def _extract_embed_urls(cls, url, webpage):
+        # more info at https://player.ivideosmart.com/ivsplayer/v4/dist/js/loader.js
+        mobj = re.search(
+            r'<ivs-player\s*[^>]+data-ivs-key\s*=\s*"(?P<player_key>[\w]+)\s*[^>]+\bdata-ivs-vid="(?P<video_id>[\w-]+)',
+            webpage)
+        if mobj:
+            yield f'ivxplayer:{mobj.group("video_id")}:{mobj.group("player_key")}'
+            raise cls.StopExtraction()
+
+    def _real_extract(self, url):
+        video_id, player_key = self._match_valid_url(url).group('video_id', 'player_key')
+        json_data = self._download_json(
+            f'https://ivxplayer.ivideosmart.com/prod/video/{video_id}?key={player_key}', video_id)
+
+        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
+            json_data['player']['video_url'], video_id)
+
+        return {
+            'id': str(json_data['ivx']['id']),
+            'title': traverse_obj(json_data, ('ivx', 'name')),
+            'description': traverse_obj(json_data, ('ivx', 'description')),
+            'duration': int_or_none(traverse_obj(json_data, ('ivx', 'duration'))),
+            'timestamp': parse_iso8601(traverse_obj(json_data, ('ivx', 'published_at'))),
+            'formats': formats,
+            'subtitles': subtitles,
+            'thumbnail': traverse_obj(json_data, ('ivx', 'thumbnail_url'))
+        }
 
 
 class TempoIE(InfoExtractor):
@@ -7,14 +83,14 @@ class TempoIE(InfoExtractor):
     _TESTS = [{
         'url': 'https://video.tempo.co/read/30058/anies-baswedan-ajukan-banding-putusan-ptun-batalkan-ump-dki',
         'info_dict': {
-            'id': '2144438',
+            'id': '2144275',
+            'display_id': 'anies-baswedan-ajukan-banding-putusan-ptun-batalkan-ump-dki',
             'ext': 'mp4',
             'title': 'Anies Baswedan Ajukan Banding Putusan PTUN Batalkan UMP DKI',
-            'display_id': 'anies-baswedan-ajukan-banding-putusan-ptun-batalkan-ump-dki',
-            'duration': 84,
+            'duration': 85,
             'description': 'md5:a6822b7c4c874fa7e5bd63e96a387b66',
             'thumbnail': 'https://statik.tempo.co/data/2022/07/27/id_1128287/1128287_720.jpg',
-            'timestamp': 1658911277,
+            'timestamp': 1658907970,
             'upload_date': '20220727',
             'tags': ['Anies Baswedan', ' PTUN', ' PTUN | Pengadilan Tata Usaha Negara', ' PTUN Batalkan UMP DKI', ' UMP DKI'],
         }
@@ -24,30 +100,15 @@ def _real_extract(self, url):
         display_id = self._match_id(url)
         webpage = self._download_webpage(url, display_id)
 
-        player_key, widget_id = self._search_regex(
-            r'<ivs-player\s*[^>]+data-ivs-key\s*=\s*"(?P<player_key>[\w]+)[^>]+\bdata-ivs-wid="(?P<widget_id>[\w-]+)',
-            webpage, 'player_key, widget_id', group=('player_key', 'widget_id'))
+        _, video_id, player_key = next(IVXPlayerIE._extract_embed_urls(url, webpage)).split(':')
 
         json_ld_data = self._search_json_ld(webpage, display_id)
 
-        json_data = self._download_json(
-            f'https://ivxplayer.ivideosmart.com/prod/widget/{widget_id}',
-            display_id, query={'key': player_key})
-        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
-            json_data['player']['video_url'], display_id, ext='mp4')
-
-        return {
-            'id': str(json_data['ivx']['id']),
-            'display_id': display_id,
-            'formats': formats,
-            'subtitles': subtitles,
-            'title': (self._html_search_meta('twitter:title', webpage) or self._og_search_title(webpage)
-                      or traverse_obj(json_data, ('ivx', 'name'))),
-            'duration': int_or_none(traverse_obj(json_data, ('ivx', 'duration'))),
-            'thumbnail': (self._html_search_meta('twitter:image:src', webpage) or self._og_search_thumbnail(webpage)
-                          or traverse_obj(json_data, ('ivx', 'thumbnail_url'))),
-            'description': (json_ld_data.get('description') or self._html_search_meta(['description', 'twitter:description'], webpage)
-                            or self._og_search_description(webpage)),
-            'timestamp': parse_iso8601(traverse_obj(json_data, ('ivx', 'created_at'))),
-            'tags': str_or_none(self._html_search_meta('keywords', webpage), '').split(','),
-        }
+        return self.url_result(
+            f'ivxplayer:{video_id}:{player_key}', display_id=display_id,
+            thumbnail=self._html_search_meta('twitter:image:src', webpage) or self._og_search_thumbnail(webpage),
+            tags=try_call(lambda: self._html_search_meta('keywords', webpage).split(',')),
+            description=(json_ld_data.get('description')
+                         or self._html_search_meta(('description', 'twitter:description'), webpage)
+                         or self._og_search_description(webpage)),
+            url_transparent=True)

**IMPORTANT**: PRs without the template will be CLOSED

### Description of your *pull request* and other information

</details>

<!--

Explanation of your *pull request* in arbitrary form goes here. Please **make sure the description explains the purpose and effect** of your *pull request* and is worded well enough to be understood. Provide as much **context and examples** as possible

-->

This PR change code in tempo.py to be more general, as tempo use ivxplayer as backend. Ivxplayer also used by other website like cantika.com and gotoo.com. This change may change current tempo id as the old id seems inconsistent

Fixes #


<details open><summary>Template</summary> <!-- OPEN is intentional -->

<!--

# PLEASE FOLLOW THE GUIDE BELOW

- You will be asked some questions, please read them **carefully** and answer honestly
- Put an `x` into all the boxes `[ ]` relevant to your *pull request* (like [x])
- Use *Preview* tab to see how your *pull request* will actually look like

-->

### Before submitting a *pull request* make sure you have:
- [x] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [x] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
- [x] Checked the code with [flake8](https://pypi.python.org/pypi/flake8) and [ran relevant tests](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions)

### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check one of the following options:
- [x] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/)
- [ ] I am not the original author of this code but it is in public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)

### What is the purpose of your *pull request*?
- [x] Fix or improvement to an extractor (Make sure to add/update tests)
- [x] New extractor ([Piracy websites will not be accepted](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy))
- [ ] Core bug fix/improvement
- [ ] New feature (It is strongly [recommended to open an issue first](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#adding-new-feature-or-making-overarching-changes))
