Compare commits

...

11 Commits

Author SHA1 Message Date
Peter Rowlands (변기호)
fcf5e9c36d
Merge bd62cdba1a into a9f85670d0 2024-11-12 01:16:18 +00:00
manav_chaudhary
a9f85670d0
[ie/Chaturbate] Support alternate domains (#10595)
Closes #10594
Authored by: manavchaudhary1
2024-11-11 23:41:56 +01:00
Sam
6b43a8d84b
[ie/goplay] Fix extractor (#11466)
Closes #10857
Authored by: SamDecrock, bashonly

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2024-11-11 22:03:31 +00:00
Hugo
2db8c2e7d5
[ie/CloudflareStream] Avoid extraction via videodelivery.net (#11478)
Closes #11477
Authored by: hugovdev
2024-11-11 22:00:05 +00:00
bashonly
f9c8deb4e5
[build] Bump PyInstaller version pin to >=6.11.1 (#11507)
Authored by: bashonly
2024-11-11 21:19:03 +00:00
Sakura286
0ec9bfed4d
[ie/MixchMovie] Add extractor (#10897)
Closes #10765
Authored by: Sakura286
2024-11-11 21:40:29 +01:00
Subrat Lima
c673731061
[ie/spreaker] Support podcast and feed pages (#10968)
Closes #10925
Authored by: subrat-lima
2024-11-11 20:08:18 +01:00
Peter Rowlands
bd62cdba1a [fd/dash] support DASH SEA (AES-128-CBC) decryption 2024-10-05 17:21:50 +09:00
Peter Rowlands
e0ce6eed92 [extractor] Parse DASH-SEA content protection in DASH manifests 2024-10-05 17:21:47 +09:00
Peter Rowlands
6b0ce31939 [fd/dash, pp/ffmpeg] support DASH CENC decryption 2024-10-05 00:59:58 +09:00
Peter Rowlands
a95757d3b7 [extractor] parse CENC + Clear Key information in DASH manifests 2024-10-04 21:02:26 +09:00
17 changed files with 691 additions and 87 deletions

View File

@ -411,7 +411,7 @@ jobs:
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
python devscripts/install_deps.py -o --include build
python devscripts/install_deps.py --include curl-cffi
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.10.0-py3-none-any.whl"
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.11.1-py3-none-any.whl"
- name: Prepare
run: |
@ -460,7 +460,7 @@ jobs:
run: |
python devscripts/install_deps.py -o --include build
python devscripts/install_deps.py
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.10.0-py3-none-any.whl"
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.11.1-py3-none-any.whl"
- name: Prepare
run: |

View File

@ -83,7 +83,7 @@ test = [
"pytest-rerunfailures~=14.0",
]
pyinstaller = [
"pyinstaller>=6.10.0", # Windows temp cleanup fixed in 6.10.0
"pyinstaller>=6.11.1", # Windows temp cleanup fixed in 6.11.1
]
[project.urls]

View File

@ -1381,6 +1381,175 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
},
],
},
), (
# Clear Key with CENC default_KID
'clearkey_cenc',
'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd', # mpd_url
'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/', # mpd_base_url
[{
'manifest_url': 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd',
'ext': 'mp4',
'format_id': '1',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.64001f',
'tbr': 389.802,
'width': 512,
'height': 288,
'dash_cenc': {
'laurl': 'https://drm-clearkey-testvectors.axtest.net/AcquireLicense',
'key_ids': ['9eb4050de44b4802932e27d75083e266'],
},
}, {
'manifest_url': 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd',
'ext': 'mp4',
'format_id': '2',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.64001f',
'tbr': 764.935,
'width': 640,
'height': 360,
'dash_cenc': {
'laurl': 'https://drm-clearkey-testvectors.axtest.net/AcquireLicense',
'key_ids': ['9eb4050de44b4802932e27d75083e266'],
},
}, {
'manifest_url': 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd',
'ext': 'mp4',
'format_id': '3',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.640028',
'tbr': 1120.439,
'width': 852,
'height': 480,
'dash_cenc': {
'laurl': 'https://drm-clearkey-testvectors.axtest.net/AcquireLicense',
'key_ids': ['9eb4050de44b4802932e27d75083e266'],
},
}, {
'manifest_url': 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd',
'ext': 'mp4',
'format_id': '4',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.640032',
'tbr': 1945.258,
'width': 1280,
'height': 720,
'dash_cenc': {
'laurl': 'https://drm-clearkey-testvectors.axtest.net/AcquireLicense',
'key_ids': ['9eb4050de44b4802932e27d75083e266'],
},
}, {
'manifest_url': 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd',
'ext': 'mp4',
'format_id': '5',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.640033',
'tbr': 2726.377,
'width': 1920,
'height': 1080,
'dash_cenc': {
'laurl': 'https://drm-clearkey-testvectors.axtest.net/AcquireLicense',
'key_ids': ['9eb4050de44b4802932e27d75083e266'],
},
}],
{},
), (
# default CENC KID overridden via W3C PSSH box, no license server in manifest
'w3c_pssh',
'https://unknown/manifest.mpd', # mpd_url
'https://unknown/', # mpd_base_url
[{
'manifest_url': 'https://unknown/manifest.mpd',
'ext': 'mp4',
'format_id': '1',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.64001f',
'tbr': 389.802,
'width': 512,
'height': 288,
'dash_cenc': {
'key_ids': ['43215678123412341234123412341234'],
},
'has_drm': True,
}],
{},
), (
# DASH SEA with AES-128-CBC
'dash_sea',
'https://unknown/manifest.mpd', # mpd_url
'https://unknown/', # mpd_base_url
[{
'manifest_url': 'https://unknown/manifest.mpd',
'ext': 'm4a',
'format_id': '5_A_aac_eng_2_127999_2_1_1',
'format_note': 'DASH audio',
'protocol': 'http_dash_segments',
'acodec': 'mp4a.40.2',
'vcodec': 'none',
'tbr': 127.999,
'hls_aes': {
'uri': 'https://zavideoplatform.keydelivery.eastus.media.azure.net/?kid=9280864f-064e-48c0-97e0-f2bcb1d8d012',
'iv': '0x7BD31E102B0CE9CCD39691782533656C',
},
}, {
'manifest_url': 'https://unknown/manifest.mpd',
'ext': 'mp4',
'format_id': '1_V_video_3',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.64001F',
'tbr': 258.591,
'width': 960,
'height': 540,
'hls_aes': {
'uri': 'https://zavideoplatform.keydelivery.eastus.media.azure.net/?kid=9280864f-064e-48c0-97e0-f2bcb1d8d012',
'iv': '0x7BD31E102B0CE9CCD39691782533656C',
},
}, {
'manifest_url': 'https://unknown/manifest.mpd',
'ext': 'mp4',
'format_id': '1_V_video_2',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.64001F',
'tbr': 422.519,
'width': 1280,
'height': 720,
'hls_aes': {
'uri': 'https://zavideoplatform.keydelivery.eastus.media.azure.net/?kid=9280864f-064e-48c0-97e0-f2bcb1d8d012',
'iv': '0x7BD31E102B0CE9CCD39691782533656C',
},
}, {
'manifest_url': 'https://unknown/manifest.mpd',
'ext': 'mp4',
'format_id': '1_V_video_1',
'format_note': 'DASH video',
'protocol': 'http_dash_segments',
'acodec': 'none',
'vcodec': 'avc1.640028',
'tbr': 628.102,
'width': 1920,
'height': 1080,
'hls_aes': {
'uri': 'https://zavideoplatform.keydelivery.eastus.media.azure.net/?kid=9280864f-064e-48c0-97e0-f2bcb1d8d012',
'iv': '0x7BD31E102B0CE9CCD39691782533656C',
},
}],
{},
),
]

29
test/testdata/mpd/clearkey_cenc.mpd vendored Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Version information:
Axinom.MediaProcessing v3.0.0 targeting General Purpose Media Format specification v7
ffmpeg version N-81423-g61fac0e Copyright (c) 2000-2016 the FFmpeg developers
x265 [info]: HEVC encoder version 2.0+12-49a0d1176aef5bc6
x264 0.148.2705 3f5ed56
MP4Box - GPAC version 0.6.2-DEV-rev683-g7b29fbe-master
MediaInfoLib - v0.7.87
For more info about this video, see https://github.com/Axinom/dash-test-vectors
-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H12M14.000S" maxSegmentDuration="PT0H0M4.000S" profiles="urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash264" xmlns:cenc="urn:mpeg:cenc:2013" xmlns:clearkey="http://dashif.org/guidelines/clearKey">
<Period duration="PT0H12M14.000S">
<AdaptationSet segmentAlignment="true" group="1" maxWidth="1920" maxHeight="1080" maxFrameRate="24" par="16:9" lang="und">
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="9eb4050d-e44b-4802-932e-27d75083e266" />
<ContentProtection value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
<clearkey:Laurl Lic_type="EME-1.0">https://drm-clearkey-testvectors.axtest.net/AcquireLicense</clearkey:Laurl>
</ContentProtection>
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
<SegmentTemplate timescale="24" media="$RepresentationID$/$Number%04d$.m4s" startNumber="1" duration="96" initialization="$RepresentationID$/init.mp4" />
<Representation id="1" mimeType="video/mp4" codecs="avc1.64001f" width="512" height="288" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="389802"></Representation>
<Representation id="2" mimeType="video/mp4" codecs="avc1.64001f" width="640" height="360" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="764935"></Representation>
<Representation id="3" mimeType="video/mp4" codecs="avc1.640028" width="852" height="480" frameRate="24" sar="640:639" startWithSAP="1" bandwidth="1120439"></Representation>
<Representation id="4" mimeType="video/mp4" codecs="avc1.640032" width="1280" height="720" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="1945258"></Representation>
<Representation id="5" mimeType="video/mp4" codecs="avc1.640033" width="1920" height="1080" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="2726377"></Representation>
</AdaptationSet>
</Period>
</MPD>

109
test/testdata/mpd/dash_sea.mpd vendored Normal file
View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<MPD
xmlns="urn:mpeg:dash:schema:mpd:2011"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="static"
xmlns:sea="urn:mpeg:dash:schema:sea:2012" mediaPresentationDuration="PT3M32.949S" minBufferTime="PT3S">
<Period>
<AdaptationSet id="1" group="5" profiles="ccff" bitstreamSwitching="false" segmentAlignment="true" contentType="audio" mimeType="audio/mp4" codecs="mp4a.40.2" lang="en">
<ContentProtection schemeIdUri="urn:mpeg:dash:sea:2012">
<sea:SegmentEncryption schemeIdUri="urn:mpeg:dash:sea:aes128-cbc:2013"/>
<sea:KeySystem keySystemUri="urn:mpeg:dash:sea:keysys:http:2013"/>
<sea:CryptoPeriod keyUriTemplate="https://zavideoplatform.keydelivery.eastus.media.azure.net/?kid=9280864f-064e-48c0-97e0-f2bcb1d8d012" IV="0x7BD31E102B0CE9CCD39691782533656C"/>
</ContentProtection>
<Label>aac_eng_2_127999_2_1</Label>
<SegmentTemplate timescale="10000000" media="QualityLevels($Bandwidth$)/Fragments(aac_eng_2_127999_2_1=$Time$,format=mpd-time-csf)" initialization="QualityLevels($Bandwidth$)/Fragments(aac_eng_2_127999_2_1=i,format=mpd-time-csf)">
<SegmentTimeline>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333" r="1"/>
<S d="20053334"/>
<S d="20053333"/>
<S d="3840000"/>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="5_A_aac_eng_2_127999_2_1_1" bandwidth="127999" audioSamplingRate="48000"/>
</AdaptationSet>
<AdaptationSet id="2" group="1" profiles="ccff" bitstreamSwitching="false" segmentAlignment="true" contentType="video" mimeType="video/mp4" codecs="avc1.640028" maxWidth="1920" maxHeight="1080" startWithSAP="1">
<ContentProtection schemeIdUri="urn:mpeg:dash:sea:2012">
<sea:SegmentEncryption schemeIdUri="urn:mpeg:dash:sea:aes128-cbc:2013"/>
<sea:KeySystem keySystemUri="urn:mpeg:dash:sea:keysys:http:2013"/>
<sea:CryptoPeriod keyUriTemplate="https://zavideoplatform.keydelivery.eastus.media.azure.net/?kid=9280864f-064e-48c0-97e0-f2bcb1d8d012" IV="0x7BD31E102B0CE9CCD39691782533656C"/>
</ContentProtection>
<SegmentTemplate timescale="10000000" media="QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)" initialization="QualityLevels($Bandwidth$)/Fragments(video=i,format=mpd-time-csf)">
<SegmentTimeline>
<S d="20000000" r="105"/>
<S d="8666666"/>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="1_V_video_1" bandwidth="628102" width="1920" height="1080"/>
<Representation id="1_V_video_2" bandwidth="422519" codecs="avc1.64001F" width="1280" height="720"/>
<Representation id="1_V_video_3" bandwidth="258591" codecs="avc1.64001F" width="960" height="540"/>
</AdaptationSet>
</Period>
</MPD>

13
test/testdata/mpd/w3c_pssh.mpd vendored Normal file
View File

@ -0,0 +1,13 @@
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H12M14.000S" maxSegmentDuration="PT0H0M4.000S" profiles="urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash264" xmlns:cenc="urn:mpeg:cenc:2013" xmlns:clearkey="http://dashif.org/guidelines/clearKey">
<Period duration="PT0H12M14.000S">
<AdaptationSet segmentAlignment="true" group="1" maxWidth="1920" maxHeight="1080" maxFrameRate="24" par="16:9" lang="und">
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="9eb4050d-e44b-4802-932e-27d75083e266" />
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAFDIVZ4EjQSNBI0EjQSNBI0AAAAAA==</cenc:pssh>
</ContentProtection>
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
<SegmentTemplate timescale="24" media="$RepresentationID$/$Number%04d$.m4s" startNumber="1" duration="96" initialization="$RepresentationID$/init.mp4" />
<Representation id="1" mimeType="video/mp4" codecs="avc1.64001f" width="512" height="288" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="389802"></Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -48,6 +48,7 @@ from .plugins import directories as plugin_directories
from .postprocessor import _PLUGIN_CLASSES as plugin_pps
from .postprocessor import (
EmbedThumbnailPP,
FFmpegCENCDecryptPP,
FFmpegFixupDuplicateMoovPP,
FFmpegFixupDurationPP,
FFmpegFixupM3u8PP,
@ -3380,6 +3381,8 @@ class YoutubeDL:
self.report_error(f'{msg}. Aborting')
return
decrypter = FFmpegCENCDecryptPP(self)
info_dict.setdefault('__files_to_cenc_decrypt', [])
if info_dict.get('requested_formats') is not None:
old_ext = info_dict['ext']
if self.params.get('merge_output_format') is None:
@ -3460,8 +3463,12 @@ class YoutubeDL:
downloaded.append(fname)
partial_success, real_download = self.dl(fname, new_info)
info_dict['__real_download'] = info_dict['__real_download'] or real_download
if new_info.get('dash_cenc', {}).get('key'):
info_dict['__files_to_cenc_decrypt'].append((fname, new_info['dash_cenc']['key']))
success = success and partial_success
if downloaded and info_dict['__files_to_cenc_decrypt'] and decrypter.available:
info_dict['__postprocessors'].append(decrypter)
if downloaded and merger.available and not self.params.get('allow_unplayable_formats'):
info_dict['__postprocessors'].append(merger)
info_dict['__files_to_merge'] = downloaded
@ -3478,6 +3485,9 @@ class YoutubeDL:
# So we should try to resume the download
success, real_download = self.dl(temp_filename, info_dict)
info_dict['__real_download'] = real_download
if info_dict.get('dash_cenc', {}).get('key') and decrypter.available:
info_dict['__postprocessors'].append(decrypter)
info_dict['__files_to_cenc_decrypt'] = [(temp_filename, info_dict['dash_cenc']['key'])]
else:
self.report_file_already_downloaded(dl_filename)

View File

@ -1,9 +1,14 @@
import base64
import binascii
import json
import time
import urllib.parse
from . import get_suitable_downloader
from .fragment import FragmentFD
from ..utils import update_url_query, urljoin
from ..networking import Request
from ..networking.exceptions import RequestError
from ..utils import remove_start, traverse_obj, update_url_query, urljoin
class DashSegmentsFD(FragmentFD):
@ -49,6 +54,25 @@ class DashSegmentsFD(FragmentFD):
if extra_param_to_segment_url:
extra_query = urllib.parse.parse_qs(extra_param_to_segment_url)
hls_aes = fmt.get('hls_aes', {})
if hls_aes:
decrypt_info = {'METHOD', 'AES-128'}
key = hls_aes.get('key')
if key:
key = binascii.unhexlify(remove_start(key, '0x'))
assert len(key) in (16, 24, 32), 'Invalid length for HLS AES-128 key'
decrypt_info['KEY'] = key
iv = hls_aes.get('iv')
if iv:
iv = binascii.unhexlify(remove_start(iv, '0x').zfill(32))
decrypt_info['IV'] = iv
uri = hls_aes.get('uri')
if uri:
if extra_query:
uri = update_url_query(uri, extra_query)
decrypt_info['URI'] = uri
ctx['decrypt_info'] = decrypt_info
fragments_to_download = self._get_fragments(fmt, ctx, extra_query)
if real_downloader:
@ -60,6 +84,12 @@ class DashSegmentsFD(FragmentFD):
args.append([ctx, fragments_to_download, fmt])
cenc_key = traverse_obj(info_dict, ('dash_cenc', 'key'))
cenc_key_ids = traverse_obj(info_dict, ('dash_cenc', 'key_ids'))
clearkey_laurl = traverse_obj(info_dict, ('dash_cenc', 'laurl'))
if not cenc_key and cenc_key_ids and clearkey_laurl:
self._get_clearkey_cenc(info_dict, clearkey_laurl, cenc_key_ids)
return self.download_and_append_fragments_multiple(*args, is_fatal=lambda idx: idx == 0)
def _resolve_fragments(self, fragments, ctx):
@ -87,4 +117,35 @@ class DashSegmentsFD(FragmentFD):
'fragment_count': fragment.get('fragment_count'),
'index': i,
'url': fragment_url,
'decrypt_info': ctx.get('decrypt_info', {'METHOD': 'NONE'}),
}
def _get_clearkey_cenc(self, info_dict, laurl, key_ids):
dash_cenc = info_dict.get('dash_cenc', {})
payload = json.dumps({
'kids': [
base64.urlsafe_b64encode(bytes.fromhex(k)).decode().rstrip('=')
for k in key_ids
],
'type': 'temporary',
}).encode()
try:
response = self.ydl.urlopen(Request(
laurl, data=payload, headers={'Content-Type': 'application/json'}))
data = json.loads(response.read())
except (RequestError, json.JSONDecodeError) as err:
self.report_error(f'Failed to retrieve key from Clear Key license server: {err}')
return
keys = data.get('keys', [])
if len(keys) > 1:
self.report_warning('Clear Key license server returned multiple keys but only single key CENC is supported')
for key in keys:
k = key.get('k')
if k:
try:
dash_cenc.update({'key': base64.urlsafe_b64decode(f'{k}==').hex()})
info_dict['dash_cenc'] = dash_cenc
return
except (ValueError, binascii.Error):
pass
self.report_error('Clear key license server did not return any valid CENC keys')

View File

@ -1156,6 +1156,7 @@ from .mitele import MiTeleIE
from .mixch import (
MixchArchiveIE,
MixchIE,
MixchMovieIE,
)
from .mixcloud import (
MixcloudIE,
@ -1940,7 +1941,6 @@ from .spotify import (
from .spreaker import (
SpreakerIE,
SpreakerShowIE,
SpreakerShowPageIE,
)
from .springboardplatform import SpringboardPlatformIE
from .sprout import SproutIE

View File

@ -9,7 +9,7 @@ from ..utils import (
class ChaturbateIE(InfoExtractor):
_VALID_URL = r'https?://(?:[^/]+\.)?chaturbate\.com/(?:fullvideo/?\?.*?\bb=)?(?P<id>[^/?&#]+)'
_VALID_URL = r'https?://(?:[^/]+\.)?chaturbate\.(?P<tld>com|eu|global)/(?:fullvideo/?\?.*?\bb=)?(?P<id>[^/?&#]+)'
_TESTS = [{
'url': 'https://www.chaturbate.com/siswet19/',
'info_dict': {
@ -29,15 +29,24 @@ class ChaturbateIE(InfoExtractor):
}, {
'url': 'https://en.chaturbate.com/siswet19/',
'only_matching': True,
}, {
'url': 'https://chaturbate.eu/siswet19/',
'only_matching': True,
}, {
'url': 'https://chaturbate.eu/fullvideo/?b=caylin',
'only_matching': True,
}, {
'url': 'https://chaturbate.global/siswet19/',
'only_matching': True,
}]
_ROOM_OFFLINE = 'Room is currently offline'
def _real_extract(self, url):
video_id = self._match_id(url)
video_id, tld = self._match_valid_url(url).group('id', 'tld')
webpage = self._download_webpage(
f'https://chaturbate.com/{video_id}/', video_id,
f'https://chaturbate.{tld}/{video_id}/', video_id,
headers=self.geo_verification_headers())
found_m3u8_urls = []

View File

@ -8,7 +8,7 @@ class CloudflareStreamIE(InfoExtractor):
_DOMAIN_RE = r'(?:cloudflarestream\.com|(?:videodelivery|bytehighway)\.net)'
_EMBED_RE = rf'(?:embed\.|{_SUBDOMAIN_RE}){_DOMAIN_RE}/embed/[^/?#]+\.js\?(?:[^#]+&)?video='
_ID_RE = r'[\da-f]{32}|eyJ[\w-]+\.[\w-]+\.[\w-]+'
_VALID_URL = rf'https?://(?:{_SUBDOMAIN_RE}{_DOMAIN_RE}/|{_EMBED_RE})(?P<id>{_ID_RE})'
_VALID_URL = rf'https?://(?:{_SUBDOMAIN_RE}(?P<domain>{_DOMAIN_RE})/|{_EMBED_RE})(?P<id>{_ID_RE})'
_EMBED_REGEX = [
rf'<script[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//{_EMBED_RE}(?:{_ID_RE})(?:(?!\1).)*)\1',
rf'<iframe[^>]+\bsrc=["\'](?P<url>https?://{_SUBDOMAIN_RE}{_DOMAIN_RE}/[\da-f]{{32}})',
@ -19,7 +19,7 @@ class CloudflareStreamIE(InfoExtractor):
'id': '31c9291ab41fac05471db4e73aa11717',
'ext': 'mp4',
'title': '31c9291ab41fac05471db4e73aa11717',
'thumbnail': 'https://videodelivery.net/31c9291ab41fac05471db4e73aa11717/thumbnails/thumbnail.jpg',
'thumbnail': 'https://cloudflarestream.com/31c9291ab41fac05471db4e73aa11717/thumbnails/thumbnail.jpg',
},
'params': {
'skip_download': 'm3u8',
@ -30,7 +30,7 @@ class CloudflareStreamIE(InfoExtractor):
'id': '0e8e040aec776862e1d632a699edf59e',
'ext': 'mp4',
'title': '0e8e040aec776862e1d632a699edf59e',
'thumbnail': 'https://videodelivery.net/0e8e040aec776862e1d632a699edf59e/thumbnails/thumbnail.jpg',
'thumbnail': 'https://cloudflarestream.com/0e8e040aec776862e1d632a699edf59e/thumbnails/thumbnail.jpg',
},
}, {
'url': 'https://watch.cloudflarestream.com/9df17203414fd1db3e3ed74abbe936c1',
@ -54,7 +54,7 @@ class CloudflareStreamIE(InfoExtractor):
'id': 'eaef9dea5159cf968be84241b5cedfe7',
'ext': 'mp4',
'title': 'eaef9dea5159cf968be84241b5cedfe7',
'thumbnail': 'https://videodelivery.net/eaef9dea5159cf968be84241b5cedfe7/thumbnails/thumbnail.jpg',
'thumbnail': 'https://cloudflarestream.com/eaef9dea5159cf968be84241b5cedfe7/thumbnails/thumbnail.jpg',
},
'params': {
'skip_download': 'm3u8',
@ -62,8 +62,9 @@ class CloudflareStreamIE(InfoExtractor):
}]
def _real_extract(self, url):
video_id = self._match_id(url)
domain = 'bytehighway.net' if 'bytehighway.net/' in url else 'videodelivery.net'
video_id, domain = self._match_valid_url(url).group('id', 'domain')
if domain != 'bytehighway.net':
domain = 'cloudflarestream.com'
base_url = f'https://{domain}/{video_id}/'
if '.' in video_id:
video_id = self._parse_json(base64.urlsafe_b64decode(

View File

@ -14,12 +14,14 @@ import netrc
import os
import random
import re
import struct
import subprocess
import sys
import time
import types
import urllib.parse
import urllib.request
import uuid
import xml.etree.ElementTree
from ..compat import (
@ -247,7 +249,9 @@ class InfoExtractor:
* hls_aes A dictionary of HLS AES-128 decryption information
used by the native HLS downloader to override the
values in the media playlist when an '#EXT-X-KEY' tag
is present in the playlist:
is present in the playlist. Used by the native DASH downloader
when DASH-SEA with AES-128-CBC content protection is present
in the manifest.:
* uri The URI from which the key will be downloaded
* key The key (as hex) used to decrypt fragments.
If `key` is given, any key URI will be ignored
@ -259,6 +263,16 @@ class InfoExtractor:
* ffmpeg_args_out Extra arguments for ffmpeg downloader (output)
* is_dash_periods Whether the format is a result of merging
multiple DASH periods.
* dash_cenc A dictionary of DASH CENC decryption information
used by the native DASH downloader when MPEG CENC content protection
is present in the manifest.
* laurl The Clear Key license server URL from which
CENC keys will be downloaded.
* key_ids List of key IDs (as hex) to request from the ClearKey
license server.
* key The CENC key (as hex) used to decrypt fragments.
If `key` is given, any license server URL and
key IDs will be ignored.
RTMP formats can also have the additional fields: page_url,
app, play_path, tc_url, flash_version, rtmp_live, rtmp_conn,
rtmp_protocol, rtmp_real_time
@ -2679,7 +2693,11 @@ class InfoExtractor:
assert 'is_dash_periods' not in f, 'format already processed'
f['is_dash_periods'] = True
format_key = tuple(v for k, v in f.items() if k not in (
('format_id', 'fragments', 'manifest_stream_number')))
('format_id', 'fragments', 'manifest_stream_number', 'dash_cenc', 'hls_aes')))
for k in ('dash_cenc', 'hls_aes'):
if k in f:
format_key = format_key + tuple(
tuple(v) if isinstance(v, list) else v for v in f[k].values())
if format_key not in formats:
formats[format_key] = f
elif 'fragments' in f:
@ -2713,8 +2731,16 @@ class InfoExtractor:
def _add_ns(path):
return self._xpath_ns(path, namespace)
def is_drm_protected(element):
return element.find(_add_ns('ContentProtection')) is not None
def extract_drm_info(element):
info = {}
has_drm = False
for cp_e in element.findall(_add_ns('ContentProtection')):
has_drm = True
self._extract_mpd_content_protection_info(cp_e, info)
cenc_info = info.get('dash_cenc', {})
if has_drm and not ('hls_aes' in info or cenc_info.get('key') or (cenc_info.get('laurl') and cenc_info.get('key_ids'))):
info['has_drm'] = True
return info
def extract_multisegment_info(element, ms_parent_info):
ms_info = ms_parent_info.copy()
@ -2788,6 +2814,7 @@ class InfoExtractor:
'timescale': 1,
})
for adaptation_set in period.findall(_add_ns('AdaptationSet')):
adaptation_set_drm_info = extract_drm_info(adaptation_set)
adaption_set_ms_info = extract_multisegment_info(adaptation_set, period_ms_info)
for representation in adaptation_set.findall(_add_ns('Representation')):
representation_attrib = adaptation_set.attrib.copy()
@ -2874,8 +2901,8 @@ class InfoExtractor:
'acodec': 'none',
'vcodec': 'none',
}
if is_drm_protected(adaptation_set) or is_drm_protected(representation):
f['has_drm'] = True
f.update(adaptation_set_drm_info)
f.update(extract_drm_info(representation))
representation_ms_info = extract_multisegment_info(representation, adaption_set_ms_info)
def prepare_template(template_name, identifiers):
@ -3036,6 +3063,86 @@ class InfoExtractor:
period_entry['subtitles'][lang or 'und'].append(f)
yield period_entry
def _extract_mpd_content_protection_info(self, cp_e, info):
"""
Extract supported DASH-CENC parameters for an MPD ContentProtection element.
Called multiple times per extracted format in an MPD (once per ContentProtection element
within AdaptationSet and Representation elements). Subclasses may override this method
when necessary (such as when the Clear Key license server URL is provided separately
from the manifest or when an extractor needs to process the optional data section in W3C
PSSH boxes).
Note that after all ContentProtection elements have been handled, the `has_drm` flag
will be set for any format that does not meet one or more of these conditions:
* `dash_cenc` is set and both `laurl` and `key_ids` are set (indicating the native
DASH downloader should use the specified Clear Key server URL to retreive the
CENC key for this format).
* `dash_cenc` is set and `key` is set (indicating the native DASH downloader should
use the specified CENC key for this format).
* `hls_aes` is set (indicating the native DASH downloader should use DASH SEA
AES-128-CBC decryption for this format).
References:
1. DASH-IF Content Protection Identifiers
https://dashif.org/identifiers/content_protection/
2. DASH-IF Content Protection Guidelines
https://dashif.org/docs/IOP-Guidelines/DASH-IF-IOP-Part6-v5.0.0.pdf
3. W3C "cenc" Initialization Data Format
https://w3c.github.io/encrypted-media/format-registry/initdata/cenc.html
"""
scheme_id = cp_e.get('schemeIdUri')
cenc_info = info.get('dash_cenc', {})
if scheme_id == 'urn:mpeg:dash:mp4protection:2011':
if cp_e.get('value') == 'cenc':
# ISO/IEC 23009-1 MPEG Common Encryption (CENC)
if not cenc_info.get('key_ids'):
try:
default_kid = uuid.UUID(cp_e.get('{urn:mpeg:cenc:2013}default_KID')).hex
cenc_info['key_ids'] = [default_kid]
except (ValueError, TypeError):
pass
elif scheme_id == 'urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e':
# Clear Key DASH-IF
for tag, ns in itertools.product(
('Laurl', 'laurl'),
('https://dashif.org/CPS', 'http://dashif.org/guidelines/clearKey'),
):
url_e = cp_e.find(self._xpath_ns(tag, ns))
if url_e is not None:
cenc_info['laurl'] = url_e.text
break
elif scheme_id == 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b':
# W3C Common System ID
pssh_e = cp_e.find(self._xpath_ns('pssh', 'urn:mpeg:cenc:2013'))
if pssh_e is not None:
# W3C PSSH box (may contain Clear Key KIDs but can also be used
# to store KIDs for other DRM systems)
try:
pssh_box = base64.b64decode(pssh_e.text)
kid_count, = struct.unpack('!L', pssh_box[28:32])
kids = []
for i in range(kid_count):
kid = pssh_box[32 + i * 16:32 + (i + 1) * 16]
kids.append(kid.hex())
cenc_info['key_ids'] = kids
except (ValueError, TypeError, struct.error):
pass
elif scheme_id == 'urn:mpeg:dash:sea:2012':
# ISO/IEC 23009-4 DASH Segment Encryption and Authentication (AES-128-CBC)
sea_ns = 'urn:mpeg:dash:schema:sea:2012'
se_e = cp_e.find(self._xpath_ns('SegmentEncryption', sea_ns))
ks_e = cp_e.find(self._xpath_ns('KeySystem', sea_ns))
crypto_e = cp_e.find(self._xpath_ns('CryptoPeriod', sea_ns))
if (se_e is not None and se_e.get('schemeIdUri') == 'urn:mpeg:dash:sea:aes128-cbc:2013'
and ks_e is not None and ks_e.get('keySystemUri') == 'urn:mpeg:dash:sea:keysys:http:2013'
and crypto_e is not None and crypto_e.get('keyUriTemplate') and crypto_e.get('IV')
):
info['hls_aes'] = {'uri': crypto_e.get('keyUriTemplate'), 'iv': crypto_e.get('IV')}
if cenc_info:
info['dash_cenc'] = cenc_info
def _extract_ism_formats(self, *args, **kwargs):
fmts, subs = self._extract_ism_formats_and_subtitles(*args, **kwargs)
if subs:

View File

@ -5,56 +5,63 @@ import hashlib
import hmac
import json
import os
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
ExtractorError,
int_or_none,
js_to_json,
remove_end,
traverse_obj,
unescapeHTML,
)
class GoPlayIE(InfoExtractor):
_VALID_URL = r'https?://(www\.)?goplay\.be/video/([^/]+/[^/]+/|)(?P<display_id>[^/#]+)'
_VALID_URL = r'https?://(www\.)?goplay\.be/video/([^/?#]+/[^/?#]+/|)(?P<id>[^/#]+)'
_NETRC_MACHINE = 'goplay'
_TESTS = [{
'url': 'https://www.goplay.be/video/de-container-cup/de-container-cup-s3/de-container-cup-s3-aflevering-2#autoplay',
'url': 'https://www.goplay.be/video/de-slimste-mens-ter-wereld/de-slimste-mens-ter-wereld-s22/de-slimste-mens-ter-wereld-s22-aflevering-1',
'info_dict': {
'id': '9c4214b8-e55d-4e4b-a446-f015f6c6f811',
'id': '2baa4560-87a0-421b-bffc-359914e3c387',
'ext': 'mp4',
'title': 'S3 - Aflevering 2',
'series': 'De Container Cup',
'season': 'Season 3',
'season_number': 3,
'episode': 'Episode 2',
'episode_number': 2,
'title': 'S22 - Aflevering 1',
'description': r're:In aflevering 1 nemen Daan Alferink, Tess Elst en Xander De Rycke .{66}',
'series': 'De Slimste Mens ter Wereld',
'episode': 'Episode 1',
'season_number': 22,
'episode_number': 1,
'season': 'Season 22',
},
'params': {'skip_download': True},
'skip': 'This video is only available for registered users',
}, {
'url': 'https://www.goplay.be/video/a-family-for-thr-holidays-s1-aflevering-1#autoplay',
'url': 'https://www.goplay.be/video/1917',
'info_dict': {
'id': '74e3ed07-748c-49e4-85a0-393a93337dbf',
'id': '40cac41d-8d29-4ef5-aa11-75047b9f0907',
'ext': 'mp4',
'title': 'A Family for the Holidays',
'title': '1917',
'description': r're:Op het hoogtepunt van de Eerste Wereldoorlog krijgen twee jonge .{94}',
},
'params': {'skip_download': True},
'skip': 'This video is only available for registered users',
}, {
'url': 'https://www.goplay.be/video/de-mol/de-mol-s11/de-mol-s11-aflevering-1#autoplay',
'info_dict': {
'id': '03eb8f2f-153e-41cb-9805-0d3a29dab656',
'id': 'ecb79672-92b9-4cd9-a0d7-e2f0250681ee',
'ext': 'mp4',
'title': 'S11 - Aflevering 1',
'description': r're:Tien kandidaten beginnen aan hun verovering van Amerika en ontmoeten .{102}',
'episode': 'Episode 1',
'series': 'De Mol',
'season_number': 11,
'episode_number': 1,
'season': 'Season 11',
},
'params': {
'skip_download': True,
},
'params': {'skip_download': True},
'skip': 'This video is only available for registered users',
}]
@ -69,27 +76,42 @@ class GoPlayIE(InfoExtractor):
if not self._id_token:
raise self.raise_login_required(method='password')
def _real_extract(self, url):
url, display_id = self._match_valid_url(url).group(0, 'display_id')
webpage = self._download_webpage(url, display_id)
video_data_json = self._html_search_regex(r'<div\s+data-hero="([^"]+)"', webpage, 'video_data')
video_data = self._parse_json(unescapeHTML(video_data_json), display_id).get('data')
def _find_json(self, s):
return self._search_json(
r'\w+\s*:\s*', s, 'next js data', None, contains_pattern=r'\[(?s:.+)\]', default=None)
movie = video_data.get('movie')
if movie:
video_id = movie['videoUuid']
info_dict = {
'title': movie.get('title'),
}
else:
episode = traverse_obj(video_data, ('playlists', ..., 'episodes', lambda _, v: v['pageInfo']['url'] == url), get_all=False)
video_id = episode['videoUuid']
info_dict = {
'title': episode.get('episodeTitle'),
'series': traverse_obj(episode, ('program', 'title')),
'season_number': episode.get('seasonNumber'),
'episode_number': episode.get('episodeNumber'),
}
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
nextjs_data = traverse_obj(
re.findall(r'<script[^>]*>\s*self\.__next_f\.push\(\s*(\[.+?\])\s*\);?\s*</script>', webpage),
(..., {js_to_json}, {json.loads}, ..., {self._find_json}, ...))
meta = traverse_obj(nextjs_data, (
..., lambda _, v: v['meta']['path'] == urllib.parse.urlparse(url).path, 'meta', any))
video_id = meta['uuid']
info_dict = traverse_obj(meta, {
'title': ('title', {str}),
'description': ('description', {str.strip}),
})
if traverse_obj(meta, ('program', 'subtype')) != 'movie':
for season_data in traverse_obj(nextjs_data, (..., 'children', ..., 'playlists', ...)):
episode_data = traverse_obj(
season_data, ('videos', lambda _, v: v['videoId'] == video_id, any))
if not episode_data:
continue
episode_title = traverse_obj(
episode_data, 'contextualTitle', 'episodeTitle', expected_type=str)
info_dict.update({
'title': episode_title or info_dict.get('title'),
'series': remove_end(info_dict.get('title'), f' - {episode_title}'),
'season_number': traverse_obj(season_data, ('season', {int_or_none})),
'episode_number': traverse_obj(episode_data, ('episodeNumber', {int_or_none})),
})
break
api = self._download_json(
f'https://api.goplay.be/web/v1/videos/long-form/{video_id}',

View File

@ -12,7 +12,7 @@ from ..utils.traversal import traverse_obj
class MixchIE(InfoExtractor):
IE_NAME = 'mixch'
_VALID_URL = r'https?://(?:www\.)?mixch\.tv/u/(?P<id>\d+)'
_VALID_URL = r'https?://mixch\.tv/u/(?P<id>\d+)'
_TESTS = [{
'url': 'https://mixch.tv/u/16943797/live',
@ -74,7 +74,7 @@ class MixchIE(InfoExtractor):
class MixchArchiveIE(InfoExtractor):
IE_NAME = 'mixch:archive'
_VALID_URL = r'https?://(?:www\.)?mixch\.tv/archive/(?P<id>\d+)'
_VALID_URL = r'https?://mixch\.tv/archive/(?P<id>\d+)'
_TESTS = [{
'url': 'https://mixch.tv/archive/421',
@ -116,3 +116,56 @@ class MixchArchiveIE(InfoExtractor):
'formats': self._extract_m3u8_formats(info_json['archiveURL'], video_id),
'thumbnail': traverse_obj(info_json, ('thumbnailURL', {url_or_none})),
}
class MixchMovieIE(InfoExtractor):
IE_NAME = 'mixch:movie'
_VALID_URL = r'https?://mixch\.tv/m/(?P<id>\w+)'
_TESTS = [{
'url': 'https://mixch.tv/m/Ve8KNkJ5',
'info_dict': {
'id': 'Ve8KNkJ5',
'title': '夏☀️\nムービーへのポイントは本イベントに加算されないので配信にてお願い致します🙇🏻\u200d♀️\n#TGCCAMPUS #ミス東大 #ミス東大2024 ',
'ext': 'mp4',
'uploader': 'ミス東大No.5 松藤百香🍑💫',
'uploader_id': '12299174',
'channel_follower_count': int,
'view_count': int,
'like_count': int,
'comment_count': int,
'timestamp': 1724070828,
'uploader_url': 'https://mixch.tv/u/12299174',
'live_status': 'not_live',
'upload_date': '20240819',
},
}, {
'url': 'https://mixch.tv/m/61DzpIKE',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
data = self._download_json(
f'https://mixch.tv/api-web/movies/{video_id}', video_id)
return {
'id': video_id,
'formats': [{
'format_id': 'mp4',
'url': data['movie']['file'],
'ext': 'mp4',
}],
**traverse_obj(data, {
'title': ('movie', 'title', {str}),
'thumbnail': ('movie', 'thumbnailURL', {url_or_none}),
'uploader': ('ownerInfo', 'name', {str}),
'uploader_id': ('ownerInfo', 'id', {int}, {str_or_none}),
'channel_follower_count': ('ownerInfo', 'fan', {int_or_none}),
'view_count': ('ownerInfo', 'view', {int_or_none}),
'like_count': ('movie', 'favCount', {int_or_none}),
'comment_count': ('movie', 'commentCount', {int_or_none}),
'timestamp': ('movie', 'published', {int_or_none}),
'uploader_url': ('ownerInfo', 'id', {lambda x: x and f'https://mixch.tv/u/{x}'}, filter),
}),
'live_status': 'not_live',
}

View File

@ -2,6 +2,7 @@ import itertools
from .common import InfoExtractor
from ..utils import (
filter_dict,
float_or_none,
int_or_none,
parse_qs,
@ -119,29 +120,46 @@ class SpreakerIE(InfoExtractor):
def _real_extract(self, url):
episode_id = self._match_id(url)
data = self._download_json(
f'https://api.spreaker.com/v2/episodes/{episode_id}',
episode_id, query=traverse_obj(parse_qs(url), {'key': ('key', 0)}))['response']['episode']
f'https://api.spreaker.com/v2/episodes/{episode_id}', episode_id,
query=traverse_obj(parse_qs(url), {'key': ('key', 0)}))['response']['episode']
return _extract_episode(data, episode_id)
class SpreakerShowIE(InfoExtractor):
_VALID_URL = r'https?://api\.spreaker\.com/show/(?P<id>\d+)'
_VALID_URL = [
r'https?://api\.spreaker\.com/show/(?P<id>\d+)',
r'https?://(?:www\.)?spreaker\.com/podcast/[\w-]+--(?P<id>[\d]+)',
r'https?://(?:www\.)?spreaker\.com/show/(?P<id>\d+)/episodes/feed',
]
_TESTS = [{
'url': 'https://api.spreaker.com/show/4652058',
'info_dict': {
'id': '4652058',
},
'playlist_mincount': 118,
}, {
'url': 'https://www.spreaker.com/podcast/health-wealth--5918323',
'info_dict': {
'id': '5918323',
},
'playlist_mincount': 60,
}, {
'url': 'https://www.spreaker.com/show/5887186/episodes/feed',
'info_dict': {
'id': '5887186',
},
'playlist_mincount': 290,
}]
def _entries(self, show_id):
def _entries(self, show_id, key=None):
for page_num in itertools.count(1):
episodes = self._download_json(
f'https://api.spreaker.com/show/{show_id}/episodes',
show_id, note=f'Downloading JSON page {page_num}', query={
show_id, note=f'Downloading JSON page {page_num}', query=filter_dict({
'page': page_num,
'max_per_page': 100,
})
'key': key,
}))
pager = try_get(episodes, lambda x: x['response']['pager'], dict)
if not pager:
break
@ -157,21 +175,5 @@ class SpreakerShowIE(InfoExtractor):
def _real_extract(self, url):
show_id = self._match_id(url)
return self.playlist_result(self._entries(show_id), playlist_id=show_id)
class SpreakerShowPageIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?spreaker\.com/show/(?P<id>[^/?#&]+)'
_TESTS = [{
'url': 'https://www.spreaker.com/show/success-with-music',
'only_matching': True,
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
show_id = self._search_regex(
r'show_id\s*:\s*(?P<id>\d+)', webpage, 'show id')
return self.url_result(
f'https://api.spreaker.com/show/{show_id}',
ie=SpreakerShowIE.ie_key(), video_id=show_id)
key = traverse_obj(parse_qs(url), ('key', 0))
return self.playlist_result(self._entries(show_id, key), playlist_id=show_id)

View File

@ -8,6 +8,7 @@ from .ffmpeg import (
FFmpegCopyStreamPP,
FFmpegEmbedSubtitlePP,
FFmpegExtractAudioPP,
FFmpegCENCDecryptPP,
FFmpegFixupDuplicateMoovPP,
FFmpegFixupDurationPP,
FFmpegFixupM3u8PP,

View File

@ -331,7 +331,7 @@ class FFmpegPostProcessor(PostProcessor):
[(path, []) for path in input_paths],
[(out_path, opts)], **kwargs)
def real_run_ffmpeg(self, input_path_opts, output_path_opts, *, expected_retcodes=(0,)):
def real_run_ffmpeg(self, input_path_opts, output_path_opts, *, prepend_opts=None, expected_retcodes=(0,)):
self.check_version()
oldest_mtime = min(
@ -342,6 +342,9 @@ class FFmpegPostProcessor(PostProcessor):
if self.basename == 'ffmpeg':
cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')]
if prepend_opts:
cmd += prepend_opts
def make_args(file, args, name, number):
keys = [f'_{name}{number}', f'_{name}']
if name == 'o':
@ -857,12 +860,23 @@ class FFmpegMergerPP(FFmpegPostProcessor):
return True
class FFmpegCENCDecryptPP(FFmpegPostProcessor):
@PostProcessor._restrict_to(images=False)
def run(self, info):
for filename, key in info.get('__files_to_cenc_decrypt', []):
temp_filename = prepend_extension(filename, 'temp')
self.to_screen(f'Decrypting "{filename}"')
self.run_ffmpeg(filename, temp_filename, self.stream_copy_opts(), prepend_opts=['-decryption_key', key])
os.replace(temp_filename, filename)
return [], info
class FFmpegFixupPostProcessor(FFmpegPostProcessor):
def _fixup(self, msg, filename, options):
def _fixup(self, msg, filename, options, prepend_opts=None):
temp_filename = prepend_extension(filename, 'temp')
self.to_screen(f'{msg} of "{filename}"')
self.run_ffmpeg(filename, temp_filename, options)
self.run_ffmpeg(filename, temp_filename, options, prepend_opts=prepend_opts)
os.replace(temp_filename, filename)
@ -934,7 +948,11 @@ class FFmpegCopyStreamPP(FFmpegFixupPostProcessor):
@PostProcessor._restrict_to(images=False)
def run(self, info):
self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts())
self._fixup(
self.MESSAGE,
info['filepath'],
self.stream_copy_opts(),
)
return [], info