yt-dlp 2025.11.1.73148.dev0__py3-none-any.whl → 2025.11.11.5312.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yt_dlp/cookies.py +1 -1
- yt_dlp/downloader/external.py +41 -45
- yt_dlp/extractor/_extractors.py +6 -1
- yt_dlp/extractor/bunnycdn.py +20 -3
- yt_dlp/extractor/dplay.py +86 -8
- yt_dlp/extractor/firsttv.py +34 -1
- yt_dlp/extractor/floatplane.py +26 -27
- yt_dlp/extractor/goplay.py +7 -5
- yt_dlp/extractor/kika.py +48 -42
- yt_dlp/extractor/lazy_extractors.py +30 -7
- yt_dlp/extractor/mux.py +92 -0
- yt_dlp/extractor/nascar.py +60 -0
- yt_dlp/extractor/ntvru.py +94 -57
- yt_dlp/extractor/tubetugraz.py +9 -2
- yt_dlp/extractor/twitch.py +5 -2
- yt_dlp/extractor/xhamster.py +31 -0
- yt_dlp/extractor/youtube/_base.py +11 -0
- yt_dlp/extractor/youtube/_tab.py +3 -2
- yt_dlp/extractor/youtube/_video.py +156 -32
- yt_dlp/extractor/youtube/jsc/_builtin/vendor/_info.py +1 -1
- yt_dlp/networking/_curlcffi.py +4 -1
- yt_dlp/networking/_requests.py +14 -9
- yt_dlp/networking/_urllib.py +19 -1
- yt_dlp/networking/common.py +6 -2
- yt_dlp/options.py +12 -5
- yt_dlp/postprocessor/sponsorblock.py +1 -0
- yt_dlp/utils/_jsruntime.py +13 -4
- yt_dlp/version.py +3 -3
- {yt_dlp-2025.11.1.73148.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/doc/yt_dlp/README.txt +42 -29
- {yt_dlp-2025.11.1.73148.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/fish/vendor_completions.d/yt-dlp.fish +3 -3
- {yt_dlp-2025.11.1.73148.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/man/man1/yt-dlp.1 +23 -14
- {yt_dlp-2025.11.1.73148.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/zsh/site-functions/_yt-dlp +2 -2
- {yt_dlp-2025.11.1.73148.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/METADATA +35 -23
- {yt_dlp-2025.11.1.73148.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/RECORD +38 -36
- {yt_dlp-2025.11.1.73148.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/bash-completion/completions/yt-dlp +0 -0
- {yt_dlp-2025.11.1.73148.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/WHEEL +0 -0
- {yt_dlp-2025.11.1.73148.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/entry_points.txt +0 -0
- {yt_dlp-2025.11.1.73148.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/licenses/LICENSE +0 -0
yt_dlp/cookies.py
CHANGED
|
@@ -557,7 +557,7 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
|
|
557
557
|
|
|
558
558
|
|
|
559
559
|
def _extract_safari_cookies(profile, logger):
|
|
560
|
-
if sys.platform
|
|
560
|
+
if sys.platform not in ('darwin', 'ios'):
|
|
561
561
|
raise ValueError(f'unsupported platform: {sys.platform}')
|
|
562
562
|
|
|
563
563
|
if profile:
|
yt_dlp/downloader/external.py
CHANGED
|
@@ -488,20 +488,6 @@ class FFmpegFD(ExternalFD):
|
|
|
488
488
|
if not self.params.get('verbose'):
|
|
489
489
|
args += ['-hide_banner']
|
|
490
490
|
|
|
491
|
-
args += traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args', ...))
|
|
492
|
-
|
|
493
|
-
# These exists only for compatibility. Extractors should use
|
|
494
|
-
# info_dict['downloader_options']['ffmpeg_args'] instead
|
|
495
|
-
args += info_dict.get('_ffmpeg_args') or []
|
|
496
|
-
seekable = info_dict.get('_seekable')
|
|
497
|
-
if seekable is not None:
|
|
498
|
-
# setting -seekable prevents ffmpeg from guessing if the server
|
|
499
|
-
# supports seeking(by adding the header `Range: bytes=0-`), which
|
|
500
|
-
# can cause problems in some cases
|
|
501
|
-
# https://github.com/ytdl-org/youtube-dl/issues/11800#issuecomment-275037127
|
|
502
|
-
# http://trac.ffmpeg.org/ticket/6125#comment:10
|
|
503
|
-
args += ['-seekable', '1' if seekable else '0']
|
|
504
|
-
|
|
505
491
|
env = None
|
|
506
492
|
proxy = self.params.get('proxy')
|
|
507
493
|
if proxy:
|
|
@@ -521,39 +507,10 @@ class FFmpegFD(ExternalFD):
|
|
|
521
507
|
env['HTTP_PROXY'] = proxy
|
|
522
508
|
env['http_proxy'] = proxy
|
|
523
509
|
|
|
524
|
-
protocol = info_dict.get('protocol')
|
|
525
|
-
|
|
526
|
-
if protocol == 'rtmp':
|
|
527
|
-
player_url = info_dict.get('player_url')
|
|
528
|
-
page_url = info_dict.get('page_url')
|
|
529
|
-
app = info_dict.get('app')
|
|
530
|
-
play_path = info_dict.get('play_path')
|
|
531
|
-
tc_url = info_dict.get('tc_url')
|
|
532
|
-
flash_version = info_dict.get('flash_version')
|
|
533
|
-
live = info_dict.get('rtmp_live', False)
|
|
534
|
-
conn = info_dict.get('rtmp_conn')
|
|
535
|
-
if player_url is not None:
|
|
536
|
-
args += ['-rtmp_swfverify', player_url]
|
|
537
|
-
if page_url is not None:
|
|
538
|
-
args += ['-rtmp_pageurl', page_url]
|
|
539
|
-
if app is not None:
|
|
540
|
-
args += ['-rtmp_app', app]
|
|
541
|
-
if play_path is not None:
|
|
542
|
-
args += ['-rtmp_playpath', play_path]
|
|
543
|
-
if tc_url is not None:
|
|
544
|
-
args += ['-rtmp_tcurl', tc_url]
|
|
545
|
-
if flash_version is not None:
|
|
546
|
-
args += ['-rtmp_flashver', flash_version]
|
|
547
|
-
if live:
|
|
548
|
-
args += ['-rtmp_live', 'live']
|
|
549
|
-
if isinstance(conn, list):
|
|
550
|
-
for entry in conn:
|
|
551
|
-
args += ['-rtmp_conn', entry]
|
|
552
|
-
elif isinstance(conn, str):
|
|
553
|
-
args += ['-rtmp_conn', conn]
|
|
554
|
-
|
|
555
510
|
start_time, end_time = info_dict.get('section_start') or 0, info_dict.get('section_end')
|
|
556
511
|
|
|
512
|
+
fallback_input_args = traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args', ...))
|
|
513
|
+
|
|
557
514
|
selected_formats = info_dict.get('requested_formats') or [info_dict]
|
|
558
515
|
for i, fmt in enumerate(selected_formats):
|
|
559
516
|
is_http = re.match(r'https?://', fmt['url'])
|
|
@@ -572,6 +529,44 @@ class FFmpegFD(ExternalFD):
|
|
|
572
529
|
if end_time:
|
|
573
530
|
args += ['-t', str(end_time - start_time)]
|
|
574
531
|
|
|
532
|
+
protocol = fmt.get('protocol')
|
|
533
|
+
|
|
534
|
+
if protocol == 'rtmp':
|
|
535
|
+
player_url = fmt.get('player_url')
|
|
536
|
+
page_url = fmt.get('page_url')
|
|
537
|
+
app = fmt.get('app')
|
|
538
|
+
play_path = fmt.get('play_path')
|
|
539
|
+
tc_url = fmt.get('tc_url')
|
|
540
|
+
flash_version = fmt.get('flash_version')
|
|
541
|
+
live = fmt.get('rtmp_live', False)
|
|
542
|
+
conn = fmt.get('rtmp_conn')
|
|
543
|
+
if player_url is not None:
|
|
544
|
+
args += ['-rtmp_swfverify', player_url]
|
|
545
|
+
if page_url is not None:
|
|
546
|
+
args += ['-rtmp_pageurl', page_url]
|
|
547
|
+
if app is not None:
|
|
548
|
+
args += ['-rtmp_app', app]
|
|
549
|
+
if play_path is not None:
|
|
550
|
+
args += ['-rtmp_playpath', play_path]
|
|
551
|
+
if tc_url is not None:
|
|
552
|
+
args += ['-rtmp_tcurl', tc_url]
|
|
553
|
+
if flash_version is not None:
|
|
554
|
+
args += ['-rtmp_flashver', flash_version]
|
|
555
|
+
if live:
|
|
556
|
+
args += ['-rtmp_live', 'live']
|
|
557
|
+
if isinstance(conn, list):
|
|
558
|
+
for entry in conn:
|
|
559
|
+
args += ['-rtmp_conn', entry]
|
|
560
|
+
elif isinstance(conn, str):
|
|
561
|
+
args += ['-rtmp_conn', conn]
|
|
562
|
+
|
|
563
|
+
elif protocol == 'http_dash_segments' and info_dict.get('is_live'):
|
|
564
|
+
# ffmpeg may try to read past the latest available segments for
|
|
565
|
+
# live DASH streams unless we pass `-re`. In modern ffmpeg, this
|
|
566
|
+
# is an alias of `-readrate 1`, but `-readrate` was not added
|
|
567
|
+
# until ffmpeg 5.0, so we must stick to using `-re`
|
|
568
|
+
args += ['-re']
|
|
569
|
+
|
|
575
570
|
url = fmt['url']
|
|
576
571
|
if self.params.get('enable_file_urls') and url.startswith('file:'):
|
|
577
572
|
# The default protocol_whitelist is 'file,crypto,data' when reading local m3u8 URLs,
|
|
@@ -586,6 +581,7 @@ class FFmpegFD(ExternalFD):
|
|
|
586
581
|
# https://trac.ffmpeg.org/ticket/2702
|
|
587
582
|
url = re.sub(r'^file://(?:localhost)?/', 'file:' if os.name == 'nt' else 'file:/', url)
|
|
588
583
|
|
|
584
|
+
args += traverse_obj(fmt, ('downloader_options', 'ffmpeg_args', ...)) or fallback_input_args
|
|
589
585
|
args += [*self._configuration_args((f'_i{i + 1}', '_i')), '-i', url]
|
|
590
586
|
|
|
591
587
|
if not (start_time or end_time) or not self.params.get('force_keyframes_at_cuts'):
|
yt_dlp/extractor/_extractors.py
CHANGED
|
@@ -640,7 +640,10 @@ from .filmon import (
|
|
|
640
640
|
FilmOnIE,
|
|
641
641
|
)
|
|
642
642
|
from .filmweb import FilmwebIE
|
|
643
|
-
from .firsttv import
|
|
643
|
+
from .firsttv import (
|
|
644
|
+
FirstTVIE,
|
|
645
|
+
FirstTVLiveIE,
|
|
646
|
+
)
|
|
644
647
|
from .fivetv import FiveTVIE
|
|
645
648
|
from .flextv import FlexTVIE
|
|
646
649
|
from .flickr import FlickrIE
|
|
@@ -1197,6 +1200,7 @@ from .musicdex import (
|
|
|
1197
1200
|
MusicdexPlaylistIE,
|
|
1198
1201
|
MusicdexSongIE,
|
|
1199
1202
|
)
|
|
1203
|
+
from .mux import MuxIE
|
|
1200
1204
|
from .mx3 import (
|
|
1201
1205
|
Mx3IE,
|
|
1202
1206
|
Mx3NeoIE,
|
|
@@ -1218,6 +1222,7 @@ from .n1 import (
|
|
|
1218
1222
|
N1InfoAssetIE,
|
|
1219
1223
|
N1InfoIIE,
|
|
1220
1224
|
)
|
|
1225
|
+
from .nascar import NascarClassicsIE
|
|
1221
1226
|
from .nate import (
|
|
1222
1227
|
NateIE,
|
|
1223
1228
|
NateProgramIE,
|
yt_dlp/extractor/bunnycdn.py
CHANGED
|
@@ -16,7 +16,7 @@ from ..utils.traversal import find_element, traverse_obj
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class BunnyCdnIE(InfoExtractor):
|
|
19
|
-
_VALID_URL = r'https?://(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)/(?:embed|play)/(?P<library_id>\d+)/(?P<id>[\da-f-]+)'
|
|
19
|
+
_VALID_URL = r'https?://(?:(?:iframe|player)\.mediadelivery\.net|video\.bunnycdn\.com)/(?:embed|play)/(?P<library_id>\d+)/(?P<id>[\da-f-]+)'
|
|
20
20
|
_EMBED_REGEX = [rf'<iframe[^>]+src=[\'"](?P<url>{_VALID_URL}[^\'"]*)[\'"]']
|
|
21
21
|
_TESTS = [{
|
|
22
22
|
'url': 'https://iframe.mediadelivery.net/embed/113933/e73edec1-e381-4c8b-ae73-717a140e0924',
|
|
@@ -39,7 +39,7 @@ class BunnyCdnIE(InfoExtractor):
|
|
|
39
39
|
'timestamp': 1691145748,
|
|
40
40
|
'thumbnail': r're:^https?://.*\.b-cdn\.net/32e34c4b-0d72-437c-9abb-05e67657da34/thumbnail_9172dc16\.jpg',
|
|
41
41
|
'duration': 106.0,
|
|
42
|
-
'description': 'md5:
|
|
42
|
+
'description': 'md5:11452bcb31f379ee3eaf1234d3264e44',
|
|
43
43
|
'upload_date': '20230804',
|
|
44
44
|
'title': 'Sanela ist Teil der #arbeitsmarktkraft',
|
|
45
45
|
},
|
|
@@ -58,6 +58,23 @@ class BunnyCdnIE(InfoExtractor):
|
|
|
58
58
|
'thumbnail': r're:^https?://.*\.b-cdn\.net/2e8545ec-509d-4571-b855-4cf0235ccd75/thumbnail\.jpg',
|
|
59
59
|
},
|
|
60
60
|
'params': {'skip_download': True},
|
|
61
|
+
}, {
|
|
62
|
+
# Requires any Referer
|
|
63
|
+
'url': 'https://iframe.mediadelivery.net/embed/289162/6372f5a3-68df-4ef7-a115-e1110186c477',
|
|
64
|
+
'info_dict': {
|
|
65
|
+
'id': '6372f5a3-68df-4ef7-a115-e1110186c477',
|
|
66
|
+
'ext': 'mp4',
|
|
67
|
+
'title': '12-Creating Small Asset Blockouts -Timelapse.mp4',
|
|
68
|
+
'description': '',
|
|
69
|
+
'duration': 263.0,
|
|
70
|
+
'timestamp': 1724485440,
|
|
71
|
+
'upload_date': '20240824',
|
|
72
|
+
'thumbnail': r're:^https?://.*\.b-cdn\.net/6372f5a3-68df-4ef7-a115-e1110186c477/thumbnail\.jpg',
|
|
73
|
+
},
|
|
74
|
+
'params': {'skip_download': True},
|
|
75
|
+
}, {
|
|
76
|
+
'url': 'https://player.mediadelivery.net/embed/519128/875880a9-bcc2-4038-9e05-e5024bba9b70',
|
|
77
|
+
'only_matching': True,
|
|
61
78
|
}]
|
|
62
79
|
_WEBPAGE_TESTS = [{
|
|
63
80
|
# Stream requires Referer
|
|
@@ -100,7 +117,7 @@ class BunnyCdnIE(InfoExtractor):
|
|
|
100
117
|
video_id, library_id = self._match_valid_url(url).group('id', 'library_id')
|
|
101
118
|
webpage = self._download_webpage(
|
|
102
119
|
f'https://iframe.mediadelivery.net/embed/{library_id}/{video_id}', video_id,
|
|
103
|
-
headers=
|
|
120
|
+
headers={'Referer': smuggled_data.get('Referer') or 'https://iframe.mediadelivery.net/'},
|
|
104
121
|
query=traverse_obj(parse_qs(url), {'token': 'token', 'expires': 'expires'}))
|
|
105
122
|
|
|
106
123
|
if html_title := self._html_extract_title(webpage, default=None) == '403':
|
yt_dlp/extractor/dplay.py
CHANGED
|
@@ -13,6 +13,7 @@ from ..utils import (
|
|
|
13
13
|
try_get,
|
|
14
14
|
unified_timestamp,
|
|
15
15
|
)
|
|
16
|
+
from ..utils.traversal import traverse_obj
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class DPlayBaseIE(InfoExtractor):
|
|
@@ -1053,7 +1054,7 @@ class DiscoveryPlusIndiaIE(DiscoveryPlusBaseIE):
|
|
|
1053
1054
|
|
|
1054
1055
|
|
|
1055
1056
|
class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
|
|
1056
|
-
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de
|
|
1057
|
+
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de)/(?:programme|show|sendungen)/(?P<programme>[^/?#]+)/(?:video/)?(?P<alternate_id>[^/?#]+)'
|
|
1057
1058
|
|
|
1058
1059
|
_TESTS = [{
|
|
1059
1060
|
'url': 'https://dmax.de/sendungen/goldrausch-in-australien/german-gold',
|
|
@@ -1074,6 +1075,7 @@ class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
|
|
|
1074
1075
|
'creators': ['DMAX'],
|
|
1075
1076
|
'thumbnail': 'https://eu1-prod-images.disco-api.com/2023/05/09/f72fb510-7992-3b12-af7f-f16a2c22d1e3.jpeg',
|
|
1076
1077
|
'tags': ['schatzsucher', 'schatz', 'nugget', 'bodenschätze', 'down under', 'australien', 'goldrausch'],
|
|
1078
|
+
'categories': ['Gold', 'Schatzsucher'],
|
|
1077
1079
|
},
|
|
1078
1080
|
'params': {'skip_download': 'm3u8'},
|
|
1079
1081
|
}, {
|
|
@@ -1100,20 +1102,96 @@ class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
|
|
|
1100
1102
|
}, {
|
|
1101
1103
|
'url': 'https://www.dmax.de/programme/dmax-highlights/video/tuning-star-sidney-hoffmann-exklusiv-bei-dmax/191023082312316',
|
|
1102
1104
|
'only_matching': True,
|
|
1103
|
-
}, {
|
|
1104
|
-
'url': 'https://www.dplay.co.uk/show/ghost-adventures/video/hotel-leger-103620/EHD_280313B',
|
|
1105
|
-
'only_matching': True,
|
|
1106
1105
|
}, {
|
|
1107
1106
|
'url': 'https://tlc.de/sendungen/breaking-amish/die-welt-da-drauen/',
|
|
1108
1107
|
'only_matching': True,
|
|
1108
|
+
}, {
|
|
1109
|
+
'url': 'https://dmax.de/sendungen/feuerwache-3-alarm-in-muenchen/24-stunden-auf-der-feuerwache-3',
|
|
1110
|
+
'info_dict': {
|
|
1111
|
+
'id': '8873549',
|
|
1112
|
+
'ext': 'mp4',
|
|
1113
|
+
'title': '24 Stunden auf der Feuerwache 3',
|
|
1114
|
+
'description': 'md5:f3084ef6170bfb79f9a6e0c030e09330',
|
|
1115
|
+
'display_id': 'feuerwache-3-alarm-in-muenchen/24-stunden-auf-der-feuerwache-3',
|
|
1116
|
+
'episode': 'Episode 1',
|
|
1117
|
+
'episode_number': 1,
|
|
1118
|
+
'season': 'Season 1',
|
|
1119
|
+
'season_number': 1,
|
|
1120
|
+
'series': 'Feuerwache 3 - Alarm in München',
|
|
1121
|
+
'duration': 2632.0,
|
|
1122
|
+
'upload_date': '20251016',
|
|
1123
|
+
'timestamp': 1760645100,
|
|
1124
|
+
'creators': ['DMAX'],
|
|
1125
|
+
'thumbnail': 'https://eu1-prod-images.disco-api.com/2025/10/14/0bdee68c-a8d8-33d9-9204-16eb61108552.jpeg',
|
|
1126
|
+
'tags': [],
|
|
1127
|
+
'categories': ['DMAX Originals', 'Jobs', 'Blaulicht'],
|
|
1128
|
+
},
|
|
1129
|
+
'params': {'skip_download': 'm3u8'},
|
|
1130
|
+
}, {
|
|
1131
|
+
'url': 'https://tlc.de/sendungen/ghost-adventures/der-poltergeist-im-kostumladen',
|
|
1132
|
+
'info_dict': {
|
|
1133
|
+
'id': '4550602',
|
|
1134
|
+
'ext': 'mp4',
|
|
1135
|
+
'title': 'Der Poltergeist im Kostümladen',
|
|
1136
|
+
'description': 'md5:20b52b9736a0a3a7873d19a238fad7fc',
|
|
1137
|
+
'display_id': 'ghost-adventures/der-poltergeist-im-kostumladen',
|
|
1138
|
+
'episode': 'Episode 1',
|
|
1139
|
+
'episode_number': 1,
|
|
1140
|
+
'season': 'Season 25',
|
|
1141
|
+
'season_number': 25,
|
|
1142
|
+
'series': 'Ghost Adventures',
|
|
1143
|
+
'duration': 2493.0,
|
|
1144
|
+
'upload_date': '20241223',
|
|
1145
|
+
'timestamp': 1734948900,
|
|
1146
|
+
'creators': ['TLC'],
|
|
1147
|
+
'thumbnail': 'https://eu1-prod-images.disco-api.com/2023/04/05/59941d26-a81b-365f-829f-69d8cd81fd0f.jpeg',
|
|
1148
|
+
'tags': [],
|
|
1149
|
+
'categories': ['Paranormal', 'Gruselig!'],
|
|
1150
|
+
},
|
|
1151
|
+
'params': {'skip_download': 'm3u8'},
|
|
1152
|
+
}, {
|
|
1153
|
+
'url': 'https://tlc.de/sendungen/evil-gesichter-des-boesen/das-geheimnis-meines-bruders',
|
|
1154
|
+
'info_dict': {
|
|
1155
|
+
'id': '7792288',
|
|
1156
|
+
'ext': 'mp4',
|
|
1157
|
+
'title': 'Das Geheimnis meines Bruders',
|
|
1158
|
+
'description': 'md5:3167550bb582eb9c92875c86a0a20882',
|
|
1159
|
+
'display_id': 'evil-gesichter-des-boesen/das-geheimnis-meines-bruders',
|
|
1160
|
+
'episode': 'Episode 1',
|
|
1161
|
+
'episode_number': 1,
|
|
1162
|
+
'season': 'Season 1',
|
|
1163
|
+
'season_number': 1,
|
|
1164
|
+
'series': 'Evil - Gesichter des Bösen',
|
|
1165
|
+
'duration': 2626.0,
|
|
1166
|
+
'upload_date': '20240926',
|
|
1167
|
+
'timestamp': 1727388000,
|
|
1168
|
+
'creators': ['TLC'],
|
|
1169
|
+
'thumbnail': 'https://eu1-prod-images.disco-api.com/2024/11/29/e9f3e3ae-74ec-3631-81b7-fc7bbe844741.jpeg',
|
|
1170
|
+
'tags': 'count:13',
|
|
1171
|
+
'categories': ['True Crime', 'Mord'],
|
|
1172
|
+
},
|
|
1173
|
+
'params': {'skip_download': 'm3u8'},
|
|
1109
1174
|
}]
|
|
1110
1175
|
|
|
1111
1176
|
def _real_extract(self, url):
|
|
1112
1177
|
domain, programme, alternate_id = self._match_valid_url(url).groups()
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1178
|
+
display_id = f'{programme}/{alternate_id}'
|
|
1179
|
+
meta = self._download_json(
|
|
1180
|
+
f'https://de-api.loma-cms.com/feloma/videos/{alternate_id}/',
|
|
1181
|
+
display_id, query={
|
|
1182
|
+
'environment': domain.split('.')[0],
|
|
1183
|
+
'v': '2',
|
|
1184
|
+
'filter[show.slug]': programme,
|
|
1185
|
+
}, fatal=False)
|
|
1186
|
+
video_id = traverse_obj(meta, ('uid', {str}, {lambda s: s[-7:]})) or display_id
|
|
1187
|
+
|
|
1188
|
+
disco_api_info = self._get_disco_api_info(
|
|
1189
|
+
url, video_id, 'eu1-prod.disco-api.com', domain.replace('.', ''), 'DE')
|
|
1190
|
+
disco_api_info['display_id'] = display_id
|
|
1191
|
+
disco_api_info['categories'] = traverse_obj(meta, (
|
|
1192
|
+
'taxonomies', lambda _, v: v['category'] == 'genre', 'title', {str.strip}, filter, all, filter))
|
|
1193
|
+
|
|
1194
|
+
return disco_api_info
|
|
1117
1195
|
|
|
1118
1196
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
|
1119
1197
|
headers.update({
|
yt_dlp/extractor/firsttv.py
CHANGED
|
@@ -10,7 +10,7 @@ from ..utils import (
|
|
|
10
10
|
unified_strdate,
|
|
11
11
|
url_or_none,
|
|
12
12
|
)
|
|
13
|
-
from ..utils.traversal import traverse_obj
|
|
13
|
+
from ..utils.traversal import require, traverse_obj
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class FirstTVIE(InfoExtractor):
|
|
@@ -129,3 +129,36 @@ class FirstTVIE(InfoExtractor):
|
|
|
129
129
|
return self.playlist_result(
|
|
130
130
|
self._entries(items), display_id, self._og_search_title(webpage, default=None),
|
|
131
131
|
thumbnail=self._og_search_thumbnail(webpage, default=None))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class FirstTVLiveIE(InfoExtractor):
|
|
135
|
+
IE_NAME = '1tv:live'
|
|
136
|
+
IE_DESC = 'Первый канал (прямой эфир)'
|
|
137
|
+
_VALID_URL = r'https?://(?:www\.)?1tv\.ru/live'
|
|
138
|
+
|
|
139
|
+
_TESTS = [{
|
|
140
|
+
'url': 'https://www.1tv.ru/live',
|
|
141
|
+
'info_dict': {
|
|
142
|
+
'id': 'live',
|
|
143
|
+
'ext': 'mp4',
|
|
144
|
+
'title': r're:ПЕРВЫЙ КАНАЛ ПРЯМОЙ ЭФИР СМОТРЕТЬ ОНЛАЙН \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
|
145
|
+
'live_status': 'is_live',
|
|
146
|
+
},
|
|
147
|
+
'params': {'skip_download': 'livestream'},
|
|
148
|
+
}]
|
|
149
|
+
|
|
150
|
+
def _real_extract(self, url):
|
|
151
|
+
display_id = 'live'
|
|
152
|
+
webpage = self._download_webpage(url, display_id, fatal=False)
|
|
153
|
+
|
|
154
|
+
streams_list = self._download_json('https://stream.1tv.ru/api/playlist/1tvch-v1_as_array.json', display_id)
|
|
155
|
+
mpd_url = traverse_obj(streams_list, ('mpd', ..., {url_or_none}, any, {require('mpd url')}))
|
|
156
|
+
# FFmpeg needs to be passed -re to not seek past live window. This is handled by core
|
|
157
|
+
formats, _ = self._extract_mpd_formats_and_subtitles(mpd_url, display_id, mpd_id='dash')
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
'id': display_id,
|
|
161
|
+
'title': self._html_extract_title(webpage),
|
|
162
|
+
'formats': formats,
|
|
163
|
+
'is_live': True,
|
|
164
|
+
}
|
yt_dlp/extractor/floatplane.py
CHANGED
|
@@ -6,15 +6,15 @@ from ..utils import (
|
|
|
6
6
|
OnDemandPagedList,
|
|
7
7
|
clean_html,
|
|
8
8
|
determine_ext,
|
|
9
|
+
float_or_none,
|
|
9
10
|
format_field,
|
|
10
11
|
int_or_none,
|
|
11
12
|
join_nonempty,
|
|
12
|
-
parse_codecs,
|
|
13
13
|
parse_iso8601,
|
|
14
14
|
url_or_none,
|
|
15
15
|
urljoin,
|
|
16
16
|
)
|
|
17
|
-
from ..utils.traversal import traverse_obj
|
|
17
|
+
from ..utils.traversal import require, traverse_obj
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class FloatplaneBaseIE(InfoExtractor):
|
|
@@ -50,37 +50,31 @@ class FloatplaneBaseIE(InfoExtractor):
|
|
|
50
50
|
media_id = media['id']
|
|
51
51
|
media_typ = media.get('type') or 'video'
|
|
52
52
|
|
|
53
|
-
metadata = self._download_json(
|
|
54
|
-
f'{self._BASE_URL}/api/v3/content/{media_typ}', media_id, query={'id': media_id},
|
|
55
|
-
note=f'Downloading {media_typ} metadata', impersonate=self._IMPERSONATE_TARGET)
|
|
56
|
-
|
|
57
53
|
stream = self._download_json(
|
|
58
|
-
f'{self._BASE_URL}/api/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}, note=f'Downloading {media_typ} stream data',
|
|
54
|
+
f'{self._BASE_URL}/api/v3/delivery/info', media_id,
|
|
55
|
+
query={'scenario': 'onDemand', 'entityId': media_id},
|
|
56
|
+
note=f'Downloading {media_typ} stream data',
|
|
62
57
|
impersonate=self._IMPERSONATE_TARGET)
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
metadata = self._download_json(
|
|
60
|
+
f'{self._BASE_URL}/api/v3/content/{media_typ}', media_id,
|
|
61
|
+
f'Downloading {media_typ} metadata', query={'id': media_id},
|
|
62
|
+
fatal=False, impersonate=self._IMPERSONATE_TARGET)
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for i, val in (params or {}).items():
|
|
69
|
-
path = path.replace(f'{{qualityLevelParams.{i}}}', val)
|
|
70
|
-
return path
|
|
64
|
+
cdn_base_url = traverse_obj(stream, (
|
|
65
|
+
'groups', 0, 'origins', ..., 'url', {url_or_none}, any, {require('cdn base url')}))
|
|
71
66
|
|
|
72
67
|
formats = []
|
|
73
|
-
for
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
format_id = traverse_obj(quality, ('name', {str}))
|
|
68
|
+
for variant in traverse_obj(stream, ('groups', 0, 'variants', lambda _, v: v['url'])):
|
|
69
|
+
format_url = urljoin(cdn_base_url, variant['url'])
|
|
70
|
+
format_id = traverse_obj(variant, ('name', {str}))
|
|
77
71
|
hls_aes = {}
|
|
78
72
|
m3u8_data = None
|
|
79
73
|
|
|
80
74
|
# If we need impersonation for the API, then we need it for HLS keys too: extract in advance
|
|
81
75
|
if self._IMPERSONATE_TARGET is not None:
|
|
82
76
|
m3u8_data = self._download_webpage(
|
|
83
|
-
|
|
77
|
+
format_url, media_id, fatal=False, impersonate=self._IMPERSONATE_TARGET, headers=self._HEADERS,
|
|
84
78
|
note=join_nonempty('Downloading', format_id, 'm3u8 information', delim=' '),
|
|
85
79
|
errnote=join_nonempty('Failed to download', format_id, 'm3u8 information', delim=' '))
|
|
86
80
|
if not m3u8_data:
|
|
@@ -98,14 +92,19 @@ class FloatplaneBaseIE(InfoExtractor):
|
|
|
98
92
|
hls_aes['key'] = urlh.read().hex()
|
|
99
93
|
|
|
100
94
|
formats.append({
|
|
101
|
-
**traverse_obj(
|
|
95
|
+
**traverse_obj(variant, {
|
|
102
96
|
'format_note': ('label', {str}),
|
|
103
|
-
'width': ('width', {
|
|
104
|
-
'height': ('height', {
|
|
97
|
+
'width': ('meta', 'video', 'width', {int_or_none}),
|
|
98
|
+
'height': ('meta', 'video', 'height', {int_or_none}),
|
|
99
|
+
'vcodec': ('meta', 'video', 'codec', {str}),
|
|
100
|
+
'acodec': ('meta', 'audio', 'codec', {str}),
|
|
101
|
+
'vbr': ('meta', 'video', 'bitrate', 'average', {int_or_none(scale=1000)}),
|
|
102
|
+
'abr': ('meta', 'audio', 'bitrate', 'average', {int_or_none(scale=1000)}),
|
|
103
|
+
'audio_channels': ('meta', 'audio', 'channelCount', {int_or_none}),
|
|
104
|
+
'fps': ('meta', 'video', 'fps', {float_or_none}),
|
|
105
105
|
}),
|
|
106
|
-
|
|
107
|
-
'
|
|
108
|
-
'ext': determine_ext(url.partition('/chunk.m3u8')[0], 'mp4'),
|
|
106
|
+
'url': format_url,
|
|
107
|
+
'ext': determine_ext(format_url.partition('/chunk.m3u8')[0], 'mp4'),
|
|
109
108
|
'format_id': format_id,
|
|
110
109
|
'hls_media_playlist_data': m3u8_data,
|
|
111
110
|
'hls_aes': hls_aes or None,
|
yt_dlp/extractor/goplay.py
CHANGED
|
@@ -13,12 +13,14 @@ from ..utils.traversal import get_first, traverse_obj
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class GoPlayIE(InfoExtractor):
|
|
16
|
-
|
|
16
|
+
IE_NAME = 'play.tv'
|
|
17
|
+
IE_DESC = 'PLAY (formerly goplay.be)'
|
|
18
|
+
_VALID_URL = r'https?://(www\.)?play\.tv/video/([^/?#]+/[^/?#]+/|)(?P<id>[^/#]+)'
|
|
17
19
|
|
|
18
20
|
_NETRC_MACHINE = 'goplay'
|
|
19
21
|
|
|
20
22
|
_TESTS = [{
|
|
21
|
-
'url': 'https://www.
|
|
23
|
+
'url': 'https://www.play.tv/video/de-slimste-mens-ter-wereld/de-slimste-mens-ter-wereld-s22/de-slimste-mens-ter-wereld-s22-aflevering-1',
|
|
22
24
|
'info_dict': {
|
|
23
25
|
'id': '2baa4560-87a0-421b-bffc-359914e3c387',
|
|
24
26
|
'ext': 'mp4',
|
|
@@ -33,7 +35,7 @@ class GoPlayIE(InfoExtractor):
|
|
|
33
35
|
'params': {'skip_download': True},
|
|
34
36
|
'skip': 'This video is only available for registered users',
|
|
35
37
|
}, {
|
|
36
|
-
'url': 'https://www.
|
|
38
|
+
'url': 'https://www.play.tv/video/1917',
|
|
37
39
|
'info_dict': {
|
|
38
40
|
'id': '40cac41d-8d29-4ef5-aa11-75047b9f0907',
|
|
39
41
|
'ext': 'mp4',
|
|
@@ -43,7 +45,7 @@ class GoPlayIE(InfoExtractor):
|
|
|
43
45
|
'params': {'skip_download': True},
|
|
44
46
|
'skip': 'This video is only available for registered users',
|
|
45
47
|
}, {
|
|
46
|
-
'url': 'https://www.
|
|
48
|
+
'url': 'https://www.play.tv/video/de-mol/de-mol-s11/de-mol-s11-aflevering-1#autoplay',
|
|
47
49
|
'info_dict': {
|
|
48
50
|
'id': 'ecb79672-92b9-4cd9-a0d7-e2f0250681ee',
|
|
49
51
|
'ext': 'mp4',
|
|
@@ -101,7 +103,7 @@ class GoPlayIE(InfoExtractor):
|
|
|
101
103
|
break
|
|
102
104
|
|
|
103
105
|
api = self._download_json(
|
|
104
|
-
f'https://api.
|
|
106
|
+
f'https://api.play.tv/web/v1/videos/long-form/{video_id}',
|
|
105
107
|
video_id, headers={
|
|
106
108
|
'Authorization': f'Bearer {self._id_token}',
|
|
107
109
|
**self.geo_verification_headers(),
|
yt_dlp/extractor/kika.py
CHANGED
|
@@ -17,57 +17,60 @@ class KikaIE(InfoExtractor):
|
|
|
17
17
|
_GEO_COUNTRIES = ['DE']
|
|
18
18
|
|
|
19
19
|
_TESTS = [{
|
|
20
|
-
|
|
21
|
-
'
|
|
20
|
+
# Video without season/episode info
|
|
21
|
+
'url': 'https://www.kika.de/logo/videos/logo-vom-dienstag-achtundzwanzig-oktober-zweitausendfuenfundzwanzig-100',
|
|
22
|
+
'md5': '4a9f6e0f9c6bfcc82394c294f186d6db',
|
|
22
23
|
'info_dict': {
|
|
23
|
-
'id': 'logo-vom-
|
|
24
|
+
'id': 'logo-vom-dienstag-achtundzwanzig-oktober-zweitausendfuenfundzwanzig-100',
|
|
24
25
|
'ext': 'mp4',
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'duration': 634,
|
|
33
|
-
'title': 'logo! vom Samstag, 31. August 2024',
|
|
34
|
-
'modified_timestamp': 1725129983,
|
|
26
|
+
'title': 'logo! vom Dienstag, 28. Oktober 2025',
|
|
27
|
+
'description': 'md5:4d28b92cef423bec99740ffaa3e7ec04',
|
|
28
|
+
'duration': 651,
|
|
29
|
+
'timestamp': 1761678000,
|
|
30
|
+
'upload_date': '20251028',
|
|
31
|
+
'modified_timestamp': 1761682624,
|
|
32
|
+
'modified_date': '20251028',
|
|
35
33
|
},
|
|
36
34
|
}, {
|
|
35
|
+
# Video with season/episode info
|
|
36
|
+
# Also: Video with subtitles
|
|
37
37
|
'url': 'https://www.kika.de/kaltstart/videos/video92498',
|
|
38
|
-
'md5': '
|
|
38
|
+
'md5': 'e58073070acb195906c55c4ad31dceb3',
|
|
39
39
|
'info_dict': {
|
|
40
40
|
'id': 'video92498',
|
|
41
41
|
'ext': 'mp4',
|
|
42
42
|
'title': '7. Wo ist Leo?',
|
|
43
43
|
'description': 'md5:fb48396a5b75068bcac1df74f1524920',
|
|
44
44
|
'duration': 436,
|
|
45
|
+
'season': 'Season 1',
|
|
46
|
+
'season_number': 1,
|
|
47
|
+
'episode': 'Episode 7',
|
|
48
|
+
'episode_number': 7,
|
|
45
49
|
'timestamp': 1702926876,
|
|
46
50
|
'upload_date': '20231218',
|
|
47
|
-
'episode_number': 7,
|
|
48
|
-
'modified_date': '20240319',
|
|
49
51
|
'modified_timestamp': 1710880610,
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'season': 'Season 1',
|
|
52
|
+
'modified_date': '20240319',
|
|
53
|
+
'subtitles': 'count:1',
|
|
53
54
|
},
|
|
54
55
|
}, {
|
|
55
|
-
|
|
56
|
-
'
|
|
56
|
+
# Video without subtitles
|
|
57
|
+
'url': 'https://www.kika.de/die-pfefferkoerner/videos/abgezogen-102',
|
|
58
|
+
'md5': '62e97961ce5343c19f0f330a1b6dd736',
|
|
57
59
|
'info_dict': {
|
|
58
|
-
'id': '
|
|
60
|
+
'id': 'abgezogen-102',
|
|
59
61
|
'ext': 'mp4',
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'duration':
|
|
63
|
-
'modified_timestamp': 1711093771,
|
|
64
|
-
'episode_number': 8,
|
|
65
|
-
'title': 'Es ist nicht leicht, ein Astrobrot zu sein',
|
|
66
|
-
'modified_date': '20240322',
|
|
67
|
-
'description': 'md5:d3641deaf1b5515a160788b2be4159a9',
|
|
68
|
-
'season_number': 1,
|
|
69
|
-
'episode': 'Episode 8',
|
|
62
|
+
'title': '1. Abgezogen',
|
|
63
|
+
'description': 'md5:42d87963364391f9f8eba8affcb30bd2',
|
|
64
|
+
'duration': 1574,
|
|
70
65
|
'season': 'Season 1',
|
|
66
|
+
'season_number': 1,
|
|
67
|
+
'episode': 'Episode 1',
|
|
68
|
+
'episode_number': 1,
|
|
69
|
+
'timestamp': 1735382700,
|
|
70
|
+
'upload_date': '20241228',
|
|
71
|
+
'modified_timestamp': 1757344051,
|
|
72
|
+
'modified_date': '20250908',
|
|
73
|
+
'subtitles': 'count:0',
|
|
71
74
|
},
|
|
72
75
|
}]
|
|
73
76
|
|
|
@@ -78,16 +81,19 @@ class KikaIE(InfoExtractor):
|
|
|
78
81
|
video_assets = self._download_json(doc['assets']['url'], video_id)
|
|
79
82
|
|
|
80
83
|
subtitles = {}
|
|
81
|
-
if
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
# Subtitle API endpoints may be present in the JSON even if there are no subtitles.
|
|
85
|
+
# They then return HTTP 200 with invalid data. So we must check explicitly.
|
|
86
|
+
if doc.get('hasSubtitle'):
|
|
87
|
+
if ttml_resource := url_or_none(video_assets.get('videoSubtitle')):
|
|
88
|
+
subtitles['de'] = [{
|
|
89
|
+
'url': ttml_resource,
|
|
90
|
+
'ext': 'ttml',
|
|
91
|
+
}]
|
|
92
|
+
if webvtt_resource := url_or_none(video_assets.get('webvttUrl')):
|
|
93
|
+
subtitles.setdefault('de', []).append({
|
|
94
|
+
'url': webvtt_resource,
|
|
95
|
+
'ext': 'vtt',
|
|
96
|
+
})
|
|
91
97
|
|
|
92
98
|
return {
|
|
93
99
|
'id': video_id,
|