Repository: yt-dlp/yt-dlp (python)

Issue:
mir24.tv
### 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 the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
- [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 am willing to share it if required

### Region

Russia

### Example URLs

- Single video: https://mir24.tv/news/16635210/dni-kultury-rossii-otkrylis-v-uzbekistane.-na-prazdnichnom-koncerte-vystupili-zvezdy-rossijskoj-estrady
- Single video: https://mir24.tv/news/16635295/sadyr-zhaparov-i-dzhordzha-meloni-obsudili-perspektivnye-napravleniya-sotrudnichestva-kyrgyzstana-i-italii
- Playlist: https://mir24.tv/videos

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

M3U8 playlists from https://video.platformcraft.ru/vod/ are used to access videos.

### 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']
[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@2025.05.22 from yt-dlp/yt-dlp [7977b329e] (pip)
[debug] Python 3.13.3 (CPython arm64 64bit) - macOS-14.7.6-arm64-arm-64bit-Mach-O (OpenSSL 3.5.0 8 Apr 2025)
[debug] exe versions: ffmpeg 7.1.1 (setts), ffprobe 7.1.1
[debug] Optional libraries: Cryptodome-3.23.0, brotli-1.1.0, certifi-2025.04.26, mutagen-1.47.0, requests-2.32.3, sqlite3-3.50.0, urllib3-2.4.0, websockets-15.0.1
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Plugin directories: none
[debug] Loaded 1859 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2025.05.22 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2025.05.22 from yt-dlp/yt-dlp)
```

Pull request description:
[ie/mir24.tv] Add extractor
<!--
    **IMPORTANT**: PRs without the template will be CLOSED
    
    Due to the high volume of pull requests, it may be a while before your PR is reviewed.
    Please try to keep your pull request focused on a single bugfix or new feature.
    Pull requests with a vast scope and/or very large diff will take much longer to review.
    It is recommended for new contributors to stick to smaller pull requests, so you can receive much more immediate feedback as you familiarize yourself with the codebase.

    PLEASE AVOID FORCE-PUSHING after opening a PR, as it makes reviewing more difficult.
-->

### Description of your *pull request* and other information

ADD DETAILED DESCRIPTION HERE

Fixes #13365


<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 what 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

### 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 those that apply and remove the others:
- [x] I am the original author of the code in this PR, and I am willing to release it under [Unlicense](http://unlicense.org/)
- [ ] I am not the original author of the code in this PR, but it is in the public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)

### What is the purpose of your *pull request*? Check those that apply and remove the others:
- [ ] 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))

</details>


Patch:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index ada12b3a8..84da570b0 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1147,6 +1147,7 @@
     MindsIE,
 )
 from .minoto import MinotoIE
+from .mir24tv import Mir24TvIE
 from .mirrativ import (
     MirrativIE,
     MirrativUserIE,
diff --git a/yt_dlp/extractor/mir24tv.py b/yt_dlp/extractor/mir24tv.py
new file mode 100644
index 000000000..5832901bf
--- /dev/null
+++ b/yt_dlp/extractor/mir24tv.py
@@ -0,0 +1,37 @@
+from .common import InfoExtractor
+from ..utils import parse_qs, url_or_none
+from ..utils.traversal import require, traverse_obj
+
+
+class Mir24TvIE(InfoExtractor):
+    IE_NAME = 'mir24.tv'
+    _VALID_URL = r'https?://(?:www\.)?mir24\.tv/news/(?P<id>[0-9]+)/[^/?#]+'
+    _TESTS = [{
+        'url': 'https://mir24.tv/news/16635210/dni-kultury-rossii-otkrylis-v-uzbekistane.-na-prazdnichnom-koncerte-vystupili-zvezdy-rossijskoj-estrada',
+        'info_dict': {
+            'id': '16635210',
+            'title': 'Дни культуры России открылись в Узбекистане. На праздничном концерте выступили звезды российской эстрады',
+            'ext': 'mp4',
+            'thumbnail': r're:https://images\.mir24\.tv/.+\.jpg',
+        },
+    }]
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        webpage = self._download_webpage(url, video_id, impersonate=True)
+
+        iframe_url = self._search_regex(
+            r'<iframe\b[^>]+\bsrc=["\'](https?://mir24\.tv/players/[^"\']+)',
+            webpage, 'iframe URL')
+
+        m3u8_url = traverse_obj(iframe_url, (
+            {parse_qs}, 'source', -1, {self._proto_relative_url}, {url_or_none}, {require('m3u8 URL')}))
+        formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4', m3u8_id='hls')
+
+        return {
+            'id': video_id,
+            'title': self._og_search_title(webpage, default=None) or self._html_extract_title(webpage),
+            'thumbnail': self._og_search_thumbnail(webpage, default=None),
+            'formats': formats,
+            'subtitles': subtitles,
+        }
diff --git a/yt_dlp/extractor/ninegag.py b/yt_dlp/extractor/ninegag.py
index 2979f3a50..1b88e9c54 100644
--- a/yt_dlp/extractor/ninegag.py
+++ b/yt_dlp/extractor/ninegag.py
@@ -1,6 +1,5 @@
 from .common import InfoExtractor
 from ..utils import (
-    ExtractorError,
     determine_ext,
     int_or_none,
     traverse_obj,
@@ -61,10 +60,10 @@ def _real_extract(self, url):
         post = self._download_json(
             'https://9gag.com/v1/post', post_id, query={
                 'id': post_id,
-            })['data']['post']
+            }, impersonate=True)['data']['post']
 
         if post.get('type') != 'Animated':
-            raise ExtractorError(
+            self.raise_no_formats(
                 'The given url does not contain a video',
                 expected=True)
 
diff --git a/yt_dlp/extractor/youtube/_video.py b/yt_dlp/extractor/youtube/_video.py
index 8fa3b0a34..208abee93 100644
--- a/yt_dlp/extractor/youtube/_video.py
+++ b/yt_dlp/extractor/youtube/_video.py
@@ -3273,6 +3273,10 @@ def append_client(*client_names):
                 # web_creator may work around age-verification for all videos but requires PO token
                 append_client('tv_embedded', 'web_creator')
 
+            status = traverse_obj(pr, ('playabilityStatus', 'status', {str}))
+            if status not in ('OK', 'LIVE_STREAM_OFFLINE', 'AGE_CHECK_REQUIRED', 'AGE_VERIFICATION_REQUIRED'):
+                self.write_debug(f'{video_id}: {client} player response playability status: {status}')
+
         prs.extend(deprioritized_prs)
 
         if skipped_clients:
diff --git a/yt_dlp/jsinterp.py b/yt_dlp/jsinterp.py
index f06d96832..460bc2c03 100644
--- a/yt_dlp/jsinterp.py
+++ b/yt_dlp/jsinterp.py
@@ -677,8 +677,9 @@ def dict_item(key, val):
                 # Set value as JS_Undefined or its pre-existing value
                 local_vars.set_local(var, ret)
             else:
-                ret = local_vars.get(var, JS_Undefined)
-                if ret is JS_Undefined:
+                ret = local_vars.get(var, NO_DEFAULT)
+                if ret is NO_DEFAULT:
+                    ret = JS_Undefined
                     self._undefined_varnames.add(var)
             return ret, should_return
 


Test patch:
diff --git a/test/test_jsinterp.py b/test/test_jsinterp.py
index a1088cea4..43b1d0fde 100644
--- a/test/test_jsinterp.py
+++ b/test/test_jsinterp.py
@@ -536,6 +536,11 @@ def test_nested_function_scoping(self):
             }
         ''', 31)
 
+    def test_undefined_varnames(self):
+        jsi = JSInterpreter('function f(){ var a; return [a, b]; }')
+        self._test(jsi, [JS_Undefined, JS_Undefined])
+        self.assertEqual(jsi._undefined_varnames, {'b'})
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/test_youtube_signature.py b/test/test_youtube_signature.py
index 98607df55..456246753 100644
--- a/test/test_youtube_signature.py
+++ b/test/test_youtube_signature.py
@@ -373,6 +373,10 @@
         'https://www.youtube.com/s/player/e12fbea4/player_ias_tce.vflset/en_US/base.js',
         'kM5r52fugSZRAKHfo3', 'XkeRfXIPOkSwfg',
     ),
+    (
+        'https://www.youtube.com/s/player/ef259203/player_ias_tce.vflset/en_US/base.js',
+        'rPqBC01nJpqhhi2iA2U', 'hY7dbiKFT51UIA',
+    ),
 ]