Repository: yt-dlp/yt-dlp
Pull request #9902 (2024-05-10 14:04:39)

[ie/Digiview] Add extractor
**IMPORTANT**: PRs without the template will be CLOSED

### Description of your *pull request* and other information

<!--

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

-->

Added extractor for Digiview URLs (https://ladigitale.dev/digiview).
The extractor forwards the video ID to the YouTube extractor.


<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 all of the following options that apply:
- [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*?
- [ ] 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/.github/ISSUE_TEMPLATE/1_broken_site.yml b/.github/ISSUE_TEMPLATE/1_broken_site.yml
index fd7591ba2..c8d3de06b 100644
--- a/.github/ISSUE_TEMPLATE/1_broken_site.yml
+++ b/.github/ISSUE_TEMPLATE/1_broken_site.yml
@@ -6,7 +6,7 @@ body:
     attributes:
       value: |
         > [!IMPORTANT]
-        > Not providing the required (*) information will result in your issue being closed and ignored.
+        > Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
   - type: checkboxes
     id: checklist
     attributes:
diff --git a/.github/ISSUE_TEMPLATE/2_site_support_request.yml b/.github/ISSUE_TEMPLATE/2_site_support_request.yml
index 111f423d0..a9564c0c2 100644
--- a/.github/ISSUE_TEMPLATE/2_site_support_request.yml
+++ b/.github/ISSUE_TEMPLATE/2_site_support_request.yml
@@ -6,7 +6,7 @@ body:
     attributes:
       value: |
         > [!IMPORTANT]
-        > Not providing the required (*) information will result in your issue being closed and ignored.
+        > Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
   - type: checkboxes
     id: checklist
     attributes:
diff --git a/.github/ISSUE_TEMPLATE/3_site_feature_request.yml b/.github/ISSUE_TEMPLATE/3_site_feature_request.yml
index fd74be331..6e2380fae 100644
--- a/.github/ISSUE_TEMPLATE/3_site_feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/3_site_feature_request.yml
@@ -6,7 +6,7 @@ body:
     attributes:
       value: |
         > [!IMPORTANT]
-        > Not providing the required (*) information will result in your issue being closed and ignored.
+        > Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
   - type: checkboxes
     id: checklist
     attributes:
diff --git a/.github/ISSUE_TEMPLATE/4_bug_report.yml b/.github/ISSUE_TEMPLATE/4_bug_report.yml
index e9ec45e14..6fc523be0 100644
--- a/.github/ISSUE_TEMPLATE/4_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/4_bug_report.yml
@@ -6,7 +6,7 @@ body:
     attributes:
       value: |
         > [!IMPORTANT]
-        > Not providing the required (*) information will result in your issue being closed and ignored.
+        > Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
   - type: checkboxes
     id: checklist
     attributes:
diff --git a/.github/ISSUE_TEMPLATE/5_feature_request.yml b/.github/ISSUE_TEMPLATE/5_feature_request.yml
index 9c7d66d81..57a33bb71 100644
--- a/.github/ISSUE_TEMPLATE/5_feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/5_feature_request.yml
@@ -6,7 +6,7 @@ body:
     attributes:
       value: |
         > [!IMPORTANT]
-        > Not providing the required (*) information will result in your issue being closed and ignored.
+        > Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
   - type: checkboxes
     id: checklist
     attributes:
diff --git a/.github/ISSUE_TEMPLATE/6_question.yml b/.github/ISSUE_TEMPLATE/6_question.yml
index 186f73a4a..28ec7cbe0 100644
--- a/.github/ISSUE_TEMPLATE/6_question.yml
+++ b/.github/ISSUE_TEMPLATE/6_question.yml
@@ -6,7 +6,7 @@ body:
     attributes:
       value: |
         > [!IMPORTANT]
-        > Not providing the required (*) information will result in your issue being closed and ignored.
+        > Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
   - type: markdown
     attributes:
       value: |
diff --git a/devscripts/make_issue_template.py b/devscripts/make_issue_template.py
index 681ebc51c..110fcc245 100644
--- a/devscripts/make_issue_template.py
+++ b/devscripts/make_issue_template.py
@@ -56,7 +56,7 @@
     attributes:
       value: |
         > [!IMPORTANT]
-        > Not providing the required (*) information will result in your issue being closed and ignored.
+        > Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
 '''.strip()
 
 
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index b7b19cf6e..3293a9076 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -598,7 +598,7 @@ class YoutubeDL:
         # NB: Keep in sync with the docstring of extractor/common.py
         'url', 'manifest_url', 'manifest_stream_number', 'ext', 'format', 'format_id', 'format_note',
         'width', 'height', 'aspect_ratio', 'resolution', 'dynamic_range', 'tbr', 'abr', 'acodec', 'asr', 'audio_channels',
-        'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns',
+        'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns', 'hls_media_playlist_data',
         'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'is_dash_periods', 'request_data',
         'preference', 'language', 'language_preference', 'quality', 'source_preference', 'cookies',
         'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'extra_param_to_segment_url', 'extra_param_to_key_url',
diff --git a/yt_dlp/downloader/hls.py b/yt_dlp/downloader/hls.py
index 7bd116d96..7a47f8f83 100644
--- a/yt_dlp/downloader/hls.py
+++ b/yt_dlp/downloader/hls.py
@@ -72,11 +72,15 @@ def check_results():
 
     def real_download(self, filename, info_dict):
         man_url = info_dict['url']
-        self.to_screen(f'[{self.FD_NAME}] Downloading m3u8 manifest')
 
-        urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url))
-        man_url = urlh.url
-        s = urlh.read().decode('utf-8', 'ignore')
+        s = info_dict.get('hls_media_playlist_data')
+        if s:
+            self.to_screen(f'[{self.FD_NAME}] Using m3u8 manifest from extracted info')
+        else:
+            self.to_screen(f'[{self.FD_NAME}] Downloading m3u8 manifest')
+            urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url))
+            man_url = urlh.url
+            s = urlh.read().decode('utf-8', 'ignore')
 
         can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None
         if can_download:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index c331bab78..c937dfe13 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -508,6 +508,7 @@
 from .dhm import DHMIE
 from .digitalconcerthall import DigitalConcertHallIE
 from .digiteka import DigitekaIE
+from .digiview import DigiviewIE
 from .discogs import DiscogsReleasePlaylistIE
 from .disney import DisneyIE
 from .dispeak import DigitallySpeakingIE
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
index 92ddad2b7..3e7734ce1 100644
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -201,6 +201,11 @@ class InfoExtractor:
                                             fragment_base_url
                                  * "duration" (optional, int or float)
                                  * "filesize" (optional, int)
+                    * hls_media_playlist_data
+                                 The M3U8 media playlist data as a string.
+                                 Only use if the data must be modified during extraction and
+                                 the native HLS downloader should bypass requesting the URL.
+                                 Does not apply if ffmpeg is used as external downloader
                     * is_from_start  Is a live format that can be downloaded
                                 from the start. Boolean
                     * preference Order number of this format. If this field is
diff --git a/yt_dlp/extractor/digiview.py b/yt_dlp/extractor/digiview.py
new file mode 100644
index 000000000..f7f23864d
--- /dev/null
+++ b/yt_dlp/extractor/digiview.py
@@ -0,0 +1,130 @@
+from .common import InfoExtractor
+from .youtube import YoutubeIE
+from ..utils import clean_html, int_or_none, traverse_obj, url_or_none, urlencode_postdata
+
+
+class DigiviewIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?ladigitale\.dev/digiview/#/v/(?P<id>[0-9a-f]+)'
+    _TESTS = [{
+        # normal video
+        'url': 'https://ladigitale.dev/digiview/#/v/67a8e50aee2ec',
+        'info_dict': {
+            'id': '67a8e50aee2ec',
+            'ext': 'mp4',
+            'title': 'Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film',
+            'thumbnail': 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg',
+            'upload_date': '20141110',
+            'playable_in_embed': True,
+            'duration': 635,
+            'view_count': int,
+            'comment_count': int,
+            'channel': 'Blender',
+            'license': 'Creative Commons Attribution license (reuse allowed)',
+            'like_count': int,
+            'tags': 'count:8',
+            'live_status': 'not_live',
+            'channel_id': 'UCSMOQeBJ2RAnuFungnQOxLg',
+            'channel_follower_count': int,
+            'channel_url': 'https://www.youtube.com/channel/UCSMOQeBJ2RAnuFungnQOxLg',
+            'uploader_id': '@BlenderOfficial',
+            'description': 'md5:8f3ed18a53a1bb36cbb3b70a15782fd0',
+            'categories': ['Film & Animation'],
+            'channel_is_verified': True,
+            'heatmap': 'count:100',
+            'section_end': 635,
+            'uploader': 'Blender',
+            'timestamp': 1415628355,
+            'uploader_url': 'https://www.youtube.com/@BlenderOfficial',
+            'age_limit': 0,
+            'section_start': 0,
+            'availability': 'public',
+        },
+    }, {
+        # cut video
+        'url': 'https://ladigitale.dev/digiview/#/v/67a8e51d0dd58',
+        'info_dict': {
+            'id': '67a8e51d0dd58',
+            'ext': 'mp4',
+            'title': 'Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film',
+            'thumbnail': 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg',
+            'upload_date': '20141110',
+            'playable_in_embed': True,
+            'duration': 5,
+            'view_count': int,
+            'comment_count': int,
+            'channel': 'Blender',
+            'license': 'Creative Commons Attribution license (reuse allowed)',
+            'like_count': int,
+            'tags': 'count:8',
+            'live_status': 'not_live',
+            'channel_id': 'UCSMOQeBJ2RAnuFungnQOxLg',
+            'channel_follower_count': int,
+            'channel_url': 'https://www.youtube.com/channel/UCSMOQeBJ2RAnuFungnQOxLg',
+            'uploader_id': '@BlenderOfficial',
+            'description': 'md5:8f3ed18a53a1bb36cbb3b70a15782fd0',
+            'categories': ['Film & Animation'],
+            'channel_is_verified': True,
+            'heatmap': 'count:100',
+            'section_end': 10,
+            'uploader': 'Blender',
+            'timestamp': 1415628355,
+            'uploader_url': 'https://www.youtube.com/@BlenderOfficial',
+            'age_limit': 0,
+            'section_start': 5,
+            'availability': 'public',
+        },
+    }, {
+        # changed title
+        'url': 'https://ladigitale.dev/digiview/#/v/67a8ea5644d7a',
+        'info_dict': {
+            'id': '67a8ea5644d7a',
+            'ext': 'mp4',
+            'title': 'Big Buck Bunny (with title changed)',
+            'thumbnail': 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg',
+            'upload_date': '20141110',
+            'playable_in_embed': True,
+            'duration': 5,
+            'view_count': int,
+            'comment_count': int,
+            'channel': 'Blender',
+            'license': 'Creative Commons Attribution license (reuse allowed)',
+            'like_count': int,
+            'tags': 'count:8',
+            'live_status': 'not_live',
+            'channel_id': 'UCSMOQeBJ2RAnuFungnQOxLg',
+            'channel_follower_count': int,
+            'channel_url': 'https://www.youtube.com/channel/UCSMOQeBJ2RAnuFungnQOxLg',
+            'uploader_id': '@BlenderOfficial',
+            'description': 'md5:8f3ed18a53a1bb36cbb3b70a15782fd0',
+            'categories': ['Film & Animation'],
+            'channel_is_verified': True,
+            'heatmap': 'count:100',
+            'section_end': 15,
+            'uploader': 'Blender',
+            'timestamp': 1415628355,
+            'uploader_url': 'https://www.youtube.com/@BlenderOfficial',
+            'age_limit': 0,
+            'section_start': 10,
+            'availability': 'public',
+        },
+    }]
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        video_data = self._download_json(
+            'https://ladigitale.dev/digiview/inc/recuperer_video.php', video_id,
+            data=urlencode_postdata({'id': video_id}))
+
+        clip_id = video_data['videoId']
+        return self.url_result(
+            f'https://www.youtube.com/watch?v={clip_id}',
+            YoutubeIE, video_id, url_transparent=True,
+            **traverse_obj(video_data, {
+                'section_start': ('debut', {int_or_none}),
+                'section_end': ('fin', {int_or_none}),
+                'description': ('description', {clean_html}, filter),
+                'title': ('titre', {str}),
+                'thumbnail': ('vignette', {url_or_none}),
+                'view_count': ('vues', {int_or_none}),
+            }),
+        )
diff --git a/yt_dlp/extractor/twitter.py b/yt_dlp/extractor/twitter.py
index c05b5bf9c..d32ae3f18 100644
--- a/yt_dlp/extractor/twitter.py
+++ b/yt_dlp/extractor/twitter.py
@@ -1,11 +1,12 @@
 import functools
 import json
-import random
+import math
 import re
 import urllib.parse
 
 from .common import InfoExtractor
 from .periscope import PeriscopeBaseIE, PeriscopeIE
+from ..jsinterp import js_number_to_string
 from ..networking.exceptions import HTTPError
 from ..utils import (
     ExtractorError,
@@ -1330,6 +1331,11 @@ def _build_graphql_query(self, media_id):
             },
         }
 
+    def _generate_syndication_token(self, twid):
+        # ((Number(twid) / 1e15) * Math.PI).toString(36).replace(/(0+|\.)/g, '')
+        translation = str.maketrans(dict.fromkeys('0.'))
+        return js_number_to_string((int(twid) / 1e15) * math.PI, 36).translate(translation)
+
     def _call_syndication_api(self, twid):
         self.report_warning(
             'Not all metadata or media is available via syndication endpoint', twid, only_once=True)
@@ -1337,8 +1343,7 @@ def _call_syndication_api(self, twid):
             'https://cdn.syndication.twimg.com/tweet-result', twid, 'Downloading syndication JSON',
             headers={'User-Agent': 'Googlebot'}, query={
                 'id': twid,
-                # TODO: token = ((Number(twid) / 1e15) * Math.PI).toString(36).replace(/(0+|\.)/g, '')
-                'token': ''.join(random.choices('123456789abcdefghijklmnopqrstuvwxyz', k=10)),
+                'token': self._generate_syndication_token(twid),
             })
         if not status:
             raise ExtractorError('Syndication endpoint returned empty JSON response')
diff --git a/yt_dlp/extractor/zdf.py b/yt_dlp/extractor/zdf.py
index 703766cd7..b64a88f6c 100644
--- a/yt_dlp/extractor/zdf.py
+++ b/yt_dlp/extractor/zdf.py
@@ -187,12 +187,20 @@ class ZDFIE(ZDFBaseIE):
         'info_dict': {
             'id': '151025_magie_farben2_tex',
             'ext': 'mp4',
+            'duration': 2615.0,
             'title': 'Die Magie der Farben (2/2)',
             'description': 'md5:a89da10c928c6235401066b60a6d5c1a',
-            'duration': 2615,
             'timestamp': 1465021200,
-            'upload_date': '20160604',
             'thumbnail': 'https://www.zdf.de/assets/mauve-im-labor-100~768x432?cb=1464909117806',
+            'upload_date': '20160604',
+            'episode': 'Die Magie der Farben (2/2)',
+            'episode_id': 'POS_954f4170-36a5-4a41-a6cf-78f1f3b1f127',
+            'season': 'Staffel 1',
+            'series': 'Die Magie der Farben',
+            'season_number': 1,
+            'series_id': 'a39900dd-cdbd-4a6a-a413-44e8c6ae18bc',
+            'season_id': '5a92e619-8a0f-4410-a3d5-19c76fbebb37',
+            'episode_number': 2,
         },
     }, {
         'url': 'https://www.zdf.de/funk/druck-11790/funk-alles-ist-verzaubert-102.html',
@@ -200,12 +208,13 @@ class ZDFIE(ZDFBaseIE):
         'info_dict': {
             'ext': 'mp4',
             'id': 'video_funk_1770473',
-            'duration': 1278,
-            'description': 'Die Neue an der Schule verdreht Ismail den Kopf.',
+            'duration': 1278.0,
             'title': 'Alles ist verzaubert',
+            'description': 'Die Neue an der Schule verdreht Ismail den Kopf.',
             'timestamp': 1635520560,
-            'upload_date': '20211029',
             'thumbnail': 'https://www.zdf.de/assets/teaser-funk-alles-ist-verzaubert-102~1920x1080?cb=1663848412907',
+            'upload_date': '20211029',
+            'episode': 'Alles ist verzaubert',
         },
     }, {
         # Same as https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche
@@ -248,22 +257,52 @@ class ZDFIE(ZDFBaseIE):
             'title': 'Das Geld anderer Leute',
             'description': 'md5:cb6f660850dc5eb7d1ab776ea094959d',
             'duration': 2581.0,
-            'timestamp': 1675160100,
-            'upload_date': '20230131',
+            'timestamp': 1728983700,
+            'upload_date': '20241015',
             'thumbnail': 'https://epg-image.zdf.de/fotobase-webdelivery/images/e2d7e55a-09f0-424e-ac73-6cac4dd65f35?layout=2400x1350',
+            'series': 'SOKO Stuttgart',
+            'series_id': 'f862ce9a-6dd1-4388-a698-22b36ac4c9e9',
+            'season': 'Staffel 11',
+            'season_number': 11,
+            'season_id': 'ae1b4990-6d87-4970-a571-caccf1ba2879',
+            'episode': 'Das Geld anderer Leute',
+            'episode_number': 10,
+            'episode_id': 'POS_7f367934-f2f0-45cb-9081-736781ff2d23',
         },
     }, {
         'url': 'https://www.zdf.de/dokumentation/terra-x/unser-gruener-planet-wuesten-doku-100.html',
         'info_dict': {
-            'id': '220605_dk_gruener_planet_wuesten_tex',
+            'id': '220525_green_planet_makingof_1_tropen_tex',
+            'ext': 'mp4',
+            'title': 'Making-of Unser grüner Planet - Tropen',
+            'description': 'md5:d7c6949dc7c75c73c4ad51c785fb0b79',
+            'duration': 435.0,
+            'timestamp': 1653811200,
+            'upload_date': '20220529',
+            'format_note': 'hd, main',
+            'thumbnail': 'https://www.zdf.de/assets/unser-gruener-planet-making-of-1-tropen-100~3840x2160?cb=1653493335577',
+            'episode': 'Making-of Unser grüner Planet - Tropen',
+        },
+        'skip': 'No longer available: "Leider kein Video verfügbar"',
+    }, {
+        'url': 'https://www.zdf.de/serien/northern-lights/begegnung-auf-der-bruecke-100.html',
+        'info_dict': {
+            'id': '240319_2310_sendung_not',
             'ext': 'mp4',
-            'title': 'Unser grüner Planet - Wüsten',
-            'description': 'md5:4fc647b6f9c3796eea66f4a0baea2862',
-            'duration': 2613.0,
-            'timestamp': 1654450200,
-            'upload_date': '20220605',
-            'format_note': 'uhd, main',
-            'thumbnail': 'https://www.zdf.de/assets/saguaro-kakteen-102~3840x2160?cb=1655910690796',
+            'title': 'Begegnung auf der Brücke',
+            'description': 'md5:e53a555da87447f7f1207f10353f8e45',
+            'thumbnail': 'https://epg-image.zdf.de/fotobase-webdelivery/images/c5ff1d1f-f5c8-4468-86ac-1b2f1dbecc76?layout=2400x1350',
+            'upload_date': '20250203',
+            'duration': 3083.0,
+            'timestamp': 1738546500,
+            'series_id': '1d7a1879-01ee-4468-8237-c6b4ecd633c7',
+            'series': 'Northern Lights',
+            'season': 'Staffel 1',
+            'season_number': 1,
+            'season_id': '22ac26a2-4ea2-4055-ac0b-98b755cdf718',
+            'episode': 'Begegnung auf der Brücke',
+            'episode_number': 1,
+            'episode_id': 'POS_71049438-024b-471f-b472-4fe2e490d1fb',
         },
     }]
 
@@ -316,12 +355,31 @@ def _extract_entry(self, url, player, content, video_id):
             'timestamp': unified_timestamp(content.get('editorialDate')),
             'thumbnails': thumbnails,
             'chapters': chapters or None,
+            'episode': title,
+            **traverse_obj(content, ('programmeItem', 0, 'http://zdf.de/rels/target', {
+                'series_id': ('http://zdf.de/rels/cmdm/series', 'seriesUuid', {str}),
+                'series': ('http://zdf.de/rels/cmdm/series', 'seriesTitle', {str}),
+                'season': ('http://zdf.de/rels/cmdm/season', 'seasonTitle', {str}),
+                'season_number': ('http://zdf.de/rels/cmdm/season', 'seasonNumber', {int_or_none}),
+                'season_id': ('http://zdf.de/rels/cmdm/season', 'seasonUuid', {str}),
+                'episode_number': ('episodeNumber', {int_or_none}),
+                'episode_id': ('contentId', {str}),
+            })),
         })
 
     def _extract_regular(self, url, player, video_id):
-        content = self._call_api(
-            player['content'], video_id, 'content', player['apiToken'], url)
-        return self._extract_entry(player['content'], player, content, video_id)
+        player_url = player['content']
+
+        try:
+            content = self._call_api(
+                update_url_query(player_url, {'profile': 'player-3'}),
+                video_id, 'content', player['apiToken'], url)
+        except ExtractorError as e:
+            self.report_warning(f'{video_id}: {e.orig_msg}; retrying with v2 profile')
+            content = self._call_api(
+                player_url, video_id, 'content', player['apiToken'], url)
+
+        return self._extract_entry(player_url, player, content, video_id)
 
     def _extract_mobile(self, video_id):
         video = self._download_v2_doc(video_id)
diff --git a/yt_dlp/jsinterp.py b/yt_dlp/jsinterp.py
index bd6a47004..ac0629715 100644
--- a/yt_dlp/jsinterp.py
+++ b/yt_dlp/jsinterp.py
@@ -25,7 +25,7 @@ def zeroise(x):
         with contextlib.suppress(TypeError):
             if math.isnan(x):  # NB: NaN cannot be checked by membership
                 return 0
-        return x
+        return int(float(x))
 
     def wrapped(a, b):
         return op(zeroise(a), zeroise(b)) & 0xffffffff


Test patch:
diff --git a/test/helper.py b/test/helper.py
index c776e70b7..193019019 100644
--- a/test/helper.py
+++ b/test/helper.py
@@ -237,6 +237,20 @@ def sanitize(key, value):
 
 
 def expect_info_dict(self, got_dict, expected_dict):
+    ALLOWED_KEYS_SORT_ORDER = (
+        # NB: Keep in sync with the docstring of extractor/common.py
+        'id', 'ext', 'direct', 'display_id', 'title', 'alt_title', 'description', 'media_type',
+        'uploader', 'uploader_id', 'uploader_url', 'channel', 'channel_id', 'channel_url', 'channel_is_verified',
+        'channel_follower_count', 'comment_count', 'view_count', 'concurrent_view_count',
+        'like_count', 'dislike_count', 'repost_count', 'average_rating', 'age_limit', 'duration', 'thumbnail', 'heatmap',
+        'chapters', 'chapter', 'chapter_number', 'chapter_id', 'start_time', 'end_time', 'section_start', 'section_end',
+        'categories', 'tags', 'cast', 'composers', 'artists', 'album_artists', 'creators', 'genres',
+        'track', 'track_number', 'track_id', 'album', 'album_type', 'disc_number',
+        'series', 'series_id', 'season', 'season_number', 'season_id', 'episode', 'episode_number', 'episode_id',
+        'timestamp', 'upload_date', 'release_timestamp', 'release_date', 'release_year', 'modified_timestamp', 'modified_date',
+        'playable_in_embed', 'availability', 'live_status', 'location', 'license', '_old_archive_ids',
+    )
+
     expect_dict(self, got_dict, expected_dict)
     # Check for the presence of mandatory fields
     if got_dict.get('_type') not in ('playlist', 'multi_video'):
@@ -252,7 +266,13 @@ def expect_info_dict(self, got_dict, expected_dict):
 
     test_info_dict = sanitize_got_info_dict(got_dict)
 
-    missing_keys = set(test_info_dict.keys()) - set(expected_dict.keys())
+    # Check for invalid/misspelled field names being returned by the extractor
+    invalid_keys = sorted(test_info_dict.keys() - ALLOWED_KEYS_SORT_ORDER)
+    self.assertFalse(invalid_keys, f'Invalid fields returned by the extractor: {", ".join(invalid_keys)}')
+
+    missing_keys = sorted(
+        test_info_dict.keys() - expected_dict.keys(),
+        key=lambda x: ALLOWED_KEYS_SORT_ORDER.index(x))
     if missing_keys:
         def _repr(v):
             if isinstance(v, str):
diff --git a/test/test_jsinterp.py b/test/test_jsinterp.py
index c1464f2cd..ed3ca61c4 100644
--- a/test/test_jsinterp.py
+++ b/test/test_jsinterp.py
@@ -93,6 +93,16 @@ def test_operators(self):
         self._test('function f(){return 0 ?? 42;}', 0)
         self._test('function f(){return "life, the universe and everything" < 42;}', False)
         self._test('function f(){return 0  - 7 * - 6;}', 42)
+        self._test('function f(){return true << "5";}', 32)
+        self._test('function f(){return true << true;}', 2)
+        self._test('function f(){return "19" & "21.9";}', 17)
+        self._test('function f(){return "19" & false;}', 0)
+        self._test('function f(){return "11.0" >> "2.1";}', 2)
+        self._test('function f(){return 5 ^ 9;}', 12)
+        self._test('function f(){return 0.0 << NaN}', 0)
+        self._test('function f(){return null << undefined}', 0)
+        # TODO: Does not work due to number too large
+        # self._test('function f(){return 21 << 4294967297}', 42)
 
     def test_array_access(self):
         self._test('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}', [5, 2, 7])
diff --git a/test/test_youtube_signature.py b/test/test_youtube_signature.py
index 13436f088..7ae627f2c 100644
--- a/test/test_youtube_signature.py
+++ b/test/test_youtube_signature.py
@@ -201,6 +201,10 @@
         'https://www.youtube.com/s/player/2f1832d2/player_ias.vflset/en_US/base.js',
         'YWt1qdbe8SAfkoPHW5d', 'RrRjWQOJmBiP',
     ),
+    (
+        'https://www.youtube.com/s/player/9c6dfc4a/player_ias.vflset/en_US/base.js',
+        'jbu7ylIosQHyJyJV', 'uwI0ESiynAmhNg',
+    ),
 ]